@siteboon/claude-code-ui 1.25.1 → 1.26.0

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.
Files changed (43) hide show
  1. package/README.de.md +239 -0
  2. package/README.ja.md +115 -230
  3. package/README.ko.md +116 -231
  4. package/README.md +2 -1
  5. package/README.ru.md +75 -54
  6. package/README.zh-CN.md +121 -238
  7. package/dist/assets/index-C08k8QbP.css +32 -0
  8. package/dist/assets/{index-D6EffcRY.js → index-DnXcHp5q.js} +249 -242
  9. package/dist/index.html +2 -2
  10. package/dist/sw.js +59 -3
  11. package/package.json +3 -2
  12. package/server/claude-sdk.js +106 -62
  13. package/server/cli.js +10 -7
  14. package/server/cursor-cli.js +59 -73
  15. package/server/database/db.js +142 -1
  16. package/server/database/init.sql +28 -1
  17. package/server/gemini-cli.js +46 -48
  18. package/server/gemini-response-handler.js +12 -73
  19. package/server/index.js +82 -55
  20. package/server/middleware/auth.js +2 -2
  21. package/server/openai-codex.js +43 -28
  22. package/server/projects.js +1 -1
  23. package/server/providers/claude/adapter.js +278 -0
  24. package/server/providers/codex/adapter.js +248 -0
  25. package/server/providers/cursor/adapter.js +353 -0
  26. package/server/providers/gemini/adapter.js +186 -0
  27. package/server/providers/registry.js +44 -0
  28. package/server/providers/types.js +119 -0
  29. package/server/providers/utils.js +29 -0
  30. package/server/routes/agent.js +7 -5
  31. package/server/routes/cli-auth.js +38 -0
  32. package/server/routes/codex.js +1 -19
  33. package/server/routes/gemini.js +0 -30
  34. package/server/routes/git.js +48 -20
  35. package/server/routes/messages.js +61 -0
  36. package/server/routes/plugins.js +5 -1
  37. package/server/routes/settings.js +99 -1
  38. package/server/routes/taskmaster.js +2 -2
  39. package/server/services/notification-orchestrator.js +227 -0
  40. package/server/services/vapid-keys.js +35 -0
  41. package/server/utils/plugin-loader.js +53 -4
  42. package/shared/networkHosts.js +22 -0
  43. package/dist/assets/index-WNTmA_ug.css +0 -32
package/dist/index.html CHANGED
@@ -25,11 +25,11 @@
25
25
 
26
26
  <!-- Prevent zoom on iOS -->
27
27
  <meta name="format-detection" content="telephone=no" />
28
- <script type="module" crossorigin src="/assets/index-D6EffcRY.js"></script>
28
+ <script type="module" crossorigin src="/assets/index-DnXcHp5q.js"></script>
29
29
  <link rel="modulepreload" crossorigin href="/assets/vendor-react-CdSTmIF1.js">
30
30
  <link rel="modulepreload" crossorigin href="/assets/vendor-codemirror-C8f1vU1x.js">
31
31
  <link rel="modulepreload" crossorigin href="/assets/vendor-xterm-CJZjLICi.js">
32
- <link rel="stylesheet" crossorigin href="/assets/index-WNTmA_ug.css">
32
+ <link rel="stylesheet" crossorigin href="/assets/index-C08k8QbP.css">
33
33
  </head>
34
34
  <body>
35
35
  <div id="root"></div>
