@kimuson/claude-code-viewer 0.4.3 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -63,6 +63,40 @@ claude-code-viewer
63
63
 
64
64
  The server will start on port 3400 (or your specified PORT). Open `http://localhost:3400` in your browser to access the interface.
65
65
 
66
+ ### Docker Deployment
67
+
68
+ Build the image locally:
69
+
70
+ ```bash
71
+ docker build -t claude-code-viewer .
72
+ ```
73
+
74
+ Run the container directly:
75
+
76
+ ```bash
77
+ docker run --rm -p 3400:3400 \
78
+ -e ANTHROPIC_BASE_URL=... \
79
+ -e ANTHROPIC_API_KEY=... \
80
+ -e ANTHROPIC_AUTH_TOKEN=... \
81
+ claude-code-viewer
82
+ ```
83
+
84
+ Alternatively, use the provided Compose configuration:
85
+
86
+ ```bash
87
+ docker compose up --build
88
+ ```
89
+
90
+ > Note: `docker-compose.yml` ships without mounting `claude_home` by default. If you need the container to reuse an existing Claude workspace, map a volume to `/root/.claude`, for example:
91
+ >
92
+ > ```yaml
93
+ > services:
94
+ > app:
95
+ > volumes:
96
+ > - /path/to/claude_home:/root/.claude
97
+ > ```
98
+
99
+
66
100
  ## Data Source
67
101
 
68
102
  The application reads Claude Code conversation logs from:
@@ -110,6 +144,11 @@ Settings can be configured from the sidebar in Claude Code Viewer.
110
144
  | Notifications | None | Enables sound notifications when running session processes complete. Choose from multiple notification sounds with test playback functionality. |
111
145
  | Language | System | Interface language selection. Supports English and Japanese with automatic system detection. |
112
146
 
147
+ ## Internationalization (i18n)
148
+
149
+ Claude Code Viewer currently supports **English** and **Japanese**. Adding new languages is straightforward—simply add a new `messages.json` file for your locale (see [src/i18n/locales/](./src/i18n/locales/) for examples).
150
+
151
+ However, we haven't added other languages yet because we're uncertain about demand. **If you'd like support for your language, please open an issue**—we'll add it quickly!
113
152
 
114
153
  ## Alternatives & Differentiation
115
154
 
package/dist/main.js CHANGED
@@ -1181,13 +1181,64 @@ import { Context as Context17, Effect as Effect20, Layer as Layer18 } from "effe
1181
1181
 
1182
1182
  // src/server/core/platform/services/UserConfigService.ts
1183
1183
  import { Context as Context11, Effect as Effect13, Layer as Layer12, Ref as Ref5 } from "effect";
