From bfef29625b4739bceca2b6d546ecf715340830c5 Mon Sep 17 00:00:00 2001 From: zenmush <122081925+zenmush@users.noreply.github.com> Date: Sat, 31 May 2025 02:08:02 +0900 Subject: [PATCH 1/4] feat(config): add persistent configuration support with conf library MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add conf dependency for cross-platform config storage - Create config module with currency preference management - Support USD and JPY currency options with persistence 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- bun.lock | 38 ++++++++++++++++++++++++++++++++++++++ config.ts | 39 +++++++++++++++++++++++++++++++++++++++ package.json | 6 +++++- 3 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 config.ts diff --git a/bun.lock b/bun.lock index 3a010ddb..6e177af8 100644 --- a/bun.lock +++ b/bun.lock @@ -3,9 +3,13 @@ "workspaces": { "": { "name": "claude-code-tools", + "dependencies": { + "conf": "^13.1.0", + }, "devDependencies": { "@biomejs/biome": "^1.9.4", "@types/bun": "^1.2.15", + "@types/conf": "^3.0.3", "bumpp": "^10.1.1", "clean-pkg-json": "^1.3.0", "cli-table3": "^0.6.5", @@ -114,10 +118,16 @@ "@types/bun": ["@types/bun@1.2.15", "", { "dependencies": { "bun-types": "1.2.15" } }, "sha512-U1ljPdBEphF0nw1MIk0hI7kPg7dFdPyM7EenHsp6W5loNHl7zqy6JQf/RKCgnUn2KDzUpkBwHPnEJEjII594bA=="], + "@types/conf": ["@types/conf@3.0.3", "", { "dependencies": { "conf": "*" } }, "sha512-GtMsT4J4YSzk34kslsAeCzX0/9dGrj3q7KLO1rN2bhS/VxqxGycPhEcF4c2i1JMEwNtWCNWcaKo4yIhVZa2B0w=="], + "@types/node": ["@types/node@22.15.24", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-w9CZGm9RDjzTh/D+hFwlBJ3ziUaVw7oufKA3vOFSOZlzmW9AkZnfjPb+DLnrV6qtgL/LNmP0/2zBNCFHL3F0ng=="], "acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="], + "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + "ansi-escapes": ["ansi-escapes@7.0.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw=="], "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -132,6 +142,8 @@ "ast-kit": ["ast-kit@2.0.0", "", { "dependencies": { "@babel/parser": "^7.27.2", "pathe": "^2.0.3" } }, "sha512-P63jzlYNz96MF9mCcprU+a7I5/ZQ5QAn3y+mZcPWEcGV3CHF/GWnkFPj3oCrWLUjL47+PD9PNiCUdXxw0cWdsg=="], + "atomically": ["atomically@2.0.3", "", { "dependencies": { "stubborn-fs": "^1.2.5", "when-exit": "^2.1.1" } }, "sha512-kU6FmrwZ3Lx7/7y3hPS5QnbJfaohcIul5fGqf7ok+4KklIEk9tJ0C2IQPdacSbVUWv6zVHXEBWoWd6NrVMT7Cw=="], + "birpc": ["birpc@2.3.0", "", {}, "sha512-ijbtkn/F3Pvzb6jHypHRyve2QApOCZDR25D/VnkY2G/lBNcXCTsnsCxgY4k4PkVB7zfwzYbY3O9Lcqe3xufS5g=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], @@ -162,12 +174,16 @@ "commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="], + "conf": ["conf@13.1.0", "", { "dependencies": { "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "atomically": "^2.0.3", "debounce-fn": "^6.0.0", "dot-prop": "^9.0.0", "env-paths": "^3.0.0", "json-schema-typed": "^8.0.1", "semver": "^7.6.3", "uint8array-extras": "^1.4.0" } }, "sha512-Bi6v586cy1CoTFViVO4lGTtx780lfF96fUmS1lSX6wpZf6330NvHUu6fReVuDP1de8Mg0nkZb01c8tAQdz1o3w=="], + "confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="], "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "debounce-fn": ["debounce-fn@6.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-rBMW+F2TXryBwB54Q0d8drNEI+TfoS9JpNTAoVpukbWEhjXQq4rySFYLaqXMFXwdv61Zb2OHtj5bviSoimqxRQ=="], + "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], @@ -176,6 +192,8 @@ "diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="], + "dot-prop": ["dot-prop@9.0.0", "", { "dependencies": { "type-fest": "^4.18.2" } }, "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ=="], + "dotenv": ["dotenv@16.5.0", "", {}, "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg=="], "dts-resolver": ["dts-resolver@2.0.1", "", { "peerDependencies": { "oxc-resolver": "^9.0.2" }, "optionalPeers": ["oxc-resolver"] }, "sha512-Pe2kqaQTNVxleYpt9Q9658fn6rEpoZbMbDpEBbcU6pnuGM3Q0IdM+Rv67kN6qcyp8Bv2Uv9NYy5Y1rG1LSgfoQ=="], @@ -184,6 +202,8 @@ "empathic": ["empathic@1.1.0", "", {}, "sha512-rsPft6CK3eHtrlp9Y5ALBb+hfK+DWnA4WFebbazxjWyx8vSm3rZeoM3z9irsjcqO3PYRzlfv27XIB4tz2DV7RA=="], + "env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="], + "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], @@ -194,6 +214,10 @@ "exsolve": ["exsolve@1.0.5", "", {}, "sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-uri": ["fast-uri@3.0.6", "", {}, "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw=="], + "fdir": ["fdir@6.4.5", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw=="], "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], @@ -226,6 +250,10 @@ "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "json-schema-typed": ["json-schema-typed@8.0.1", "", {}, "sha512-XQmWYj2Sm4kn4WeTYvmpKEbyPsL7nBsb647c7pMe6l02/yx2+Jfc4dT6UZkEXnIUb5LhD55r2HPsJ1milQ4rDg=="], + "jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="], "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], @@ -282,6 +310,8 @@ "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], @@ -314,6 +344,8 @@ "strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="], + "stubborn-fs": ["stubborn-fs@1.2.5", "", {}, "sha512-H2N9c26eXjzL/S/K+i/RHHcFanE74dptvvjM8iwzwbVcWY/zjBbgRqF3K0DY4+OD+uTTASTBvDoxPDaPN02D7g=="], + "tinyexec": ["tinyexec@1.0.1", "", {}, "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw=="], "tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], @@ -324,8 +356,12 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], + "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + "uint8array-extras": ["uint8array-extras@1.4.0", "", {}, "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ=="], + "unconfig": ["unconfig@7.3.2", "", { "dependencies": { "@quansync/fs": "^0.1.1", "defu": "^6.1.4", "jiti": "^2.4.2", "quansync": "^0.2.8" } }, "sha512-nqG5NNL2wFVGZ0NA/aCFw0oJ2pxSf1lwg4Z5ill8wd7K4KX/rQbHlwbh+bjctXL5Ly1xtzHenHGOK0b+lG6JVg=="], "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], @@ -340,6 +376,8 @@ "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], + "when-exit": ["when-exit@2.1.4", "", {}, "sha512-4rnvd3A1t16PWzrBUcSDZqcAmsUIy4minDXT/CZ8F2mVDgd65i4Aalimgz1aQkRGU0iH5eT5+6Rx2TK8o443Pg=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="], diff --git a/config.ts b/config.ts new file mode 100644 index 00000000..3f4b6dc5 --- /dev/null +++ b/config.ts @@ -0,0 +1,39 @@ +import Conf from "conf"; +import type { Schema } from "conf"; + +export type Currency = "USD" | "JPY"; + +interface ConfigSchema { + currency: Currency; +} + +const schema: Schema = { + currency: { + type: "string", + enum: ["USD", "JPY"], + default: "USD", + }, +}; + +const config = new Conf({ + projectName: "ccusage", + schema, + defaults: { + currency: "USD", + }, +}); + +export function getCurrency(): Currency { + return config.get("currency"); +} + +export function setCurrency(currency: Currency): void { + if (!["USD", "JPY"].includes(currency)) { + throw new Error(`Invalid currency: ${currency}. Must be USD or JPY.`); + } + config.set("currency", currency); +} + +export function getConfigPath(): string { + return config.path; +} diff --git a/package.json b/package.json index 2b3d5d50..c8f58dc3 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "devDependencies": { "@biomejs/biome": "^1.9.4", "@types/bun": "^1.2.15", + "@types/conf": "^3.0.3", "bumpp": "^10.1.1", "clean-pkg-json": "^1.3.0", "cli-table3": "^0.6.5", @@ -51,5 +52,8 @@ "unplugin-unused": "^0.4.4", "valibot": "^1.1.0" }, - "trustedDependencies": ["@biomejs/biome", "simple-git-hooks"] + "trustedDependencies": ["@biomejs/biome", "simple-git-hooks"], + "dependencies": { + "conf": "^13.1.0" + } } From 6d9a0d4a0cfea7a892bbd10cc9b26663a47fa5c8 Mon Sep 17 00:00:00 2001 From: zenmush <122081925+zenmush@users.noreply.github.com> Date: Sat, 31 May 2025 02:08:15 +0900 Subject: [PATCH 2/4] feat(currency): implement USD to JPY conversion with proper formatting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add currency conversion module with 1:150 exchange rate - Update formatCurrency to support multiple currencies - Format JPY with ¥ symbol and no decimals - Add comma separators for better readability 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- currency.ts | 44 ++++++++++++++++++++++++++++++++++++++++++++ utils.ts | 11 +++++++++-- 2 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 currency.ts diff --git a/currency.ts b/currency.ts new file mode 100644 index 00000000..c7393abb --- /dev/null +++ b/currency.ts @@ -0,0 +1,44 @@ +import type { Currency } from "./config"; + +const CONVERSION_RATES: Record = { + USD: 1, + JPY: 150, +}; + +const CURRENCY_SYMBOLS: Record = { + USD: "$", + JPY: "¥", +}; + +const CURRENCY_DECIMALS: Record = { + USD: 2, + JPY: 0, +}; + +export function convertFromUSD( + amountUSD: number, + toCurrency: Currency, +): number { + const rate = CONVERSION_RATES[toCurrency]; + return amountUSD * rate; +} + +export function formatCurrencyAmount( + amount: number, + currency: Currency, +): string { + const symbol = CURRENCY_SYMBOLS[currency]; + const decimals = CURRENCY_DECIMALS[currency]; + + // Format with proper locale (includes comma separators) + const formattedAmount = amount.toLocaleString("en-US", { + minimumFractionDigits: decimals, + maximumFractionDigits: decimals, + }); + + return `${symbol}${formattedAmount}`; +} + +export function getCurrencyLabel(currency: Currency): string { + return `Cost (${currency})`; +} diff --git a/utils.ts b/utils.ts index 435e1156..2756d2cb 100644 --- a/utils.ts +++ b/utils.ts @@ -1,7 +1,14 @@ +import type { Currency } from "./config"; +import { convertFromUSD, formatCurrencyAmount } from "./currency"; + export const formatNumber = (num: number): string => { return num.toLocaleString("en-US"); }; -export const formatCurrency = (amount: number): string => { - return `$${amount.toFixed(2)}`; +export const formatCurrency = ( + amountUSD: number, + currency: Currency, +): string => { + const convertedAmount = convertFromUSD(amountUSD, currency); + return formatCurrencyAmount(convertedAmount, currency); }; From a8b0ca4470462b7794c0218c90272d228469698f Mon Sep 17 00:00:00 2001 From: zenmush <122081925+zenmush@users.noreply.github.com> Date: Sat, 31 May 2025 02:08:30 +0900 Subject: [PATCH 3/4] feat(cli): add --currency flag for USD/JPY display selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add --currency flag to all commands with persistence - Update daily command to use currency-aware formatting - Update session command to use currency-aware formatting - Display appropriate currency label in table headers 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- commands/daily.ts | 8 +++++--- commands/session.ts | 8 +++++--- shared-args.ts | 17 +++++++++++++++++ 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/commands/daily.ts b/commands/daily.ts index f2566993..da9d9dd6 100644 --- a/commands/daily.ts +++ b/commands/daily.ts @@ -7,6 +7,7 @@ import { createTotalsObject, getTotalTokens, } from "../calculate-cost.ts"; +import { getCurrencyLabel } from "../currency.ts"; import { type LoadOptions, loadUsageData } from "../data-loader.ts"; import { log, logger } from "../logger.ts"; import { sharedArgs } from "../shared-args.ts"; @@ -17,6 +18,7 @@ export const dailyCommand = define({ description: "Show usage report grouped by date", args: sharedArgs, async run(ctx) { + const currency = ctx.values.currency; const options: LoadOptions = { since: ctx.values.since, until: ctx.values.until, @@ -64,7 +66,7 @@ export const dailyCommand = define({ "Cache Create", "Cache Read", "Total Tokens", - "Cost (USD)", + getCurrencyLabel(currency), ], style: { head: ["cyan"], @@ -89,7 +91,7 @@ export const dailyCommand = define({ formatNumber(data.cacheCreationTokens), formatNumber(data.cacheReadTokens), formatNumber(getTotalTokens(data)), - formatCurrency(data.totalCost), + formatCurrency(data.totalCost, currency), ]); } @@ -112,7 +114,7 @@ export const dailyCommand = define({ pc.yellow(formatNumber(totals.cacheCreationTokens)), pc.yellow(formatNumber(totals.cacheReadTokens)), pc.yellow(formatNumber(getTotalTokens(totals))), - pc.yellow(formatCurrency(totals.totalCost)), + pc.yellow(formatCurrency(totals.totalCost, currency)), ]); // biome-ignore lint/suspicious/noConsole: diff --git a/commands/session.ts b/commands/session.ts index edf9bea7..7553babf 100644 --- a/commands/session.ts +++ b/commands/session.ts @@ -7,6 +7,7 @@ import { createTotalsObject, getTotalTokens, } from "../calculate-cost.ts"; +import { getCurrencyLabel } from "../currency.ts"; import { type LoadOptions, loadSessionData } from "../data-loader.ts"; import { log, logger } from "../logger.ts"; import { sharedArgs } from "../shared-args.ts"; @@ -17,6 +18,7 @@ export const sessionCommand = define({ description: "Show usage report grouped by conversation session", args: sharedArgs, async run(ctx) { + const currency = ctx.values.currency; const options: LoadOptions = { since: ctx.values.since, until: ctx.values.until, @@ -67,7 +69,7 @@ export const sessionCommand = define({ "Cache Create", "Cache Read", "Total Tokens", - "Cost (USD)", + getCurrencyLabel(currency), "Last Activity", ], style: { @@ -106,7 +108,7 @@ export const sessionCommand = define({ formatNumber(data.cacheCreationTokens), formatNumber(data.cacheReadTokens), formatNumber(getTotalTokens(data)), - formatCurrency(data.totalCost), + formatCurrency(data.totalCost, currency), data.lastActivity, ]); } @@ -133,7 +135,7 @@ export const sessionCommand = define({ pc.yellow(formatNumber(totals.cacheCreationTokens)), pc.yellow(formatNumber(totals.cacheReadTokens)), pc.yellow(formatNumber(getTotalTokens(totals))), - pc.yellow(formatCurrency(totals.totalCost)), + pc.yellow(formatCurrency(totals.totalCost, currency)), "", ]); diff --git a/shared-args.ts b/shared-args.ts index 62e040c5..a58edb2e 100644 --- a/shared-args.ts +++ b/shared-args.ts @@ -1,5 +1,6 @@ import type { Args } from "gunshi"; import * as v from "valibot"; +import { type Currency, getCurrency, setCurrency } from "./config"; import { getDefaultClaudePath } from "./data-loader"; import { dateSchema } from "./types"; @@ -11,6 +12,15 @@ const parseDateArg = (value: string): string => { return result.output; }; +const parseCurrencyArg = (value: string): Currency => { + const upperValue = value.toUpperCase() as Currency; + if (upperValue !== "USD" && upperValue !== "JPY") { + throw new TypeError(`Invalid currency: ${value}. Must be USD or JPY.`); + } + setCurrency(upperValue); + return upperValue; +}; + export const sharedArgs = { since: { type: "custom", @@ -36,4 +46,11 @@ export const sharedArgs = { description: "Output in JSON format", default: false, }, + currency: { + type: "custom", + short: "c", + description: "Display currency (USD or JPY)", + parse: parseCurrencyArg, + default: getCurrency(), + }, } as const satisfies Args; From 51b9fc240546b4176022e6f14e807a040f9543b2 Mon Sep 17 00:00:00 2001 From: zenmush <122081925+zenmush@users.noreply.github.com> Date: Sat, 31 May 2025 02:08:44 +0900 Subject: [PATCH 4/4] test(currency): add comprehensive tests and documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add tests for currency conversion functions - Add tests for configuration persistence - Update existing tests for new formatCurrency signature - Add currency feature documentation to README 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- README.md | 24 ++++++++++++++++++- config.test.ts | 62 ++++++++++++++++++++++++++++++++++++++++++++++++ currency.test.ts | 49 ++++++++++++++++++++++++++++++++++++++ utils.test.ts | 46 ++++++++++++++++++++++++----------- 4 files changed, 166 insertions(+), 15 deletions(-) create mode 100644 config.test.ts create mode 100644 currency.test.ts diff --git a/README.md b/README.md index 66b29972..f092c07d 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,8 @@ This tool helps you understand the value you're getting from your subscription b - 📁 **Custom Path**: Support for custom Claude data directory locations - 🎨 **Beautiful Output**: Colorful table-formatted display - 📄 **JSON Output**: Export data in structured JSON format with `--json` -- 💰 **Cost Tracking**: Shows costs in USD for each day/session +- 💰 **Cost Tracking**: Shows costs in USD or JPY for each day/session +- 💱 **Currency Support**: Switch between USD and JPY display (1 USD = 150 JPY) - 🔄 **Cache Token Support**: Tracks and displays cache creation and cache read tokens separately ## Limitations @@ -108,6 +109,9 @@ ccusage daily --since 20250525 --until 20250530 # Use custom Claude data directory ccusage daily --path /custom/path/to/.claude +# Display costs in Japanese Yen +ccusage daily --currency JPY + # Output in JSON format ccusage daily --json ``` @@ -128,6 +132,9 @@ ccusage session --since 20250525 # Combine filters ccusage session --since 20250525 --until 20250530 --path /custom/path +# Display costs in Japanese Yen +ccusage session --currency JPY + # Output in JSON format ccusage session --json ``` @@ -139,6 +146,7 @@ All commands support the following options: - `-s, --since `: Filter from date (YYYYMMDD format) - `-u, --until `: Filter until date (YYYYMMDD format) - `-p, --path `: Custom path to Claude data directory (default: `~/.claude`) +- `-c, --currency `: Display currency - USD or JPY (default: USD, persists across sessions) - `-j, --json`: Output results in JSON format instead of table - `-h, --help`: Display help message - `-v, --version`: Display version @@ -183,6 +191,20 @@ All commands support the following options: └─────────────┴────────────┴────────┴─────────┴──────────────┴────────────┴──────────────┴────────────┴───────────────┘ ``` +## Currency Configuration + +The tool supports displaying costs in either USD (default) or JPY (Japanese Yen): + +- **USD**: Default currency, shows costs with `$` symbol and 2 decimal places +- **JPY**: Japanese Yen, shows costs with `¥` symbol and no decimal places (rounded to whole numbers) +- **Conversion Rate**: Fixed at 1 USD = 150 JPY + +Your currency preference is saved and persists across sessions. Once you set `--currency JPY`, all future reports will display in JPY until you explicitly change it back to USD. + +Configuration is stored in: +- macOS/Linux: `~/.config/ccusage/config.json` +- Windows: `%APPDATA%\ccusage\config.json` + ## Requirements - Claude Code usage history files (`~/.claude/projects/**/*.jsonl`) diff --git a/config.test.ts b/config.test.ts new file mode 100644 index 00000000..6f6f9eef --- /dev/null +++ b/config.test.ts @@ -0,0 +1,62 @@ +import { beforeEach, describe, expect, test } from "bun:test"; +import { getConfigPath, getCurrency, setCurrency } from "./config"; + +describe("config", () => { + // Store original currency to restore after tests + let originalCurrency: "USD" | "JPY"; + + beforeEach(() => { + // Save the current currency setting + originalCurrency = getCurrency(); + }); + + describe("getCurrency", () => { + test("returns current currency setting", () => { + const currency = getCurrency(); + expect(["USD", "JPY"]).toContain(currency); + }); + }); + + describe("setCurrency", () => { + test("sets currency to USD", () => { + setCurrency("USD"); + expect(getCurrency()).toBe("USD"); + // Restore original + setCurrency(originalCurrency); + }); + + test("sets currency to JPY", () => { + setCurrency("JPY"); + expect(getCurrency()).toBe("JPY"); + // Restore original + setCurrency(originalCurrency); + }); + + test("throws error for invalid currency", () => { + // biome-ignore lint/suspicious/noExplicitAny: testing invalid input + expect(() => setCurrency("EUR" as any)).toThrow( + "Invalid currency: EUR. Must be USD or JPY.", + ); + }); + + test("persists currency setting", () => { + setCurrency("JPY"); + expect(getCurrency()).toBe("JPY"); + + setCurrency("USD"); + expect(getCurrency()).toBe("USD"); + + // Restore original + setCurrency(originalCurrency); + }); + }); + + describe("getConfigPath", () => { + test("returns a valid config path", () => { + const path = getConfigPath(); + expect(typeof path).toBe("string"); + expect(path.length).toBeGreaterThan(0); + expect(path).toContain("ccusage"); + }); + }); +}); diff --git a/currency.test.ts b/currency.test.ts new file mode 100644 index 00000000..1e1e5c80 --- /dev/null +++ b/currency.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, test } from "bun:test"; +import { + convertFromUSD, + formatCurrencyAmount, + getCurrencyLabel, +} from "./currency"; + +describe("convertFromUSD", () => { + test("converts USD to USD (no conversion)", () => { + expect(convertFromUSD(100, "USD")).toBe(100); + expect(convertFromUSD(0, "USD")).toBe(0); + expect(convertFromUSD(-50, "USD")).toBe(-50); + }); + + test("converts USD to JPY at 1:150 rate", () => { + expect(convertFromUSD(1, "JPY")).toBe(150); + expect(convertFromUSD(100, "JPY")).toBe(15000); + expect(convertFromUSD(0.5, "JPY")).toBe(75); + expect(convertFromUSD(-10, "JPY")).toBe(-1500); + }); +}); + +describe("formatCurrencyAmount", () => { + test("formats USD with 2 decimal places", () => { + expect(formatCurrencyAmount(100, "USD")).toBe("$100.00"); + expect(formatCurrencyAmount(100.5, "USD")).toBe("$100.50"); + expect(formatCurrencyAmount(100.999, "USD")).toBe("$101.00"); + expect(formatCurrencyAmount(0, "USD")).toBe("$0.00"); + }); + + test("formats JPY with 0 decimal places", () => { + expect(formatCurrencyAmount(15000, "JPY")).toBe("¥15,000"); + expect(formatCurrencyAmount(15000.99, "JPY")).toBe("¥15,001"); + expect(formatCurrencyAmount(0, "JPY")).toBe("¥0"); + expect(formatCurrencyAmount(1.5, "JPY")).toBe("¥2"); + }); + + test("handles negative amounts", () => { + expect(formatCurrencyAmount(-100, "USD")).toBe("$-100.00"); + expect(formatCurrencyAmount(-15000, "JPY")).toBe("¥-15,000"); + }); +}); + +describe("getCurrencyLabel", () => { + test("returns correct labels for currencies", () => { + expect(getCurrencyLabel("USD")).toBe("Cost (USD)"); + expect(getCurrencyLabel("JPY")).toBe("Cost (JPY)"); + }); +}); diff --git a/utils.test.ts b/utils.test.ts index 56398a9f..47b612b9 100644 --- a/utils.test.ts +++ b/utils.test.ts @@ -34,34 +34,52 @@ describe("formatNumber", () => { describe("formatCurrency", () => { test("formats positive amounts", () => { - expect(formatCurrency(10)).toBe("$10.00"); - expect(formatCurrency(100.5)).toBe("$100.50"); - expect(formatCurrency(1234.56)).toBe("$1234.56"); + expect(formatCurrency(10, "USD")).toBe("$10.00"); + expect(formatCurrency(100.5, "USD")).toBe("$100.50"); + expect(formatCurrency(1234.56, "USD")).toBe("$1,234.56"); }); test("formats zero", () => { - expect(formatCurrency(0)).toBe("$0.00"); + expect(formatCurrency(0, "USD")).toBe("$0.00"); }); test("formats negative amounts", () => { - expect(formatCurrency(-10)).toBe("$-10.00"); - expect(formatCurrency(-100.5)).toBe("$-100.50"); + expect(formatCurrency(-10, "USD")).toBe("$-10.00"); + expect(formatCurrency(-100.5, "USD")).toBe("$-100.50"); }); test("rounds to two decimal places", () => { - expect(formatCurrency(10.999)).toBe("$11.00"); - expect(formatCurrency(10.994)).toBe("$10.99"); - expect(formatCurrency(10.995)).toBe("$10.99"); // JavaScript's toFixed uses banker's rounding + expect(formatCurrency(10.999, "USD")).toBe("$11.00"); + expect(formatCurrency(10.994, "USD")).toBe("$10.99"); + expect(formatCurrency(10.995, "USD")).toBe("$11.00"); // toLocaleString rounds differently than toFixed }); test("handles small decimal values", () => { - expect(formatCurrency(0.01)).toBe("$0.01"); - expect(formatCurrency(0.001)).toBe("$0.00"); - expect(formatCurrency(0.009)).toBe("$0.01"); + expect(formatCurrency(0.01, "USD")).toBe("$0.01"); + expect(formatCurrency(0.001, "USD")).toBe("$0.00"); + expect(formatCurrency(0.009, "USD")).toBe("$0.01"); }); test("handles large numbers", () => { - expect(formatCurrency(1000000)).toBe("$1000000.00"); - expect(formatCurrency(9999999.99)).toBe("$9999999.99"); + expect(formatCurrency(1000000, "USD")).toBe("$1,000,000.00"); + expect(formatCurrency(9999999.99, "USD")).toBe("$9,999,999.99"); + }); + + test("formats JPY currency with conversion", () => { + // 1 USD = 150 JPY + expect(formatCurrency(10, "JPY")).toBe("¥1,500"); + expect(formatCurrency(100.5, "JPY")).toBe("¥15,075"); + expect(formatCurrency(1234.56, "JPY")).toBe("¥185,184"); + }); + + test("formats JPY with no decimals", () => { + expect(formatCurrency(0.01, "JPY")).toBe("¥2"); // 0.01 * 150 = 1.5, rounds to 2 + expect(formatCurrency(0.001, "JPY")).toBe("¥0"); // 0.001 * 150 = 0.15, rounds to 0 + expect(formatCurrency(0.009, "JPY")).toBe("¥1"); // 0.009 * 150 = 1.35, rounds to 1 + }); + + test("handles negative JPY amounts", () => { + expect(formatCurrency(-10, "JPY")).toBe("¥-1,500"); + expect(formatCurrency(-100.5, "JPY")).toBe("¥-15,075"); }); });