package/dist/sw.js CHANGED
@@ -19,14 +19,17 @@ self.addEventListener('install', event => {
19
19
 
20
20
  // Fetch event
21
21
  self.addEventListener('fetch', event => {
22
+ // Never cache API requests or WebSocket upgrades
23
+ if (event.request.url.includes('/api/') || event.request.url.includes('/ws')) {
24
+ return;
25
+ }
26
+
22
27
  event.respondWith(
23
28
  caches.match(event.request)
24
29
  .then(response => {
25
- // Return cached response if found
26
30
  if (response) {
27
31
  return response;
28
32
  }
29
- // Otherwise fetch from network
30
33
  return fetch(event.request);
31
34
  }
32
35
  )
@@ -46,4 +49,57 @@ self.addEventListener('activate', event => {
46
49
  );
47
50
  })
48
51
  );
49
- });
52
+ self.clients.claim();
53
+ });
54
+
55
+ // Push notification event
56
+ self.addEventListener('push', event => {
57
+ if (!event.data) return;
58
+
59
+ let payload;
60
+ try {
61
+ payload = event.data.json();
62
+ } catch {
63
+ payload = { title: 'Claude Code UI', body: event.data.text() };
64
+ }
65
+
66
+ const options = {
67
+ body: payload.body || '',
68
+ icon: '/logo-256.png',
69
+ badge: '/logo-128.png',
70
+ data: payload.data || {},
71
+ tag: payload.data?.tag || `${payload.data?.sessionId || 'global'}:${payload.data?.code || 'default'}`,
72
+ renotify: true
73
+ };
74
+
75
+ event.waitUntil(
76
+ self.registration.showNotification(payload.title || 'Claude Code UI', options)
77
+ );
78
+ });
79
+
80
+ // Notification click event
81
+ self.addEventListener('notificationclick', event => {
82
+ event.notification.close();
83
+
84
+ const sessionId = event.notification.data?.sessionId;
85
+ const provider = event.notification.data?.provider || null;
86
+ const urlPath = sessionId ? `/session/${sessionId}` : '/';
87
+
88
+ event.waitUntil(
89
+ self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then(async clientList => {
90
+ for (const client of clientList) {
91
+ if (client.url.includes(self.location.origin)) {
92
+ await client.focus();
93
+ client.postMessage({
94
+ type: 'notification:navigate',
95
+ sessionId: sessionId || null,
96
+ provider,
97
+ urlPath
98
+ });
99
+ return;
100
+ }
101
+ }
102
+ return self.clients.openWindow(urlPath);
103
+ })
104
+ );
105
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siteboon/claude-code-ui",
3
- "version": "1.25.1",
3
+ "version": "1.26.0",
4
4
  "description": "A web-based UI for Claude Code CLI",
5
5
  "type": "module",
6
6
  "main": "server/index.js",
@@ -26,7 +26,7 @@
26
26
  "scripts": {
27
27
  "dev": "concurrently --kill-others \"npm run server\" \"npm run client\"",
28
28
  "server": "node server/index.js",
29
- "client": "vite --host",
29
+ "client": "vite",
30
30
  "build": "vite build",
31
31
  "preview": "vite preview",
32
32
  "typecheck": "tsc --noEmit -p tsconfig.json",
@@ -103,6 +103,7 @@
103
103
  "sqlite": "^5.1.1",
104
104
  "sqlite3": "^5.1.7",
105
105
  "tailwind-merge": "^3.3.1",
106
+ "web-push": "^3.6.7",
106
107
  "ws": "^8.14.2"
107
108
  },
108
109
  "devDependencies": {
@@ -18,6 +18,14 @@ import { promises as fs } from 'fs';
18
18
  import path from 'path';
19
19
  import os from 'os';
20
20
  import { CLAUDE_MODELS } from '../shared/modelConstants.js';
21
+ import {
22
+ createNotificationEvent,
23
+ notifyRunFailed,
24
+ notifyRunStopped,
25
+ notifyUserIfEnabled
26
+ } from './services/notification-orchestrator.js';
27
+ import { claudeAdapter } from './providers/claude/adapter.js';
28
+ import { createNormalizedMessage } from './providers/types.js';
21
29
 
22
30
  const activeSessions = new Map();
23
31
  const pendingToolApprovals = new Map();
@@ -136,7 +144,7 @@ function matchesToolPermission(entry, toolName, input) {
136
144
  * @returns {Object} SDK-compatible options
137
145
  */
138
146
  function mapCliOptionsToSDK(options = {}) {
139
- const { sessionId, cwd, toolsSettings, permissionMode, images } = options;
147
+ const { sessionId, cwd, toolsSettings, permissionMode } = options;
140
148
 
141
149
  const sdkOptions = {};
142
150
 
@@ -187,7 +195,7 @@ function mapCliOptionsToSDK(options = {}) {
187
195
  // Map model (default to sonnet)
188
196
  // Valid models: sonnet, opus, haiku, opusplan, sonnet[1m]
189
197
  sdkOptions.model = options.model || CLAUDE_MODELS.DEFAULT;
190
- console.log(`Using model: ${sdkOptions.model}`);
198
+ // Model logged at query start below
191
199
 
192
200
  // Map system prompt configuration
193
201
  sdkOptions.systemPrompt = {
@@ -298,7 +306,7 @@ function extractTokenBudget(resultMessage) {
298
306
  // This is the user's budget limit, not the model's context window
299
307
  const contextWindow = parseInt(process.env.CONTEXT_WINDOW) || 160000;
300
308
 
301
- console.log(`Token calculation: input=${inputTokens}, output=${outputTokens}, cache=${cacheReadTokens + cacheCreationTokens}, total=${totalUsed}/${contextWindow}`);
309
+ // Token calc logged via token-budget WS event
302
310
 
303
311
  return {
304
312
  used: totalUsed,
@@ -354,7 +362,7 @@ async function handleImages(command, images, cwd) {
354
362
  modifiedCommand = command + imageNote;
355
363
  }
356
364
 
357
- console.log(`Processed ${tempImagePaths.length} images to temp directory: ${tempDir}`);
365
+ // Images processed
358
366
  return { modifiedCommand, tempImagePaths, tempDir };
359
367
  } catch (error) {
360
368
  console.error('Error processing images for SDK:', error);
@@ -387,7 +395,7 @@ async function cleanupTempFiles(tempImagePaths, tempDir) {
387
395
  );
388
396
  }
389
397
 
390
- console.log(`Cleaned up ${tempImagePaths.length} temp image files`);
398
+ // Temp files cleaned
391
399
  } catch (error) {
392
400
  console.error('Error during temp file cleanup:', error);
393
401
  }
@@ -407,7 +415,7 @@ async function loadMcpConfig(cwd) {
407
415
  await fs.access(claudeConfigPath);
408
416
  } catch (error) {
409
417
  // File doesn't exist, return null
410
- console.log('No ~/.claude.json found, proceeding without MCP servers');
418
+ // No config file
411
419
  return null;
412
420
  }
413
421
 
@@ -427,7 +435,7 @@ async function loadMcpConfig(cwd) {
427
435
  // Add global MCP servers
428
436
  if (claudeConfig.mcpServers && typeof claudeConfig.mcpServers === 'object') {
429
437
  mcpServers = { ...claudeConfig.mcpServers };
430
- console.log(`Loaded ${Object.keys(mcpServers).length} global MCP servers`);
438
+ // Global MCP servers loaded
431
439
  }
432
440
 
433
441
  // Add/override with project-specific MCP servers
@@ -435,17 +443,14 @@ async function loadMcpConfig(cwd) {
435
443
  const projectConfig = claudeConfig.claudeProjects[cwd];
436
444
  if (projectConfig && projectConfig.mcpServers && typeof projectConfig.mcpServers === 'object') {
437
445
  mcpServers = { ...mcpServers, ...projectConfig.mcpServers };
438
- console.log(`Loaded ${Object.keys(projectConfig.mcpServers).length} project-specific MCP servers`);
446
+ // Project MCP servers merged
439
447
  }
440
448
  }
441
449
 
442
450
  // Return null if no servers found
443
451
  if (Object.keys(mcpServers).length === 0) {
444
- console.log('No MCP servers configured');
445
452
  return null;
446
453
  }
447
-
448
- console.log(`Total MCP servers loaded: ${Object.keys(mcpServers).length}`);
449
454
  return mcpServers;
450
455
  } catch (error) {
451
456
  console.error('Error loading MCP config:', error.message);
@@ -461,12 +466,20 @@ async function loadMcpConfig(cwd) {
461
466
  * @returns {Promise<void>}
462
467
  */
463
468
  async function queryClaudeSDK(command, options = {}, ws) {
464
- const { sessionId } = options;
469
+ const { sessionId, sessionSummary } = options;
465
470
  let capturedSessionId = sessionId;
466
471
  let sessionCreatedSent = false;
467
472
  let tempImagePaths = [];
468
473
  let tempDir = null;
469
474
 
475
+ const emitNotification = (event) => {
476
+ notifyUserIfEnabled({
477
+ userId: ws?.userId || null,
478
+ writer: ws,
479
+ event
480
+ });
481
+ };
482
+
470
483
  try {
471
484
  // Map CLI options to SDK format
472
485
  const sdkOptions = mapCliOptionsToSDK(options);
@@ -483,6 +496,26 @@ async function queryClaudeSDK(command, options = {}, ws) {
483
496
  tempImagePaths = imageResult.tempImagePaths;
484
497
  tempDir = imageResult.tempDir;
485
498
 
499
+ sdkOptions.hooks = {
500
+ Notification: [{
501
+ matcher: '',
502
+ hooks: [async (input) => {
503
+ const message = typeof input?.message === 'string' ? input.message : 'Claude requires your attention.';
504
+ emitNotification(createNotificationEvent({
505
+ provider: 'claude',
506
+ sessionId: capturedSessionId || sessionId || null,
507
+ kind: 'action_required',
508
+ code: 'agent.notification',
509
+ meta: { message, sessionName: sessionSummary },
510
+ severity: 'warning',
511
+ requiresUserAction: true,
512
+ dedupeKey: `claude:hook:notification:${capturedSessionId || sessionId || 'none'}:${message}`
513
+ }));
514
+ return {};
515
+ }]
516
+ }]
517
+ };
518
+
486
519
  sdkOptions.canUseTool = async (toolName, input, context) => {
487
520
  const requiresInteraction = TOOLS_REQUIRING_INTERACTION.has(toolName);
488
521
 
@@ -507,13 +540,17 @@ async function queryClaudeSDK(command, options = {}, ws) {
507
540
  }
508
541
 
509
542
  const requestId = createRequestId();
510
- ws.send({
511
- type: 'claude-permission-request',
512
- requestId,
513
- toolName,
514
- input,
515
- sessionId: capturedSessionId || sessionId || null
516
- });
543
+ ws.send(createNormalizedMessage({ kind: 'permission_request', requestId, toolName, input, sessionId: capturedSessionId || sessionId || null, provider: 'claude' }));
544
+ emitNotification(createNotificationEvent({
545
+ provider: 'claude',
546
+ sessionId: capturedSessionId || sessionId || null,
547
+ kind: 'action_required',
548
+ code: 'permission.required',
549
+ meta: { toolName, sessionName: sessionSummary },
550
+ severity: 'warning',
551
+ requiresUserAction: true,
552
+ dedupeKey: `claude:permission:${capturedSessionId || sessionId || 'none'}:${requestId}`
553
+ }));
517
554
 
518
555
  const decision = await waitForToolApproval(requestId, {
519
556
  timeoutMs: requiresInteraction ? 0 : undefined,
@@ -525,12 +562,7 @@ async function queryClaudeSDK(command, options = {}, ws) {
525
562
  _receivedAt: new Date(),
526
563
  },
527
564
  onCancel: (reason) => {
528
- ws.send({
529
- type: 'claude-permission-cancelled',
530
- requestId,
531
- reason,
532
- sessionId: capturedSessionId || sessionId || null
533
- });
565
+ ws.send(createNormalizedMessage({ kind: 'permission_cancelled', requestId, reason, sessionId: capturedSessionId || sessionId || null, provider: 'claude' }));
534
566
  }
535
567
  });
536
568
  if (!decision) {
@@ -560,10 +592,22 @@ async function queryClaudeSDK(command, options = {}, ws) {
560
592
  const prevStreamTimeout = process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT;
561
593
  process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT = '300000';
562
594
 
563
- const queryInstance = query({
564
- prompt: finalCommand,
565
- options: sdkOptions
566
- });
595
+ let queryInstance;
596
+ try {
597
+ queryInstance = query({
598
+ prompt: finalCommand,
599
+ options: sdkOptions
600
+ });
601
+ } catch (hookError) {
602
+ // Older/newer SDK versions may not accept hook shapes yet.
603
+ // Keep notification behavior operational via runtime events even if hook registration fails.
604
+ console.warn('Failed to initialize Claude query with hooks, retrying without hooks:', hookError?.message || hookError);
605
+ delete sdkOptions.hooks;
606
+ queryInstance = query({
607
+ prompt: finalCommand,
608
+ options: sdkOptions
609
+ });
610
+ }
567
611
 
568
612
  // Restore immediately — Query constructor already captured the value
569
613
  if (prevStreamTimeout !== undefined) {
@@ -594,39 +638,35 @@ async function queryClaudeSDK(command, options = {}, ws) {
594
638
  // Send session-created event only once for new sessions
595
639
  if (!sessionId && !sessionCreatedSent) {
596
640
  sessionCreatedSent = true;
597
- ws.send({
598
- type: 'session-created',
599
- sessionId: capturedSessionId
600
- });
601
- } else {
602
- console.log('Not sending session-created. sessionId:', sessionId, 'sessionCreatedSent:', sessionCreatedSent);
641
+ ws.send(createNormalizedMessage({ kind: 'session_created', newSessionId: capturedSessionId, sessionId: capturedSessionId, provider: 'claude' }));
603
642
  }
604
643
  } else {
605
- console.log('No session_id in message or already captured. message.session_id:', message.session_id, 'capturedSessionId:', capturedSessionId);
644
+ // session_id already captured
606
645
  }
607
646
 
608
- // Transform and send message to WebSocket
647
+ // Transform and normalize message via adapter
609
648
  const transformedMessage = transformMessage(message);
610
- ws.send({
611
- type: 'claude-response',
612
- data: transformedMessage,
613
- sessionId: capturedSessionId || sessionId || null
614
- });
649
+ const sid = capturedSessionId || sessionId || null;
650
+
651
+ // Use adapter to normalize SDK events into NormalizedMessage[]
652
+ const normalized = claudeAdapter.normalizeMessage(transformedMessage, sid);
653
+ for (const msg of normalized) {
654
+ // Preserve parentToolUseId from SDK wrapper for subagent tool grouping
655
+ if (transformedMessage.parentToolUseId && !msg.parentToolUseId) {
656
+ msg.parentToolUseId = transformedMessage.parentToolUseId;
657
+ }
658
+ ws.send(msg);
659
+ }
615
660
 
616
661
  // Extract and send token budget updates from result messages
617
662
  if (message.type === 'result') {
618
663
  const models = Object.keys(message.modelUsage || {});
619
664
  if (models.length > 0) {
620
- console.log("---> Model was sent using:", models);
665
+ // Model info available in result message
621
666
  }
622
- const tokenBudget = extractTokenBudget(message);
623
- if (tokenBudget) {
624
- console.log('Token budget from modelUsage:', tokenBudget);
625
- ws.send({
626
- type: 'token-budget',
627
- data: tokenBudget,
628
- sessionId: capturedSessionId || sessionId || null
629
- });
667
+ const tokenBudgetData = extractTokenBudget(message);
668
+ if (tokenBudgetData) {
669
+ ws.send(createNormalizedMessage({ kind: 'status', text: 'token_budget', tokenBudget: tokenBudgetData, sessionId: capturedSessionId || sessionId || null, provider: 'claude' }));
630
670
  }
631
671
  }
632
672
  }
@@ -640,14 +680,15 @@ async function queryClaudeSDK(command, options = {}, ws) {
640
680
  await cleanupTempFiles(tempImagePaths, tempDir);
641
681
 
642
682
  // Send completion event
643
- console.log('Streaming complete, sending claude-complete event');
644
- ws.send({
645
- type: 'claude-complete',
646
- sessionId: capturedSessionId,
647
- exitCode: 0,
648
- isNewSession: !sessionId && !!command
683
+ ws.send(createNormalizedMessage({ kind: 'complete', exitCode: 0, isNewSession: !sessionId && !!command, sessionId: capturedSessionId, provider: 'claude' }));
684
+ notifyRunStopped({
685
+ userId: ws?.userId || null,
686
+ provider: 'claude',
687
+ sessionId: capturedSessionId || sessionId || null,
688
+ sessionName: sessionSummary,
689
+ stopReason: 'completed'
649
690
  });
650
- console.log('claude-complete event sent');
691
+ // Complete
651
692
 
652
693
  } catch (error) {
653
694
  console.error('SDK query error:', error);
@@ -661,10 +702,13 @@ async function queryClaudeSDK(command, options = {}, ws) {
661
702
  await cleanupTempFiles(tempImagePaths, tempDir);
662
703
 
663
704
  // Send error to WebSocket
664
- ws.send({
665
- type: 'claude-error',
666
- error: error.message,
667
- sessionId: capturedSessionId || sessionId || null
705
+ ws.send(createNormalizedMessage({ kind: 'error', content: error.message, sessionId: capturedSessionId || sessionId || null, provider: 'claude' }));
706
+ notifyRunFailed({
707
+ userId: ws?.userId || null,
708
+ provider: 'claude',
709
+ sessionId: capturedSessionId || sessionId || null,
710
+ sessionName: sessionSummary,
711
+ error
668
712
  });
669
713
 
670
714
  throw error;
package/server/cli.js CHANGED
@@ -110,7 +110,7 @@ function showStatus() {
110
110
 
111
111
  // Environment variables
112
112
  console.log(`\n${c.info('[INFO]')} Configuration:`);
113
- console.log(` PORT: ${c.bright(process.env.PORT || '3001')} ${c.dim(process.env.PORT ? '' : '(default)')}`);
113
+ console.log(` SERVER_PORT: ${c.bright(process.env.SERVER_PORT || process.env.PORT || '3001')} ${c.dim(process.env.SERVER_PORT || process.env.PORT ? '' : '(default)')}`);
114
114
  console.log(` DATABASE_PATH: ${c.dim(process.env.DATABASE_PATH || '(using default location)')}`);
115
115
  console.log(` CLAUDE_CLI_PATH: ${c.dim(process.env.CLAUDE_CLI_PATH || 'claude (default)')}`);
116
116
  console.log(` CONTEXT_WINDOW: ${c.dim(process.env.CONTEXT_WINDOW || '160000 (default)')}`);
@@ -134,7 +134,7 @@ function showStatus() {
134
134
  console.log(` ${c.dim('>')} Use ${c.bright('cloudcli --port 8080')} to run on a custom port`);
135
135
  console.log(` ${c.dim('>')} Use ${c.bright('cloudcli --database-path /path/to/db')} for custom database`);
136
136
  console.log(` ${c.dim('>')} Run ${c.bright('cloudcli help')} for all options`);
137
- console.log(` ${c.dim('>')} Access the UI at http://localhost:${process.env.PORT || '3001'}\n`);
137
+ console.log(` ${c.dim('>')} Access the UI at http://localhost:${process.env.SERVER_PORT || process.env.PORT || '3001'}\n`);
138
138
  }
139
139
 
140
140
  // Show help
@@ -169,7 +169,8 @@ Examples:
169
169
  $ cloudcli status # Show configuration
170
170
 
171
171
  Environment Variables:
172
- PORT Set server port (default: 3001)
172
+ SERVER_PORT Set server port (default: 3001)
173
+ PORT Set server port (default: 3001) (LEGACY)
173
174
  DATABASE_PATH Set custom database location
174
175
  CLAUDE_CLI_PATH Set custom Claude CLI path
175
176
  CONTEXT_WINDOW Set context window size (default: 160000)
@@ -260,9 +261,9 @@ function parseArgs(args) {
260
261
  const arg = args[i];
261
262
 
262
263
  if (arg === '--port' || arg === '-p') {
263
- parsed.options.port = args[++i];
264
+ parsed.options.serverPort = args[++i];
264
265
  } else if (arg.startsWith('--port=')) {
265
- parsed.options.port = arg.split('=')[1];
266
+ parsed.options.serverPort = arg.split('=')[1];
266
267
  } else if (arg === '--database-path') {
267
268
  parsed.options.databasePath = args[++i];
268
269
  } else if (arg.startsWith('--database-path=')) {
@@ -285,8 +286,10 @@ async function main() {
285
286
  const { command, options } = parseArgs(args);
286
287
 
287
288
  // Apply CLI options to environment variables
288
- if (options.port) {
289
- process.env.PORT = options.port;
289
+ if (options.serverPort) {
290
+ process.env.SERVER_PORT = options.serverPort;
291
+ } else if (!process.env.SERVER_PORT && process.env.PORT) {
292
+ process.env.SERVER_PORT = process.env.PORT;
290
293
  }
291
294
  if (options.databasePath) {
292
295
  process.env.DATABASE_PATH = options.databasePath;