@kimuson/claude-code-viewer 0.5.2 → 0.5.4

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
@@ -138,6 +138,10 @@ The application reads Claude Code conversation logs from:
138
138
 
139
139
  **Note on Version Support**: Recent versions of Claude Code have adopted more aggressive summarization behavior. To accommodate users who prefer to pin to specific versions, Claude Code Viewer maintains compatibility with Claude Code v1.0.50 and later for the foreseeable future.
140
140
 
141
+ ### Environment Variables
142
+
143
+ **NODE_ENV Consideration**: If you have `NODE_ENV=development` set in your environment (from other projects or system configuration), the application may not work correctly. Either set `NODE_ENV=production` or leave it unset when running Claude Code Viewer.
144
+
141
145
  ## Configuration
142
146
 
143
147
  ### Command-Line Options and Environment Variables
@@ -166,6 +170,7 @@ Settings can be configured from the sidebar in Claude Code Viewer.
166
170
  | Hide sessions without user messages | true | Claude Code creates logs for operations like `/compact` that aren't tied to actual tasks, which can create noise. When enabled, sessions without user messages are hidden. |
167
171
  | Unify sessions with same title | false | When resuming, Claude Code creates a new session with regenerated conversation logs. When enabled, only the latest session with the same title is displayed. |
168
172
  | Enter Key Behavior | Shift+Enter | Specifies which key combination sends messages. Options include Enter, Shift+Enter, and Command+Enter. |
173
+ | Search Hotkey | Command+K | Select the hotkey to open search dialog. Options include Ctrl+K and Command+K. |
169
174
  | Permission Mode | Ask permission | Controls the approval logic when Claude Code requests tool invocations. By default, users approve requests through the UI. This feature requires Claude Code v1.0.82 or later; earlier versions automatically approve regardless of this setting. |
170
175
  | Theme | System | Toggles between Dark Mode and Light Mode. Default follows system settings. |
171
176
  | Notifications | None | Enables sound notifications when running session processes complete. Choose from multiple notification sounds with test playback functionality. |
@@ -224,6 +229,17 @@ Claude Code Viewer is designed with remote hosting in mind. To support remote de
224
229
 
225
230
  The application features a separated client-server architecture that enables remote hosting. **Basic password authentication is available** via the `--password` command-line option or `CCV_PASSWORD` environment variable. When set, users must authenticate with the configured password before accessing the application. However, this is a simple single-password authentication mechanism without advanced features like multi-user support, role-based access control, or OAuth integration. If you require more sophisticated authentication, carefully evaluate your security requirements and implement appropriate access controls at the infrastructure level (e.g., reverse proxy with OAuth, VPN, IP whitelisting).
226
231
 
232
+ ## Privacy and Network Communication
233
+
234
+ Claude Code Viewer is designed with privacy in mind:
235
+
236
+ - **Localhost-Only Communication**: The application runs a web client and API server on localhost, communicating exclusively between your browser and the local server
237
+ - **Anthropic API Access**: Claude Code is invoked via the Claude Agent SDK, which handles communication to the Anthropic API. No other external services are contacted
238
+ - **No Tracking or Telemetry**: The application does not collect crash reports, usage statistics, or any other telemetry. Tracking for Claude Code itself follows the settings configured in Claude Code's own configuration
239
+ - **Network Isolation**: The application functions correctly even if network access is restricted to only the Anthropic API and the localhost port. There are no plans to add external network dependencies in the future
240
+
241
+ If you have concerns about network access, you can verify that the application only communicates with the Anthropic API and localhost by monitoring network traffic.
242
+
227
243
  ## License
228
244
 
229
245
  This project is available under the MIT License.
package/dist/main.js CHANGED
@@ -7,7 +7,7 @@ import { Effect as Effect49 } from "effect";
7
7
  // package.json