1184
+
1185
+ // src/lib/i18n/localeDetection.ts
1186
+ var DEFAULT_LOCALE = "en";
1187
+ var normalizeTag = (tag) => {
1188
+ if (!tag) {
1189
+ return void 0;
1190
+ }
1191
+ const normalized = tag.trim().toLowerCase().replaceAll("_", "-");
1192
+ if (normalized.length === 0 || normalized === "*") {
1193
+ return void 0;
1194
+ }
1195
+ if (normalized.startsWith("zh")) {
1196
+ return "zh_CN";
1197
+ }
1198
+ if (normalized.startsWith("ja") || normalized.startsWith("jp")) {
1199
+ return "ja";
1200
+ }
1201
+ if (normalized.startsWith("en")) {
1202
+ return "en";
1203
+ }
1204
+ return void 0;
1205
+ };
1206
+ var detectLocaleFromAcceptLanguage = (header) => {
1207
+ if (!header) {
1208
+ return void 0;
1209
+ }
1210
+ const preferences = header.split(",").map((part, index) => {
1211
+ const [rawTag, ...params] = part.trim().split(";");
1212
+ const qParam = params.map((param) => param.trim()).find((param) => param.startsWith("q="));
1213
+ const quality = qParam ? Number.parseFloat(qParam.slice(2)) : 1;
1214
+ return {
1215
+ tag: rawTag,
1216
+ quality: Number.isNaN(quality) ? 1 : quality,
1217
+ index
1218
+ };
1219
+ }).filter((item) => Boolean(item.tag)).sort((a, b) => {
1220
+ if (b.quality !== a.quality) {
1221
+ return b.quality - a.quality;
1222
+ }
1223
+ return a.index - b.index;
1224
+ });
1225
+ for (const preference of preferences) {
1226
+ const locale = normalizeTag(preference.tag);
1227
+ if (locale) {
1228
+ return locale;
1229
+ }
1230
+ }
1231
+ return void 0;
1232
+ };
1233
+
1234
+ // src/server/core/platform/services/UserConfigService.ts
1184
1235
  var LayerImpl10 = Effect13.gen(function* () {
1185
1236
  const configRef = yield* Ref5.make({
1186
1237
  hideNoUserMessageSession: true,
1187
1238
  unifySameTitleSession: false,
1188
1239
  enterKeyBehavior: "shift-enter-send",
1189
1240
  permissionMode: "default",
1190
- locale: "ja",
1241
+ locale: DEFAULT_LOCALE,
1191
1242
  theme: "system"
1192
1243
  });
1193
1244
  const setUserConfig = (newConfig) => Effect13.gen(function* () {
@@ -1377,6 +1428,97 @@ var VirtualConversationDatabase = class extends Context12.Tag(
1377
1428
  import { FileSystem as FileSystem5 } from "@effect/platform";
1378
1429
  import { Context as Context13, Effect as Effect15, Layer as Layer14, Ref as Ref7 } from "effect";
1379
1430
 
1431
+ // src/server/core/session/constants/pricing.ts
1432
+ var MODEL_PRICING = {
1433
+ "claude-3.5-sonnet": {
1434
+ input: 3,
1435
+ output: 15,
1436
+ cache_creation: 3.75,
1437
+ cache_read: 0.3
1438
+ },
1439
+ "claude-3-opus": {
1440
+ input: 15,
1441
+ output: 75,
1442
+ cache_creation: 18.75,
1443
+ cache_read: 1.5
1444
+ },
1445
+ "claude-3-haiku": {
1446
+ input: 0.25,
1447
+ output: 1.25,
1448
+ cache_creation: 0.3,
1449
+ cache_read: 0.03
1450
+ },
1451
+ "claude-instant-1.2": {
1452
+ input: 1.63,
1453
+ output: 5.51,
1454
+ cache_creation: 2.0375,
1455
+ // 1.63 * 1.25
1456
+ cache_read: 0.163
1457
+ // 1.63 * 0.1
1458
+ },
1459
+ "claude-2": {
1460
+ input: 8,
1461
+ output: 24,
1462
+ cache_creation: 10,
1463
+ // 8.0 * 1.25
1464
+ cache_read: 0.8
1465
+ // 8.0 * 0.1
1466
+ }
1467
+ };
1468
+ var DEFAULT_MODEL_PRICING = MODEL_PRICING["claude-3.5-sonnet"];
1469
+
1470
+ // src/server/core/session/functions/calculateSessionCost.ts
1471
+ function normalizeModelName(modelName) {
1472
+ const normalized = modelName.toLowerCase();
1473
+ if (normalized.includes("sonnet-4") || normalized.includes("3-5-sonnet") || normalized.includes("3.5-sonnet")) {
1474
+ return "claude-3.5-sonnet";
1475
+ }
1476
+ if (normalized.includes("3-opus") || normalized.includes("opus-20")) {
1477
+ return "claude-3-opus";
1478
+ }
1479
+ if (normalized.includes("3-haiku") || normalized.includes("haiku-20")) {
1480
+ return "claude-3-haiku";
1481
+ }
1482
+ if (normalized.includes("instant-1.2") || normalized.includes("instant-1")) {
1483
+ return "claude-instant-1.2";
1484
+ }
1485
+ if (normalized.startsWith("claude-2")) {
1486
+ return "claude-2";
1487
+ }
1488
+ return "claude-3.5-sonnet";
1489
+ }
1490
+ function getModelPricing(modelName) {
1491
+ const normalized = normalizeModelName(modelName);
1492
+ return MODEL_PRICING[normalized] ?? DEFAULT_MODEL_PRICING;
1493
+ }
1494
+ function calculateTokenCost(usage, modelName) {
1495
+ const pricing = getModelPricing(modelName);
1496
+ const inputMTok = usage.input_tokens / 1e6;
1497
+ const outputMTok = usage.output_tokens / 1e6;
1498
+ const cacheCreationMTok = (usage.cache_creation_input_tokens ?? 0) / 1e6;
1499
+ const cacheReadMTok = (usage.cache_read_input_tokens ?? 0) / 1e6;
1500
+ const inputTokensUsd = inputMTok * pricing.input;
1501
+ const outputTokensUsd = outputMTok * pricing.output;
1502
+ const cacheCreationUsd = cacheCreationMTok * pricing.cache_creation;
1503
+ const cacheReadUsd = cacheReadMTok * pricing.cache_read;
1504
+ const totalUsd = inputTokensUsd + outputTokensUsd + cacheCreationUsd + cacheReadUsd;
1505
+ return {
1506
+ totalUsd,
1507
+ breakdown: {
1508
+ inputTokensUsd,
1509
+ outputTokensUsd,
1510
+ cacheCreationUsd,
1511
+ cacheReadUsd
1512
+ },
1513
+ tokenUsage: {
1514
+ inputTokens: usage.input_tokens,
1515
+ outputTokens: usage.output_tokens,
1516
+ cacheCreationTokens: usage.cache_creation_input_tokens ?? 0,
1517
+ cacheReadTokens: usage.cache_read_input_tokens ?? 0
1518
+ }
1519
+ };
1520
+ }
1521
+
1380
1522
  // src/server/core/session/functions/extractFirstUserText.ts
1381
1523
  var extractFirstUserText = (conversation) => {
1382
1524
  if (conversation.type !== "user") {
@@ -1463,6 +1605,68 @@ var SessionMetaService = class extends Context13.Tag("SessionMetaService")() {
1463
1605
  }
1464
1606
  return firstUserMessage;
1465
1607
  });
1608
+ const aggregateTokenUsageAndCost = (content) => {
1609
+ let totalInputTokens = 0;
1610
+ let totalOutputTokens = 0;
1611
+ let totalCacheCreationTokens = 0;
1612
+ let totalCacheReadTokens = 0;
1613
+ let totalInputTokensUsd = 0;
1614
+ let totalOutputTokensUsd = 0;
1615
+ let totalCacheCreationUsd = 0;
1616
+ let totalCacheReadUsd = 0;
1617
+ let lastModelName = "claude-3.5-sonnet";
1618
+ const conversations = parseJsonl(content);
1619
+ for (const conversation of conversations) {
1620
+ if (conversation.type === "assistant") {
1621
+ const usage = conversation.message.usage;
1622
+ const modelName = conversation.message.model;
1623
+ const messageCost = calculateTokenCost(
1624
+ {
1625
+ input_tokens: usage.input_tokens,
1626
+ output_tokens: usage.output_tokens,
1627
+ cache_creation_input_tokens: usage.cache_creation_input_tokens ?? 0,
1628
+ cache_read_input_tokens: usage.cache_read_input_tokens ?? 0
1629
+ },
1630
+ modelName
1631
+ );
1632
+ totalInputTokens += usage.input_tokens;
1633
+ totalOutputTokens += usage.output_tokens;
1634
+ totalCacheCreationTokens += usage.cache_creation_input_tokens ?? 0;
1635
+ totalCacheReadTokens += usage.cache_read_input_tokens ?? 0;
1636
+ totalInputTokensUsd += messageCost.breakdown.inputTokensUsd;
1637
+ totalOutputTokensUsd += messageCost.breakdown.outputTokensUsd;
1638
+ totalCacheCreationUsd += messageCost.breakdown.cacheCreationUsd;
1639
+ totalCacheReadUsd += messageCost.breakdown.cacheReadUsd;
1640
+ lastModelName = modelName;
1641
+ }
1642
+ }
1643
+ const totalCost = {
1644
+ totalUsd: totalInputTokensUsd + totalOutputTokensUsd + totalCacheCreationUsd + totalCacheReadUsd,
1645
+ breakdown: {
1646
+ inputTokensUsd: totalInputTokensUsd,
1647
+ outputTokensUsd: totalOutputTokensUsd,
1648
+ cacheCreationUsd: totalCacheCreationUsd,
1649
+ cacheReadUsd: totalCacheReadUsd
1650
+ },
1651
+ tokenUsage: {
1652
+ inputTokens: totalInputTokens,
1653
+ outputTokens: totalOutputTokens,
1654
+ cacheCreationTokens: totalCacheCreationTokens,
1655
+ cacheReadTokens: totalCacheReadTokens
1656
+ }
1657
+ };
1658
+ const aggregatedUsage = {
1659
+ input_tokens: totalInputTokens,
1660
+ output_tokens: totalOutputTokens,
1661
+ cache_creation_input_tokens: totalCacheCreationTokens,
1662
+ cache_read_input_tokens: totalCacheReadTokens
1663
+ };
1664
+ return {
1665
+ totalUsage: aggregatedUsage,
1666
+ totalCost,
1667
+ modelName: lastModelName
1668
+ };
1669
+ };
1466
1670
  const getSessionMeta = (projectId, sessionId) => Effect15.gen(function* () {
1467
1671
  const metaCache = yield* Ref7.get(sessionMetaCacheRef);
1468
1672
  const cached = metaCache.get(sessionId);
@@ -1476,9 +1680,15 @@ var SessionMetaService = class extends Context13.Tag("SessionMetaService")() {
1476
1680
  sessionPath,
1477
1681
  lines
1478
1682
  );
1683
+ const { totalCost } = aggregateTokenUsageAndCost(content);
1479
1684
  const sessionMeta = {
1480
1685
  messageCount: lines.length,
1481
- firstUserMessage
1686
+ firstUserMessage,
1687
+ cost: {
1688
+ totalUsd: totalCost.totalUsd,
1689
+ breakdown: totalCost.breakdown,
1690
+ tokenUsage: totalCost.tokenUsage
1691
+ }
1482
1692
  };
1483
1693
  yield* Ref7.update(sessionMetaCacheRef, (cache) => {
1484
1694
  cache.set(sessionId, sessionMeta);
@@ -1569,7 +1779,22 @@ var LayerImpl11 = Effect16.gen(function* () {
1569
1779
  jsonlFilePath: `${decodeProjectId(projectId)}/${sessionId}.jsonl`,
1570
1780
  meta: {
1571
1781
  messageCount: 0,
1572
- firstUserMessage: null
1782
+ firstUserMessage: null,
1783
+ cost: {
1784
+ totalUsd: 0,
1785
+ breakdown: {
1786
+ inputTokensUsd: 0,
1787
+ outputTokensUsd: 0,
1788
+ cacheCreationUsd: 0,
1789
+ cacheReadUsd: 0
1790
+ },
1791
+ tokenUsage: {
1792
+ inputTokens: 0,
1793
+ outputTokens: 0,
1794
+ cacheCreationTokens: 0,
1795
+ cacheReadTokens: 0
1796
+ }
1797
+ }
1573
1798
  },
1574
1799
  conversations: virtualConversation.conversations,
1575
1800
  lastModifiedAt: lastConversation !== void 0 ? new Date(lastConversation.timestamp) : /* @__PURE__ */ new Date()
@@ -1670,7 +1895,22 @@ var LayerImpl11 = Effect16.gen(function* () {
1670
1895
  lastModifiedAt: last !== void 0 ? new Date(last.timestamp) : /* @__PURE__ */ new Date(),
1671
1896
  meta: {
1672
1897
  messageCount: conversations.length,
1673
- firstUserMessage: firstUserText ? parseUserMessage(firstUserText) : null
1898
+ firstUserMessage: firstUserText ? parseUserMessage(firstUserText) : null,
1899
+ cost: {
1900
+ totalUsd: 0,
1901
+ breakdown: {
1902
+ inputTokensUsd: 0,
1903
+ outputTokensUsd: 0,
1904
+ cacheCreationUsd: 0,
1905
+ cacheReadUsd: 0
1906
+ },
1907
+ tokenUsage: {
1908
+ inputTokens: 0,
1909
+ outputTokens: 0,
1910
+ cacheCreationTokens: 0,
1911
+ cacheReadTokens: 0
1912
+ }
1913
+ }
1674
1914
  }
1675
1915
  };
1676
1916
  }).sort((a, b) => {
@@ -4876,7 +5116,7 @@ import { z as z28 } from "zod";
4876
5116
  // package.json
4877
5117
  var package_default = {
4878
5118
  name: "@kimuson/claude-code-viewer",
4879
- version: "0.4.2",
5119
+ version: "0.4.4",
4880
5120
  type: "module",
4881
5121
  license: "MIT",
4882
5122
  repository: {
@@ -5127,7 +5367,7 @@ import z27 from "zod";
5127
5367
 
5128
5368
  // src/lib/i18n/schema.ts
5129
5369
  import z26 from "zod";
5130
- var localeSchema = z26.enum(["ja", "en"]);
5370
+ var localeSchema = z26.enum(["ja", "en", "zh_CN"]);
5131
5371
 
5132
5372
  // src/server/lib/config/config.ts
5133
5373
  var userConfigSchema = z27.object({
@@ -5169,6 +5409,7 @@ var configMiddleware = createMiddleware(
5169
5409
  const cookie = getCookie(c, "ccv-config");
5170
5410
  const parsed = parseUserConfig(cookie);
5171
5411
  if (cookie === void 0) {
5412
+ const preferredLocale = detectLocaleFromAcceptLanguage(c.req.header("accept-language")) ?? DEFAULT_LOCALE;
5172
5413
  setCookie(
5173
5414
  c,
5174
5415
  "ccv-config",
@@ -5177,7 +5418,7 @@ var configMiddleware = createMiddleware(
5177
5418
  unifySameTitleSession: true,
5178
5419
  enterKeyBehavior: "shift-enter-send",
5179
5420
  permissionMode: "default",
5180
- locale: "ja",
5421
+ locale: preferredLocale,
5181
5422
  theme: "system"
5182
5423
  })
5183
5424
  );