coding-tool-x 3.5.5 → 3.5.7

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 (40) hide show
  1. package/README.md +25 -4
  2. package/bin/ctx.js +6 -1
  3. package/dist/web/assets/{Analytics-gvYu5sCM.js → Analytics-C6DEmD3D.js} +1 -1
  4. package/dist/web/assets/{ConfigTemplates-CPlH8Ehd.js → ConfigTemplates-Cf_iTpC4.js} +1 -1
  5. package/dist/web/assets/{Home-B-qbu3uk.js → Home-BtBmYLJ1.js} +1 -1
  6. package/dist/web/assets/{PluginManager-B2tQ_YUq.js → PluginManager-DEk8vSw5.js} +1 -1
  7. package/dist/web/assets/{ProjectList-kDadoXXs.js → ProjectList-BMVhA_Kh.js} +1 -1
  8. package/dist/web/assets/{SessionList-eLgITwTV.js → SessionList-B5ioAXxg.js} +1 -1
  9. package/dist/web/assets/{SkillManager-B7zEB5Op.js → SkillManager-DcZOiiSf.js} +1 -1
  10. package/dist/web/assets/{WorkspaceManager-C-RzB3ud.js → WorkspaceManager-BHqI8aGV.js} +1 -1
  11. package/dist/web/assets/{icons-DlxD2wZJ.js → icons-CQuif85v.js} +1 -1
  12. package/dist/web/assets/index-CtByKdkA.js +2 -0
  13. package/dist/web/assets/{index-BHeh2z0i.css → index-VGAxnLqi.css} +1 -1
  14. package/dist/web/index.html +3 -3
  15. package/docs/Caddyfile.example +19 -0
  16. package/docs/reverse-proxy-https.md +57 -0
  17. package/package.json +2 -1
  18. package/src/commands/daemon.js +33 -5
  19. package/src/commands/stats.js +41 -4
  20. package/src/commands/ui.js +12 -3
  21. package/src/config/paths.js +6 -0
  22. package/src/index.js +125 -34
  23. package/src/server/api/codex-sessions.js +6 -3
  24. package/src/server/api/dashboard.js +25 -1
  25. package/src/server/api/gemini-sessions.js +6 -3
  26. package/src/server/api/hooks.js +17 -1
  27. package/src/server/api/opencode-sessions.js +6 -3
  28. package/src/server/api/plugins.js +24 -33
  29. package/src/server/api/sessions.js +6 -3
  30. package/src/server/index.js +31 -9
  31. package/src/server/services/codex-sessions.js +107 -9
  32. package/src/server/services/https-cert.js +171 -0
  33. package/src/server/services/network-access.js +61 -2
  34. package/src/server/services/notification-hooks.js +181 -16
  35. package/src/server/services/plugins-service.js +502 -44
  36. package/src/server/services/session-launch-command.js +81 -0
  37. package/src/server/services/sessions.js +103 -33
  38. package/src/server/services/web-ui-runtime.js +54 -0
  39. package/src/server/websocket-server.js +35 -4
  40. package/dist/web/assets/index-DG00t-zy.js +0 -2
@@ -16,6 +16,21 @@ function isLoopbackAddress(address) {
16
16
  return false;
17
17
  }
18
18
 