8
8
  var package_default = {
9
9
  name: "@kimuson/claude-code-viewer",
10
- version: "0.5.2",
10
+ version: "0.5.4",
11
11
  description: "A full-featured web-based Claude Code client that provides complete interactive functionality for managing Claude Code projects.",
12
12
  type: "module",
13
13
  license: "MIT",
@@ -51,9 +51,9 @@ var package_default = {
51
51
  prepare: "lefthook install"
52
52
  },
53
53
  dependencies: {
54
- "@anthropic-ai/claude-agent-sdk": "0.1.30",
54
+ "@anthropic-ai/claude-agent-sdk": "0.2.2",
55
55
  "@anthropic-ai/claude-code": "2.0.24",
56
- "@anthropic-ai/sdk": "0.67.0",
56
+ "@anthropic-ai/sdk": "0.71.2",
57
57
  "@effect/cluster": "0.55.0",
58
58
  "@effect/experimental": "0.57.11",
59
59
  "@effect/platform": "0.93.6",
@@ -62,7 +62,7 @@ var package_default = {
62
62
  "@effect/sql": "0.48.6",
63
63
  "@effect/workflow": "0.15.1",
64
64
  "@hono/node-server": "1.19.5",
65
- "@hono/zod-validator": "0.7.4",
65
+ "@hono/zod-validator": "0.7.6",
66
66
  "@lingui/core": "5.5.1",
67
67
  "@lingui/react": "5.5.1",
68
68
  "@radix-ui/react-avatar": "1.1.10",
@@ -668,6 +668,7 @@ var LayerImpl4 = Effect5.gen(function* () {
668
668
  globalClaudeDirectoryPath,
669
669
  "commands"
670
670
  ),
671
+ claudeSkillsDirPath: path.resolve(globalClaudeDirectoryPath, "skills"),
671
672
  claudeProjectsDirPath: path.resolve(
672
673
  globalClaudeDirectoryPath,
673
674
  "projects"
@@ -1041,6 +1042,49 @@ var scanCommandFilesRecursively = (dirPath) => Effect10.gen(function* () {
1041
1042
  })
1042
1043
  );
1043
1044
  });
1045
+ var scanSkillFilesRecursively = (dirPath) => Effect10.gen(function* () {
1046
+ const fs = yield* FileSystem5.FileSystem;
1047
+ const path = yield* Path6.Path;
1048
+ const scanDirectory = (currentPath, relativePath) => Effect10.gen(function* () {
1049
+ const exists = yield* fs.exists(currentPath);
1050
+ if (!exists) {
1051
+ return [];
1052
+ }
1053
+ const skillFilePath = path.join(currentPath, "SKILL.md");
1054
+ const skillFileExists = yield* fs.exists(skillFilePath);
1055
+ const skillNames = [];
1056
+ if (skillFileExists) {
1057
+ const skillName = relativePath.replace(/\//g, ":");
1058
+ if (skillName) {
1059
+ skillNames.push(skillName);
1060
+ }
1061
+ }
1062
+ const items = yield* fs.readDirectory(currentPath);
1063
+ const results = yield* Effect10.forEach(
1064
+ items,
1065
+ (item) => Effect10.gen(function* () {
1066
+ if (item.startsWith(".")) {
1067
+ return [];
1068
+ }
1069
+ const itemPath = path.join(currentPath, item);
1070
+ const info = yield* fs.stat(itemPath);
1071
+ if (info.type === "Directory") {
1072
+ const newRelativePath = relativePath ? `${relativePath}/${item}` : item;
1073
+ return yield* scanDirectory(itemPath, newRelativePath);
1074
+ }
1075
+ return [];
1076
+ }),
1077
+ { concurrency: "unbounded" }
1078
+ );
1079
+ return [...skillNames, ...results.flat()];
1080
+ });
1081
+ return yield* scanDirectory(dirPath, "").pipe(
1082
+ Effect10.match({
1083
+ onSuccess: (items) => items,
1084
+ onFailure: () => []
1085
+ })
1086
+ );
1087
+ });
1044
1088
 
1045
1089
  // src/server/core/claude-code/models/ClaudeCodeVersion.ts
1046
1090
  import { z as z19 } from "zod";
@@ -1093,10 +1137,8 @@ var parseMcpListOutput = (output) => {
1093
1137
  };
1094
1138
 
1095
1139
  // src/server/core/claude-code/models/ClaudeCode.ts
1096
- import { query as agentSdkQuery } from "@anthropic-ai/claude-agent-sdk";
1097
- import {
1098
- query as claudeCodeQuery
1099
- } from "@anthropic-ai/claude-code";
1140
+ import * as agentSdk from "@anthropic-ai/claude-agent-sdk";
1141
+ import * as claudeCodeSdk from "@anthropic-ai/claude-code";
1100
1142
  import { Command, Path as Path7 } from "@effect/platform";
1101
1143
  import { Data, Effect as Effect11 } from "effect";
1102
1144
  import { uniq } from "es-toolkit";
@@ -1201,10 +1243,19 @@ var getAvailableFeatures = (claudeCodeVersion) => ({
1201
1243
  minor: 0,
1202
1244
  patch: 28
1203
1245
  // Sidechain conversations stored in agent-*.jsonl since v2.0.28
1246
+ }) : false,
1247
+ runSkillsDirectly: claudeCodeVersion !== null ? greaterThanOrEqual(claudeCodeVersion, {
1248
+ major: 2,
1249
+ minor: 1,
1250
+ patch: 0
1251
+ }) || greaterThanOrEqual(claudeCodeVersion, {
1252
+ major: 2,
1253
+ minor: 0,
1254
+ patch: 77
1204
1255
  }) : false
1205
1256
  });
1206
- var query = (prompt, options) => {
1207
- const { canUseTool, permissionMode, ...baseOptions } = options;
1257
+ var query3 = (prompt, options) => {
1258
+ const { canUseTool, permissionMode, hooks, ...baseOptions } = options;
1208
1259
  return Effect11.gen(function* () {
1209
1260
  const { claudeCodeExecutablePath, claudeCodeVersion } = yield* Config;
1210
1261
  const availableFeatures = getAvailableFeatures(claudeCodeVersion);
@@ -1216,7 +1267,7 @@ var query = (prompt, options) => {
1216
1267
  }
1217
1268
  };
1218
1269
  if (availableFeatures.agentSdk) {
1219
- return agentSdkQuery({
1270
+ return agentSdk.query({
1220
1271
  prompt,
1221
1272
  options: {
1222
1273
  systemPrompt: { type: "preset", preset: "claude_code" },
@@ -1240,10 +1291,15 @@ var query = (prompt, options) => {
1240
1291
  };
1241
1292
  return fn;
1242
1293
  })();
1243
- return claudeCodeQuery({
1294
+ return claudeCodeSdk.query({
1244
1295
  prompt,
1245
1296
  options: {
1246
- ...options2,
1297
+ ...baseOptions,
1298
+ permissionMode: (
1299
+ // fallback unsupported permission modes
1300
+ permissionMode === "delegate" || permissionMode === "dontAsk" ? "bypassPermissions" : permissionMode
1301
+ ),
1302
+ hooks,
1247
1303
  canUseTool: fallbackCanUseTool
1248
1304
  }
1249
1305
  });
@@ -1300,16 +1356,25 @@ var LayerImpl9 = Effect13.gen(function* () {
1300
1356
  const getClaudeCommands = (options) => Effect13.gen(function* () {
1301
1357
  const { projectId } = options;
1302
1358
  const { project } = yield* projectRepository.getProject(projectId);
1359
+ const features = yield* claudeCodeService.getAvailableFeatures();
1303
1360
  const globalCommands = yield* scanCommandFilesRecursively(
1304
1361
  (yield* context.claudeCodePaths).claudeCommandsDirPath
1305
1362
  );
1306
1363
  const projectCommands = project.meta.projectPath === null ? [] : yield* scanCommandFilesRecursively(
1307
1364
  path.resolve(project.meta.projectPath, ".claude", "commands")
1308
1365
  );
1366
+ const globalSkills = features.runSkillsDirectly ? yield* scanSkillFilesRecursively(
1367
+ (yield* context.claudeCodePaths).claudeSkillsDirPath
1368
+ ) : [];
1369
+ const projectSkills = features.runSkillsDirectly && project.meta.projectPath !== null ? yield* scanSkillFilesRecursively(
1370
+ path.resolve(project.meta.projectPath, ".claude", "skills")
1371
+ ) : [];
1309
1372
  return {
1310
1373
  response: {
1311
1374
  globalCommands,
1312
1375
  projectCommands,
1376
+ globalSkills,
1377
+ projectSkills,
1313
1378
  defaultCommands: ["init", "compact", "security-review", "review"]
1314
1379
  },
1315
1380
  status: 200
@@ -1605,7 +1670,8 @@ var LayerImpl12 = Effect17.gen(function* () {
1605
1670
  enterKeyBehavior: "shift-enter-send",
1606
1671
  permissionMode: "default",
1607
1672
  locale: DEFAULT_LOCALE,
1608
- theme: "system"
1673
+ theme: "system",
1674
+ searchHotkey: "command-k"
1609
1675
  });
1610
1676
  const setUserConfig = (newConfig) => Effect17.gen(function* () {
1611
1677
  yield* Ref5.update(configRef, () => newConfig);
@@ -1799,12 +1865,36 @@ import { Context as Context15, Effect as Effect20, Layer as Layer17, Ref as Ref7
1799
1865
 
1800
1866
  // src/server/core/session/constants/pricing.ts
1801
1867
  var MODEL_PRICING = {
1868
+ "claude-opus-4.5": {
1869
+ input: 5,
1870
+ output: 25,
1871
+ cache_creation: 6.25,
1872
+ cache_read: 0.5
1873
+ },
1874
+ "claude-opus-4.1": {
1875
+ input: 15,
1876
+ output: 75,
1877
+ cache_creation: 18.75,
1878
+ cache_read: 1.5
1879
+ },
1880
+ "claude-sonnet-4.5": {
1881
+ input: 3,
1882
+ output: 15,
1883
+ cache_creation: 3.75,
1884
+ cache_read: 0.3
1885
+ },
1802
1886
  "claude-3.5-sonnet": {
1803
1887
  input: 3,
1804
1888
  output: 15,
1805
1889
  cache_creation: 3.75,
1806
1890
  cache_read: 0.3
1807
1891
  },
1892
+ "claude-haiku-4.5": {
1893
+ input: 1,
1894
+ output: 5,
1895
+ cache_creation: 1.25,
1896
+ cache_read: 0.1
1897
+ },
1808
1898
  "claude-3-opus": {
1809
1899
  input: 15,
1810
1900
  output: 75,
@@ -1816,22 +1906,6 @@ var MODEL_PRICING = {
1816
1906
  output: 1.25,
1817
1907
  cache_creation: 0.3,
1818
1908
  cache_read: 0.03
1819
- },
1820
- "claude-instant-1.2": {
1821
- input: 1.63,
1822
- output: 5.51,
1823
- cache_creation: 2.0375,
1824
- // 1.63 * 1.25
1825
- cache_read: 0.163
1826
- // 1.63 * 0.1
1827
- },
1828
- "claude-2": {
1829
- input: 8,
1830
- output: 24,
1831
- cache_creation: 10,
1832
- // 8.0 * 1.25
1833
- cache_read: 0.8
1834
- // 8.0 * 0.1
1835
1909
  }
1836
1910
  };
1837
1911
  var DEFAULT_MODEL_PRICING = MODEL_PRICING["claude-3.5-sonnet"];
@@ -1839,6 +1913,18 @@ var DEFAULT_MODEL_PRICING = MODEL_PRICING["claude-3.5-sonnet"];
1839
1913
  // src/server/core/session/functions/calculateSessionCost.ts
1840
1914
  function normalizeModelName(modelName) {
1841
1915
  const normalized = modelName.toLowerCase();
1916
+ if (normalized.includes("opus-4-5") || normalized.includes("opus-4.5")) {
1917
+ return "claude-opus-4.5";
1918
+ }
1919
+ if (normalized.includes("opus-4-1") || normalized.includes("opus-4.1")) {
1920
+ return "claude-opus-4.1";
1921
+ }
1922
+ if (normalized.includes("sonnet-4-5") || normalized.includes("sonnet-4.5")) {
1923
+ return "claude-sonnet-4.5";
1924
+ }
1925
+ if (normalized.includes("haiku-4-5") || normalized.includes("haiku-4.5")) {
1926
+ return "claude-haiku-4.5";
1927
+ }
1842
1928
  if (normalized.includes("sonnet-4") || normalized.includes("3-5-sonnet") || normalized.includes("3.5-sonnet")) {
1843
1929
  return "claude-3.5-sonnet";
1844
1930
  }
@@ -1848,12 +1934,6 @@ function normalizeModelName(modelName) {
1848
1934
  if (normalized.includes("3-haiku") || normalized.includes("haiku-20")) {
1849
1935
  return "claude-3-haiku";
1850
1936
  }
1851
- if (normalized.includes("instant-1.2") || normalized.includes("instant-1")) {
1852
- return "claude-instant-1.2";
1853
- }
1854
- if (normalized.startsWith("claude-2")) {
1855
- return "claude-2";
1856
- }
1857
1937
  return "claude-3.5-sonnet";
1858
1938
  }
1859
1939
  function getModelPricing(modelName) {
@@ -3051,7 +3131,7 @@ var LayerImpl15 = Effect24.gen(function* () {
3051
3131
  userConfig,
3052
3132
  sessionId: task.def.baseSessionId
3053
3133
  });
3054
- return yield* query(generateMessages(), {
3134
+ return yield* query3(generateMessages(), {
3055
3135
  resume: task.def.baseSessionId,
3056
3136
  cwd: sessionProcess.def.cwd,
3057
3137
  abortController: sessionProcess.def.abortController,
@@ -3062,6 +3142,9 @@ var LayerImpl15 = Effect24.gen(function* () {
3062
3142
  setNextMessage(input);
3063
3143
  try {
3064
3144
  for await (const message of messageIter) {
3145
+ if (sessionProcess.def.abortController.signal.aborted) {
3146
+ break;
3147
+ }
3065
3148
  const fallbackMessage = fallbackSdkMessage(message);
3066
3149
  const result = await Runtime2.runPromise(runtime)(
3067
3150
  handleMessage(fallbackMessage)
@@ -3596,6 +3679,14 @@ var LayerImpl18 = Effect29.gen(function* () {
3596
3679
  {
3597
3680
  name: "sidechain-separation",
3598
3681
  enabled: claudeCodeFeatures.sidechainSeparation
3682
+ },
3683
+ {
3684
+ name: "uuid-on-sdk-message",
3685
+ enabled: claudeCodeFeatures.uuidOnSDKMessage
3686
+ },
3687
+ {
3688
+ name: "run-skills-directly",
3689
+ enabled: claudeCodeFeatures.runSkillsDirectly
3599
3690
  }
3600
3691
  ]
3601
3692
  },
@@ -5656,14 +5747,14 @@ var LayerImpl26 = Effect40.gen(function* () {
5656
5747
  yield* Ref12.set(indexCacheRef, { index, documents, builtAt: now });
5657
5748
  return { index, documents };
5658
5749
  });
5659
- const search = (query2, limit = 20, projectId) => Effect40.gen(function* () {
5750
+ const search = (query4, limit = 20, projectId) => Effect40.gen(function* () {
5660
5751
  const { claudeProjectsDirPath } = yield* context.claudeCodePaths;
5661
5752
  const dirExists = yield* fs.exists(claudeProjectsDirPath);
5662
5753
  if (!dirExists) {
5663
5754
  return { results: [] };
5664
5755
  }
5665
5756
  const { index: miniSearch, documents } = yield* getIndex();
5666
- const searchResults = miniSearch.search(query2).slice(0, limit * 2);
5757
+ const searchResults = miniSearch.search(query4).slice(0, limit * 2);
5667
5758
  const results = [];
5668
5759
  for (const result of searchResults) {
5669
5760
  if (results.length >= limit) break;
@@ -5673,7 +5764,7 @@ var LayerImpl26 = Effect40.gen(function* () {
5673
5764
  const score = doc.type === "user" ? result.score * 1.2 : result.score;
5674
5765
  const snippetLength = 150;
5675
5766
  const text = doc.text;
5676
- const queryLower = query2.toLowerCase();
5767
+ const queryLower = query4.toLowerCase();
5677
5768
  const textLower = text.toLowerCase();
5678
5769
  const matchIndex = textLower.indexOf(queryLower);
5679
5770
  let snippet;
@@ -5713,8 +5804,8 @@ var SearchService = class extends Context32.Tag("SearchService")() {
5713
5804
  var LayerImpl27 = Effect41.gen(function* () {
5714
5805
  const searchService = yield* SearchService;
5715
5806
  const search = (options) => Effect41.gen(function* () {
5716
- const { query: query2, limit, projectId } = options;
5717
- if (query2.trim().length < 2) {
5807
+ const { query: query4, limit, projectId } = options;
5808
+ if (query4.trim().length < 2) {
5718
5809
  return {
5719
5810
  status: 400,
5720
5811
  response: {
@@ -5723,7 +5814,7 @@ var LayerImpl27 = Effect41.gen(function* () {
5723
5814
  };
5724
5815
  }
5725
5816
  const { results } = yield* searchService.search(
5726
- query2.trim(),
5817
+ query4.trim(),
5727
5818
  limit,
5728
5819
  projectId
5729
5820
  );
@@ -6881,7 +6972,8 @@ var userConfigSchema = z27.object({
6881
6972
  enterKeyBehavior: z27.enum(["shift-enter-send", "enter-send", "command-enter-send"]).optional().default("shift-enter-send"),
6882
6973
  permissionMode: z27.enum(["acceptEdits", "bypassPermissions", "default", "plan"]).optional().default("default"),
6883
6974
  locale: localeSchema.optional().default("en"),
6884
- theme: z27.enum(["light", "dark", "system"]).optional().default("system")
6975
+ theme: z27.enum(["light", "dark", "system"]).optional().default("system"),
6976
+ searchHotkey: z27.enum(["ctrl-k", "command-k"]).optional().default("command-k")
6885
6977
  });
6886
6978
  var defaultUserConfig = userConfigSchema.parse({});
6887
6979