19
+ function hasTrustedProxySocket(req) {
20
+ if (!req) return false;
21
+ return isLoopbackAddress(req.socket && req.socket.remoteAddress);
22
+ }
23
+
24
+ function getForwardedHeaderValue(req, headerName) {
25
+ if (!hasTrustedProxySocket(req) || !req.headers) {
26
+ return '';
27
+ }
28
+
29
+ const rawValue = req.headers[headerName];
30
+ const value = Array.isArray(rawValue) ? rawValue[0] : rawValue;
31
+ return String(value || '').split(',')[0].trim();
32
+ }
33
+
19
34
  function isLoopbackRequest(req) {
20
35
  if (!req) return false;
21
36
  const socketAddress = req.socket && req.socket.remoteAddress;
@@ -30,6 +45,34 @@ function isLoopbackRequest(req) {
30
45
  return true;
31
46
  }
32
47
 
48
+ function getRequestProtocol(req) {
49
+ const forwardedProto = getForwardedHeaderValue(req, 'x-forwarded-proto').toLowerCase();
50
+ if (forwardedProto === 'https' || forwardedProto === 'https:') {
51
+ return 'https:';
52
+ }
53
+ if (forwardedProto === 'http' || forwardedProto === 'http:') {
54
+ return 'http:';
55
+ }
56
+ return req && req.socket && req.socket.encrypted ? 'https:' : 'http:';
57
+ }
58
+
59
+ function getRequestHost(req) {
60
+ const forwardedHost = getForwardedHeaderValue(req, 'x-forwarded-host');
61
+ if (forwardedHost) {
62
+ return forwardedHost;
63
+ }
64
+
65
+ if (!req || !req.headers) {
66
+ return '';
67
+ }
68
+
69
+ const host = req.headers.host;
70
+ if (Array.isArray(host)) {
71
+ return String(host[0] || '').trim();
72
+ }
73
+ return String(host || '').trim();
74
+ }
75
+
33
76
  function isSameOriginRequest(req) {
34
77
  if (!req) return false;
35
78
  const origin = req.headers && req.headers.origin;
@@ -37,19 +80,32 @@ function isSameOriginRequest(req) {
37
80
  return true;
38
81
  }
39
82
 
40
- const host = req.headers && req.headers.host;
83
+ const host = getRequestHost(req);
41
84
  if (!host) {
42
85
  return false;
43
86
  }
44
87
 
45
88
  try {
46
89
  const originUrl = new URL(origin);
47
- return originUrl.host === host;
90
+ return originUrl.host.toLowerCase() === host.toLowerCase();
48
91
  } catch (error) {
49
92
  return false;
50
93
  }
51
94
  }
52
95
 
96
+ function isRemoteMutationAllowed(envValue) {
97
+ if (envValue === undefined || envValue === null) {
98
+ return true;
99
+ }
100
+
101
+ const normalized = String(envValue).trim().toLowerCase();
102
+ if (!normalized) {
103
+ return true;
104
+ }
105
+
106
+ return !['0', 'false', 'no', 'off'].includes(normalized);
107
+ }
108
+
53
109
  function createRemoteMutationGuard(options = {}) {
54
110
  const enabled = options.enabled === true;
55
111
  const allowRemoteMutation = options.allowRemoteMutation === true;
@@ -111,7 +167,10 @@ module.exports = {
111
167
  normalizeAddress,
112
168
  isLoopbackAddress,
113
169
  isLoopbackRequest,
170
+ getRequestProtocol,
171
+ getRequestHost,
114
172
  isSameOriginRequest,
173
+ isRemoteMutationAllowed,
115
174
  createRemoteMutationGuard,
116
175
  createRemoteRouteGuard,
117
176
  createSameOriginGuard
@@ -5,9 +5,11 @@ const http = require('http');
5
5
  const https = require('https');
6
6
  const { execSync, execFileSync } = require('child_process');
7
7
  const { PATHS, NATIVE_PATHS } = require('../../config/paths');
8
+ const { loadConfig } = require('../../config/loader');
8
9
  const { loadUIConfig, saveUIConfig } = require('./ui-config');
9
10
  const codexSettingsManager = require('./codex-settings-manager');
10
11
  const geminiSettingsManager = require('./gemini-settings-manager');
12
+ const { getWebUiProtocol } = require('./web-ui-runtime');
11
13
 
12
14
  const MANAGED_HOOK_NAME = 'coding-tool-notify';
13
15
  const MANAGED_OPENCODE_PLUGIN_FILE = 'coding-tool-notify.js';
@@ -57,6 +59,13 @@ function getFeishuConfig() {
57
59
  };
58
60
  }
59
61
 
62
+ function getBrowserNotificationConfig() {
63
+ const uiConfig = loadUIConfig();
64
+ return {
65
+ enabled: uiConfig.browserNotification?.enabled === true
66
+ };
67
+ }
68
+
60
69
  function applyClaudeDisablePreference(uiConfig = {}, claudeEnabled) {
61
70
  const nextConfig = (uiConfig && typeof uiConfig === 'object') ? { ...uiConfig } : {};
62
71
  if (claudeEnabled) {
@@ -67,7 +76,7 @@ function applyClaudeDisablePreference(uiConfig = {}, claudeEnabled) {
67
76
  return nextConfig;
68
77
  }
69
78
 
70
- function saveNotificationUiConfig(feishu = {}, claudeEnabled) {
79
+ function saveNotificationUiConfig(feishu = {}, browser = {}, claudeEnabled) {
71
80
  let uiConfig = loadUIConfig();
72
81
  if (typeof claudeEnabled === 'boolean') {
73
82
  uiConfig = applyClaudeDisablePreference(uiConfig, claudeEnabled);
@@ -76,6 +85,9 @@ function saveNotificationUiConfig(feishu = {}, claudeEnabled) {
76
85
  enabled: feishu.enabled === true,
77
86
  webhookUrl: feishu.webhookUrl || ''
78
87
  };
88
+ uiConfig.browserNotification = {
89
+ enabled: browser.enabled === true
90
+ };
79
91
  saveUIConfig(uiConfig);
80
92
  }
81
93
 
@@ -115,6 +127,85 @@ function removeNotifyScript() {
115
127
  }
116
128
  }
117
129
 
130
+ function getNotificationSourceLabel(source = 'claude') {
131
+ switch (String(source || '').trim().toLowerCase()) {
132
+ case 'codex':
133
+ return 'Codex CLI';
134
+ case 'gemini':
135
+ return 'Gemini CLI';
136
+ case 'opencode':
137
+ return 'OpenCode';
138
+ default:
139
+ return 'Claude Code';
140
+ }
141
+ }
142
+
143
+ function resolveFallbackNotificationMessage(source = 'claude', eventType = '') {
144
+ const normalizedSource = String(source || '').trim().toLowerCase();
145
+ const normalizedEventType = String(eventType || '').trim();
146
+
147
+ if (normalizedSource === 'codex') {
148
+ if (normalizedEventType === 'agent-turn-complete') {
149
+ return 'Codex CLI 回合已完成 | 等待交互';
150
+ }
151
+ return 'Codex CLI 已返回结果 | 等待交互';
152
+ }
153
+
154
+ if (normalizedSource === 'gemini') {
155
+ return 'Gemini CLI 回合已完成 | 等待交互';
156
+ }
157
+
158
+ if (normalizedSource === 'opencode') {
159
+ if (normalizedEventType === 'session.error') {
160
+ return 'OpenCode 会话异常,请检查日志';
161
+ }
162
+ return 'OpenCode 响应已完成 | 等待交互';
163
+ }
164
+
165
+ return 'Claude Code 任务已完成 | 等待交互';
166
+ }
167
+
168
+ function buildBrowserNotificationEndpoint() {
169
+ try {
170
+ const config = loadConfig();
171
+ const port = Number(config?.ports?.webUI);
172
+ const resolvedPort = Number.isFinite(port) && port > 0 ? port : 19999;
173
+ const protocol = getWebUiProtocol();
174
+ return `${protocol}://127.0.0.1:${resolvedPort}/api/hooks/browser-event`;
175
+ } catch (error) {
176
+ return `${getWebUiProtocol()}://127.0.0.1:19999/api/hooks/browser-event`;
177
+ }
178
+ }
179
+
180
+ function buildBrowserNotificationPayload(input = {}) {
181
+ const rawSource = String(input?.source || '').trim().toLowerCase();
182
+ const source = ['claude', 'codex', 'gemini', 'opencode'].includes(rawSource) ? rawSource : 'claude';
183
+ const eventType = String(input?.eventType || input?.event_type || '').trim();
184
+ const rawMessage = String(input?.message || '').trim();
185
+ const timestamp = Number(input?.timestamp);
186
+
187
+ return {
188
+ title: String(input?.title || '').trim() || 'Coding Tool',
189
+ source,
190
+ sourceLabel: getNotificationSourceLabel(source),
191
+ mode: normalizeType(input?.mode),
192
+ eventType,
193
+ message: rawMessage || resolveFallbackNotificationMessage(source, eventType),
194
+ timestamp: Number.isFinite(timestamp) ? timestamp : Date.now()
195
+ };
196
+ }
197
+
198
+ function emitBrowserNotificationEvent(input = {}) {
199
+ const browser = getBrowserNotificationConfig();
200
+ if (!browser.enabled) {
201
+ return false;
202
+ }
203
+
204
+ const { broadcastBrowserNotification } = require('../websocket-server');
205
+ broadcastBrowserNotification(buildBrowserNotificationPayload(input));
206
+ return true;
207
+ }
208
+
118
209
  function parseManagedType(input) {
119
210
  const value = String(input || '');
120
211
  const matches = [
@@ -433,8 +524,10 @@ function runWindowsPowerShellCommand(command) {
433
524
  });
434
525
  }
435
526
 
436
- function generateNotifyScript(feishu = {}) {
527
+ function generateNotifyScript(feishu = {}, browser = {}) {
437
528
  const feishuEnabled = feishu.enabled === true && !!feishu.webhookUrl;
529
+ const browserEnabled = browser.enabled === true;
530
+ const browserEndpoint = browserEnabled ? buildBrowserNotificationEndpoint() : '';
438
531
 
439
532
  return `#!/usr/bin/env node
440
533
  // Coding Tool 通知脚本 - 自动生成,请勿手动修改
@@ -446,6 +539,8 @@ const { execSync, execFileSync } = require('child_process')
446
539
 
447
540
  const FEISHU_ENABLED = ${feishuEnabled ? 'true' : 'false'}
448
541
  const FEISHU_WEBHOOK_URL = ${JSON.stringify(feishuEnabled ? feishu.webhookUrl : '')}
542
+ const BROWSER_ENABLED = ${browserEnabled ? 'true' : 'false'}
543
+ const BROWSER_ENDPOINT = ${JSON.stringify(browserEndpoint)}
449
544
 
450
545
  function readArg(name) {
451
546
  const prefix = \`\${name}=\`
@@ -483,21 +578,25 @@ function readOptionalPayload() {
483
578
  return null
484
579
  }
485
580
 
486
- function resolveMessage(source, eventType, payload) {
487
- const effectiveEventType = eventType || payload?.type || payload?.hook_event?.event_type || ''
581
+ function resolveEventType(eventType, payload) {
582
+ return eventType || payload?.type || payload?.hook_event?.event_type || ''
583
+ }
584
+
585
+ function resolveMessage(source, effectiveEventType) {
586
+ const normalizedSource = String(source || '').trim().toLowerCase()
488
587
 
489
- if (source === 'codex') {
588
+ if (normalizedSource === 'codex') {
490
589
  if (effectiveEventType === 'agent-turn-complete') {
491
590
  return 'Codex CLI 回合已完成 | 等待交互'
492
591
  }
493
592
  return 'Codex CLI 已返回结果 | 等待交互'
494
593
  }
495
594
 
496
- if (source === 'gemini') {
595
+ if (normalizedSource === 'gemini') {
497
596
  return 'Gemini CLI 回合已完成 | 等待交互'
498
597
  }
499
598
 
500
- if (source === 'opencode') {
599
+ if (normalizedSource === 'opencode') {
501
600
  if (effectiveEventType === 'session.error') {
502
601
  return 'OpenCode 会话异常,请检查日志'
503
602
  }
@@ -628,15 +727,67 @@ function sendFeishu(message, source) {
628
727
  })
629
728
  }
630
729
 
730
+ function sendBrowserNotification(payload) {
731
+ if (!BROWSER_ENABLED || !BROWSER_ENDPOINT) {
732
+ return Promise.resolve()
733
+ }
734
+
735
+ return new Promise((resolve) => {
736
+ try {
737
+ const urlObj = new URL(BROWSER_ENDPOINT)
738
+ const data = JSON.stringify(payload)
739
+ const options = {
740
+ hostname: urlObj.hostname,
741
+ port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
742
+ path: urlObj.pathname + urlObj.search,
743
+ method: 'POST',
744
+ headers: {
745
+ 'Content-Type': 'application/json',
746
+ 'Content-Length': Buffer.byteLength(data)
747
+ },
748
+ timeout: 5000
749
+ }
750
+
751
+ if (urlObj.protocol === 'https:' && ['127.0.0.1', 'localhost', '::1'].includes(urlObj.hostname)) {
752
+ options.rejectUnauthorized = false
753
+ }
754
+
755
+ const requestModule = urlObj.protocol === 'https:' ? https : http
756
+ const request = requestModule.request(options, () => resolve())
757
+ request.on('error', () => resolve())
758
+ request.on('timeout', () => {
759
+ request.destroy()
760
+ resolve()
761
+ })
762
+ request.write(data)
763
+ request.end()
764
+ } catch (error) {
765
+ resolve()
766
+ }
767
+ })
768
+ }
769
+
631
770
  (async () => {
632
771
  const source = readArg('--source') || 'claude'
633
772
  const mode = readArg('--mode') || readArg('--cc-notify-type') || 'notification'
634
773
  const eventType = readArg('--event-type') || ''
635
774
  const payload = readOptionalPayload()
636
- const message = resolveMessage(source, eventType, payload)
637
-
638
- notify(mode === 'dialog' ? 'dialog' : 'notification', message)
639
- await sendFeishu(message, source)
775
+ const effectiveEventType = resolveEventType(eventType, payload)
776
+ const normalizedMode = mode === 'dialog' ? 'dialog' : 'notification'
777
+ const message = resolveMessage(source, effectiveEventType)
778
+ const timestamp = Date.now()
779
+
780
+ notify(normalizedMode, message)
781
+ await Promise.all([
782
+ sendFeishu(message, source),
783
+ sendBrowserNotification({
784
+ source,
785
+ mode: normalizedMode,
786
+ eventType: effectiveEventType,
787
+ message,
788
+ timestamp
789
+ })
790
+ ])
640
791
  })().catch(() => {
641
792
  process.exit(0)
642
793
  })
@@ -856,9 +1007,9 @@ function runWindowsPowerShellCommand(command) {
856
1007
  `;
857
1008
  }
858
1009
 
859
- function writeNotifyScript(feishu = {}) {
1010
+ function writeNotifyScript(feishu = {}, browser = getBrowserNotificationConfig()) {
860
1011
  ensureParentDir(PATHS.notifyHook);
861
- fs.writeFileSync(PATHS.notifyHook, generateNotifyScript(feishu), { mode: 0o755 });
1012
+ fs.writeFileSync(PATHS.notifyHook, generateNotifyScript(feishu, browser), { mode: 0o755 });
862
1013
  }
863
1014
 
864
1015
  function getClaudeHookStatus() {
@@ -1167,6 +1318,7 @@ function getNotificationSettings() {
1167
1318
  success: true,
1168
1319
  platform: os.platform(),
1169
1320
  feishu: getFeishuConfig(),
1321
+ browser: getBrowserNotificationConfig(),
1170
1322
  platforms: {
1171
1323
  claude: getClaudeHookStatus(),
1172
1324
  codex: getCodexHookStatus(),
@@ -1185,6 +1337,7 @@ function normalizePlatformInput(platform = {}) {
1185
1337
 
1186
1338
  function saveNotificationSettings(input = {}) {
1187
1339
  const existingFeishu = getFeishuConfig();
1340
+ const existingBrowser = getBrowserNotificationConfig();
1188
1341
  const requestedWebhookUrl = String(input?.feishu?.webhookUrl || '').trim();
1189
1342
  const feishu = {
1190
1343
  enabled: input?.feishu?.enabled === true,
@@ -1193,6 +1346,11 @@ function saveNotificationSettings(input = {}) {
1193
1346
  if (feishu.enabled && feishu.webhookUrl) {
1194
1347
  validateFeishuWebhookUrl(feishu.webhookUrl);
1195
1348
  }
1349
+ const browser = {
1350
+ enabled: input?.browser !== undefined
1351
+ ? input?.browser?.enabled === true
1352
+ : existingBrowser.enabled === true
1353
+ };
1196
1354
  const platforms = {
1197
1355
  claude: normalizePlatformInput(input?.platforms?.claude),
1198
1356
  codex: normalizePlatformInput(input?.platforms?.codex),
@@ -1200,11 +1358,11 @@ function saveNotificationSettings(input = {}) {
1200
1358
  opencode: normalizePlatformInput(input?.platforms?.opencode)
1201
1359
  };
1202
1360
 
1203
- saveNotificationUiConfig(feishu, platforms.claude.enabled);
1361
+ saveNotificationUiConfig(feishu, browser, platforms.claude.enabled);
1204
1362
 
1205
1363
  const hasManagedPlatform = Object.values(platforms).some((platform) => platform.enabled);
1206
1364
  if (hasManagedPlatform) {
1207
- writeNotifyScript(feishu);
1365
+ writeNotifyScript(feishu, browser);
1208
1366
  }
1209
1367
 
1210
1368
  saveClaudeHook(platforms.claude.enabled, platforms.claude.type);
@@ -1347,6 +1505,9 @@ function buildLegacyClaudeSaveInput(input = {}, currentSettings = getNotificatio
1347
1505
  : {
1348
1506
  enabled: currentSettings?.feishu?.enabled === true,
1349
1507
  webhookUrl: currentSettings?.feishu?.webhookUrl || ''
1508
+ },
1509
+ browser: {
1510
+ enabled: currentSettings?.browser?.enabled === true
1350
1511
  }
1351
1512
  };
1352
1513
  }
@@ -1356,6 +1517,7 @@ function getLegacyClaudeHookSettings() {
1356
1517
  success: true,
1357
1518
  stopHook: parseStopHookStatus(readClaudeSettings()),
1358
1519
  feishu: getFeishuConfig(),
1520
+ browser: getBrowserNotificationConfig(),
1359
1521
  platform: os.platform()
1360
1522
  };
1361
1523
  }
@@ -1553,7 +1715,7 @@ function syncManagedNotificationAssets() {
1553
1715
  const hasManagedPlatform = Object.values(settings?.platforms || {}).some((platform) => platform?.enabled === true);
1554
1716
 
1555
1717
  if (hasManagedPlatform) {
1556
- writeNotifyScript(settings.feishu || {});
1718
+ writeNotifyScript(settings.feishu || {}, settings.browser || {});
1557
1719
  } else {
1558
1720
  removeNotifyScript();
1559
1721
  }
@@ -1575,6 +1737,7 @@ module.exports = {
1575
1737
  getLegacyClaudeHookSettings,
1576
1738
  saveNotificationSettings,
1577
1739
  saveLegacyClaudeHookSettings,
1740
+ emitBrowserNotificationEvent,
1578
1741
  testNotification,
1579
1742
  initDefaultHooks,
1580
1743
  syncManagedNotificationAssets,
@@ -1600,6 +1763,8 @@ module.exports = {
1600
1763
  buildGeminiCommand,
1601
1764
  buildStopHookCommand,
1602
1765
  buildClaudeCommand,
1766
+ buildBrowserNotificationEndpoint,
1767
+ buildBrowserNotificationPayload,
1603
1768
  buildOpenCodePluginContent,
1604
1769
  getOpenCodeManagedPluginPath,
1605
1770
  generateNotifyScript,