@sanqianx/project-knowledge 5.0.19 → 5.0.20

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 (200) hide show
  1. package/.agents/plugins/marketplace.json +3 -3
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/CHANGELOG.md +5 -0
  4. package/LICENSE +201 -201
  5. package/NOTICE +5 -5
  6. package/README.md +349 -349
  7. package/_modules/knowledge-engine/README.md +4 -0
  8. package/_modules/knowledge-engine/src/analysis-failures.js +39 -0
  9. package/_modules/knowledge-engine/src/analysis-worker.js +6 -2
  10. package/_modules/knowledge-engine/src/coordinator.js +7 -0
  11. package/_modules/knowledge-engine/src/headless-analyzer.js +2 -1
  12. package/_modules/knowledge-engine/src/server.js +1 -1
  13. package/_site/_test/agent-capture-mcp-ui-test.js +67 -67
  14. package/_site/_test/ai-profile-test.js +75 -75
  15. package/_site/_test/ai-vendor-presets-test.js +91 -91
  16. package/_site/_test/automatic-context-test.js +70 -70
  17. package/_site/_test/bin-cli-test.js +230 -230
  18. package/_site/_test/bridge-adapter-test.js +95 -95
  19. package/_site/_test/bridge-consumer-service-test.js +310 -310
  20. package/_site/_test/candidate-bin-content-test.js +35 -35
  21. package/_site/_test/candidate-product-assets-test.js +13 -13
  22. package/_site/_test/candidate-release-contract-test.js +14 -14
  23. package/_site/_test/capture-hook-dedup-test.js +158 -158
  24. package/_site/_test/claude-executable-discovery-test.js +28 -28
  25. package/_site/_test/claude-session-lifecycle-sweep-test.js +252 -252
  26. package/_site/_test/cli-instance-management-test.js +62 -62
  27. package/_site/_test/client-capture-restart-e2e-test.js +189 -189
  28. package/_site/_test/codex-conversation-projection-test.js +60 -60
  29. package/_site/_test/codex-marketplace-scoped-path-test.js +186 -186
  30. package/_site/_test/codex-notify-capture-e2e-test.js +101 -101
  31. package/_site/_test/codex-notify-fanout-test.js +134 -134
  32. package/_site/_test/codex-notify-upgrade-test.js +253 -253
  33. package/_site/_test/commit-boundary-freeze-test.js +147 -147
  34. package/_site/_test/commit-conversation-binding-test.js +4 -4
  35. package/_site/_test/configuration-generation-test.js +22 -22
  36. package/_site/_test/context-pack-test.js +200 -200
  37. package/_site/_test/conversation-api-test.js +10 -10
  38. package/_site/_test/conversation-origin-test.js +15 -15
  39. package/_site/_test/cross-repo-isolation-e2e-test.js +187 -187
  40. package/_site/_test/embedding-config-test.js +32 -32
  41. package/_site/_test/fixtures/fake-browser-exit.js +10 -10
  42. package/_site/_test/fixtures/fake-browser-hang.js +10 -10
  43. package/_site/_test/fixtures/integration-busy-worker.cjs +6 -6
  44. package/_site/_test/fixtures/make-git-repos.js +92 -92
  45. package/_site/_test/fixtures/retrieval-gold-queries.json +32 -32
  46. package/_site/_test/fresh-hook-reimport-test.js +53 -53
  47. package/_site/_test/fresh-product-data-test.js +56 -56
  48. package/_site/_test/git-path-compatibility-test.js +80 -80
  49. package/_site/_test/git-validation-test.js +81 -81
  50. package/_site/_test/hook-worktree-test.js +42 -42
  51. package/_site/_test/index-builder-test.js +41 -41
  52. package/_site/_test/index-error-display-contract-test.js +10 -10
  53. package/_site/_test/integration-adapters-test.js +67 -67
  54. package/_site/_test/integration-capture-setup-test.js +118 -118
  55. package/_site/_test/integration-runtime-binding-a2-test.js +566 -566
  56. package/_site/_test/integration-runtime-binding-a3-test.js +227 -227
  57. package/_site/_test/integration-runtime-binding-test.js +473 -473
  58. package/_site/_test/integration-status-scan-test.js +29 -29
  59. package/_site/_test/kb-framework-test.js +57 -57
  60. package/_site/_test/kbpath-follow-test.js +63 -63
  61. package/_site/_test/knowledge-db-test.js +144 -144
  62. package/_site/_test/knowledge-engine-client-test.js +109 -109
  63. package/_site/_test/knowledge-maintenance-test.js +40 -40
  64. package/_site/_test/knowledge-panel-ui-contract-test.js +112 -0
  65. package/_site/_test/knowledge-scopes-test.js +37 -37
  66. package/_site/_test/knowledge-storage-location-test.js +28 -28
  67. package/_site/_test/launcher-readiness-test.js +80 -80
  68. package/_site/_test/legacy-claude-session-alias-test.js +106 -106
  69. package/_site/_test/legacy-routes-removed-test.js +47 -47
  70. package/_site/_test/legacy-workbench-exclusion-test.js +92 -92
  71. package/_site/_test/live-gateway-test.js +48 -48
  72. package/_site/_test/local-marketplace-test.js +16 -16
  73. package/_site/_test/log-redaction-test.js +58 -58
  74. package/_site/_test/markdown-maintenance-test.js +11 -11
  75. package/_site/_test/model-context-windows-test.js +89 -89
  76. package/_site/_test/module-agent-start-proxy-test.js +65 -65
  77. package/_site/_test/module-artifact-integrity-test.js +27 -27
  78. package/_site/_test/module-boundary-test.js +30 -30
  79. package/_site/_test/module-bridge-eventbridge-test.js +202 -202
  80. package/_site/_test/module-dynamic-port-test.js +118 -118
  81. package/_site/_test/module-index-proxy-test.js +33 -33
  82. package/_site/_test/module-model-configuration-test.js +44 -44
  83. package/_site/_test/module-process-lifecycle-test.js +35 -35
  84. package/_site/_test/module-stream-proxy-test.js +34 -34
  85. package/_site/_test/module-terminal-restart-test.js +63 -63
  86. package/_site/_test/module-wiring-unbind-race-test.js +59 -59
  87. package/_site/_test/native-context-integrations-test.js +86 -86
  88. package/_site/_test/orphaned-claim-recovery-test.js +80 -80
  89. package/_site/_test/package-boundary-test.js +43 -42
  90. package/_site/_test/package-startup-test.js +56 -56
  91. package/_site/_test/packaged-ui-smoke-test.js +88 -88
  92. package/_site/_test/pr-consumer-contract-test.js +55 -55
  93. package/_site/_test/product-brand-contract-test.js +25 -25
  94. package/_site/_test/product-diagnostics-ui-test.js +93 -93
  95. package/_site/_test/product-import-ui-test.js +104 -104
  96. package/_site/_test/product-progress-status-test.js +12 -1
  97. package/_site/_test/project-remove-running-guard-test.js +79 -79
  98. package/_site/_test/project-store-test.js +90 -90
  99. package/_site/_test/registry-events-test.js +38 -38
  100. package/_site/_test/release-version-sync-test.js +115 -115
  101. package/_site/_test/requirement-binding-test.js +113 -113
  102. package/_site/_test/runtime-endpoint-test.js +64 -64
  103. package/_site/_test/scanner-test.js +83 -83
  104. package/_site/_test/server-security-test.js +96 -96
  105. package/_site/_test/sessions-stream-test.js +93 -93
  106. package/_site/_test/shared-scope-root-binding-test.js +67 -67
  107. package/_site/_test/simple-import-test.js +98 -98
  108. package/_site/_test/startup-analysis-disabled-test.js +6 -6
  109. package/_site/_test/team-knowledge-store-sparse-checkout-test.js +196 -196
  110. package/_site/_test/team-knowledge-store-test.js +132 -132
  111. package/_site/_test/test-server-cleanup-test.js +35 -35
  112. package/_site/_test/test-server-module-isolation-test.js +37 -37
  113. package/_site/_test/tracking-start-test.js +35 -35
  114. package/_site/_test/workbench-context-receipts-test.js +22 -22
  115. package/_site/_test/workspace-ui-contract-test.js +31 -31
  116. package/_site/_test/zcode-capture-integration-test.js +108 -108
  117. package/_site/lib/ai-vendor-presets.js +83 -83
  118. package/_site/lib/ai-workspace.js +3 -3
  119. package/_site/lib/atomic-file.js +204 -204
  120. package/_site/lib/bridge-adapter.js +260 -260
  121. package/_site/lib/capture-hook-config.js +108 -108
  122. package/_site/lib/claude-cli-runner.js +89 -89
  123. package/_site/lib/cli-instances.js +43 -43
  124. package/_site/lib/codex-context-status.js +17 -17
  125. package/_site/lib/codex-notify-config.js +193 -193
  126. package/_site/lib/commit-conversation-binder.js +12 -12
  127. package/_site/lib/commit-reconciler.js +38 -38
  128. package/_site/lib/context-pack-builder.js +299 -299
  129. package/_site/lib/conversation-exclusions.js +204 -204
  130. package/_site/lib/conversation-query-service.js +17 -17
  131. package/_site/lib/data-dir.js +13 -13
  132. package/_site/lib/embedding-service.js +85 -85
  133. package/_site/lib/engine-connection.js +93 -93
  134. package/_site/lib/engine-knowledge-client.js +72 -72
  135. package/_site/lib/folder-picker-output.js +3 -3
  136. package/_site/lib/git-path.js +14 -14
  137. package/_site/lib/github-team-store.js +3 -3
  138. package/_site/lib/index-builder.js +157 -157
  139. package/_site/lib/integration-status-scan.js +99 -99
  140. package/_site/lib/integration-status-worker.js +14 -14
  141. package/_site/lib/kb-framework.js +15 -15
  142. package/_site/lib/knowledge-db.js +363 -363
  143. package/_site/lib/knowledge-schema.js +121 -121
  144. package/_site/lib/live-gateway.js +78 -78
  145. package/_site/lib/llm-client.js +12 -12
  146. package/_site/lib/model-context-windows.js +82 -82
  147. package/_site/lib/module-bridge.js +874 -874
  148. package/_site/lib/module-port-resolver.js +57 -57
  149. package/_site/lib/post-commit-automation.js +54 -54
  150. package/_site/lib/product-data.js +35 -35
  151. package/_site/lib/project-lifecycle-service.js +14 -14
  152. package/_site/lib/project-registry-store.js +129 -129
  153. package/_site/lib/prompt-registry.js +148 -148
  154. package/_site/lib/registry-events.js +16 -16
  155. package/_site/lib/requirement-adapters.js +10 -10
  156. package/_site/lib/requirement-binder.js +112 -112
  157. package/_site/lib/runtime-control.js +89 -89
  158. package/_site/lib/runtime-endpoint.js +5 -5
  159. package/_site/lib/zcode-context-integration.js +60 -60
  160. package/_site/scripts/codex-notify-fanout.cjs +95 -95
  161. package/_site/scripts/install-module-candidates.js +69 -69
  162. package/_site/scripts/pack-module-candidates.js +89 -89
  163. package/_site/scripts/sync-product-content.js +28 -28
  164. package/_site/scripts/sync-release-version.js +164 -164
  165. package/_site/scripts/vendor-modules.js +49 -49
  166. package/_site/scripts/verify-module-runtime.js +50 -50
  167. package/_site/scripts/verify-product-runtime.js +36 -36
  168. package/_site/scripts/verify-retrieval-gold.js +83 -83
  169. package/_site/server.js +15 -15
  170. package/_site/start.bat +26 -26
  171. package/_site/stop.bat +11 -11
  172. package/bin/project-knowledge-integrations.js +254 -254
  173. package/bin/project-knowledge-kb.js +67 -67
  174. package/bin/project-knowledge-mcp.js +8 -8
  175. package/bin/project-knowledge.js +170 -170
  176. package/claude-prompts.json +21 -21
  177. package/docs/README.zh-CN.md +205 -205
  178. package/docs/assets/architecture.svg +181 -181
  179. package/docs/assets/logo.svg +49 -49
  180. package/docs/fresh-v5-start.md +42 -42
  181. package/docs/leader-glm-workbench-playbook.md +122 -122
  182. package/docs/project-registry-schema.md +11 -11
  183. package/docs/release-5.0.16-acceptance.md +176 -176
  184. package/docs/release-5.0.17-acceptance.md +117 -117
  185. package/docs/release-5.0.19-acceptance.md +39 -25
  186. package/docs/team-knowledge-mode-a-plan.md +73 -73
  187. package/module-runtime-manifest.json +53 -33
  188. package/package.json +3 -2
  189. package/plugins/project-knowledge/.claude-plugin/plugin.json +1 -1
  190. package/plugins/project-knowledge/.codex-plugin/plugin.json +1 -1
  191. package/plugins/project-knowledge/.mcp.json +1 -1
  192. package/plugins/project-knowledge/opencode/project-knowledge.md +3 -3
  193. package/plugins/project-knowledge/skills/project-knowledge/SKILL.md +28 -28
  194. package/templates/change.md +30 -30
  195. package/templates/project-readme.md +18 -18
  196. package/ui/index.html +1 -1
  197. package/ui/knowledge-panel.html +118 -0
  198. package/ui/product.css +8 -0
  199. package/ui/product.js +146 -4
  200. package/vendor-manifest.json +55 -54
@@ -1,108 +1,108 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const os = require('os');
5
- const path = require('path');
6
- const crypto = require('crypto');
7
- const { writeFileAtomic } = require('./atomic-file');
8
-
9
- const EVENTS = ['UserPromptSubmit', 'Stop'];
10
- function normalized(file) {
11
- const value = path.resolve(file).replace(/\\/g, '/');
12
- return process.platform === 'win32' ? value.toLowerCase() : value;
13
- }
14
- function nodeCommand(command) { return /(?:^|[\\/])node(?:\.exe)?$/i.test(command || ''); }
15
-
16
- // Only exact, known Bridge invocations are ours. A shell wrapper, extra
17
- // argument or a lookalike filename never authorizes removing a user hook.
18
- function bridgeBinding(args, options) {
19
- if (!Array.isArray(args) || typeof args[0] !== 'string' || !nodeCommand(args[0]) || typeof args[1] !== 'string') return '';
20
- if (args.length !== 2 && !(args.length === 4 && args[2] === '--source' && ['claude-code', 'claude', 'zcode', 'codex', 'opencode'].includes(args[3]))) return '';
21
- const actual = normalized(args[1]);
22
- const current = normalized(path.join(options.bridgeHomeDir, 'bin', 'bridge-hook.cjs'));
23
- const legacy = normalized(path.join(options.homeDir || os.homedir(), '.ai-coding-event-bridge', 'bin', 'bridge-hook.cjs'));
24
- return actual === current ? 'current' : actual === legacy ? 'legacy' : '';
25
- }
26
-
27
- function hookArgs(hook) {
28
- if (!hook || typeof hook !== 'object') return null;
29
- if (hook.type === 'process' && Array.isArray(hook.args)) return [hook.command, ...hook.args];
30
- if (hook.type !== 'command' || typeof hook.command !== 'string') return null;
31
- const match = hook.command.trim().match(/^(?:"([^"]+)"|'([^']+)'|(\S+))\s+(?:"([^"]+)"|'([^']+)'|(\S+))(?:\s+--source\s+(claude-code|claude|zcode|codex|opencode))?$/);
32
- return match ? [match[1] || match[2] || match[3], match[4] || match[5] || match[6], ...(match[7] ? ['--source', match[7]] : [])] : null;
33
- }
34
-
35
- function configuration(file) {
36
- const source = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
37
- let config;
38
- try { config = source ? JSON.parse(source) : {}; } catch { throw new Error('Agent 配置不是有效 JSON,未修改。'); }
39
- if (!config || typeof config !== 'object' || Array.isArray(config)) throw new Error('Agent 配置必须是 JSON 对象,未修改。');
40
- return { source, config };
41
- }
42
-
43
- function planCaptureHooks(file, client, options, removeCurrent = false) {
44
- if (!['claude', 'zcode'].includes(client)) throw new Error('不支持的采集配置类型。');
45
- const { source, config } = configuration(file);
46
- const hooks = config.hooks;
47
- if (hooks != null && (typeof hooks !== 'object' || Array.isArray(hooks))) throw new Error('Agent hooks 配置格式异常,未修改。');
48
- const events = client === 'zcode' ? hooks?.events : hooks;
49
- if (events != null && (typeof events !== 'object' || Array.isArray(events))) throw new Error('Agent 事件配置格式异常,未修改。');
50
- const removed = [], counts = {};
51
- for (const event of EVENTS) {
52
- const groups = events?.[event];
53
- if (groups == null) { counts[event] = { current: 0, legacy: 0 }; continue; }
54
- if (!Array.isArray(groups)) throw new Error('Agent 事件列表格式异常,未修改。');
55
- const count = { current: 0, legacy: 0 };
56
- for (const group of groups) {
57
- if (group?.hooks != null && !Array.isArray(group.hooks)) throw new Error('Agent Hook 列表格式异常,未修改。');
58
- for (const hook of group?.hooks || []) { const binding = bridgeBinding(hookArgs(hook), options); if (binding) count[binding]++; }
59
- }
60
- counts[event] = count;
61
- // Materialize the replacement first; never remove the only working old
62
- // capture when installing the new runtime has failed.
63
- if (!count.current) continue;
64
- let keptCurrent = false;
65
- const removedGroup = Symbol('empty-owned-group');
66
- events[event] = groups.map((group, groupIndex) => {
67
- if (!Array.isArray(group?.hooks)) return group;
68
- const kept = group.hooks.filter((hook, hookIndex) => {
69
- const binding = bridgeBinding(hookArgs(hook), options);
70
- if (!binding) return true;
71
- if (removeCurrent && binding === 'legacy') return true;
72
- if (!removeCurrent && binding === 'current' && !keptCurrent) { keptCurrent = true; return true; }
73
- // Back up only owned invocation fields, never unrelated config/env.
74
- const saved = Object.fromEntries(['type', 'command', 'args', 'timeout', 'timeoutMs'].filter(key => hook[key] !== undefined).map(key => [key, hook[key]]));
75
- removed.push({ event, groupIndex, hookIndex, hook: saved, binding });
76
- return false;
77
- });
78
- return kept.length === group.hooks.length ? group : kept.length ? { ...group, hooks: kept } : removedGroup;
79
- }).filter(group => group !== removedGroup);
80
- }
81
- return { source, config, counts, removed, repairNeeded: Object.values(counts).some(count => count.legacy > 0 || count.current > 1) };
82
- }
83
-
84
- function writePlan(file, client, plan) {
85
- if (!plan.removed.length) return { changed: false, removedLegacy: 0, removedDuplicates: 0 };
86
- const assertUnchanged = () => {
87
- if (fs.readFileSync(file, 'utf8') !== plan.source) throw Object.assign(new Error('Agent 配置已被其他程序修改,请重试接入。'), { code: 'CONFIG_CHANGED' });
88
- };
89
- assertUnchanged();
90
- const backupFile = `${file}.project-knowledge-capture-${Date.now()}-${crypto.randomBytes(4).toString('hex')}.bak`;
91
- writeFileAtomic(backupFile, JSON.stringify({ schema: 'project-knowledge/capture-backup/v1', client, removed: plan.removed }, null, 2) + '\n');
92
- const eol = plan.source.includes('\r\n') ? '\r\n' : '\n';
93
- writeFileAtomic(file, (JSON.stringify(plan.config, null, 2) + '\n').replace(/\n/g, eol), { beforeRename: assertUnchanged });
94
- return { changed: true, backupFile, removedLegacy: plan.removed.filter(row => row.binding === 'legacy').length, removedDuplicates: plan.removed.filter(row => row.binding === 'current').length };
95
- }
96
-
97
- function repairCaptureHooks(file, client, options) {
98
- const plan = planCaptureHooks(file, client, options);
99
- if (EVENTS.some(event => !plan.counts[event].current)) throw new Error('当前 Bridge 入口未完整建立,保留旧采集接线。');
100
- if (!fs.existsSync(path.join(options.bridgeHomeDir, 'bin', 'bridge-hook.cjs'))) throw new Error('当前 Bridge 运行入口缺失,保留旧采集接线。');
101
- return writePlan(file, client, plan);
102
- }
103
-
104
- function removeCaptureHooks(file, client, options) {
105
- return writePlan(file, client, planCaptureHooks(file, client, options, true));
106
- }
107
-
108
- module.exports = { bridgeBinding, planCaptureHooks, repairCaptureHooks, removeCaptureHooks };
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const crypto = require('crypto');
7
+ const { writeFileAtomic } = require('./atomic-file');
8
+
9
+ const EVENTS = ['UserPromptSubmit', 'Stop'];
10
+ function normalized(file) {
11
+ const value = path.resolve(file).replace(/\\/g, '/');
12
+ return process.platform === 'win32' ? value.toLowerCase() : value;
13
+ }
14
+ function nodeCommand(command) { return /(?:^|[\\/])node(?:\.exe)?$/i.test(command || ''); }
15
+
16
+ // Only exact, known Bridge invocations are ours. A shell wrapper, extra
17
+ // argument or a lookalike filename never authorizes removing a user hook.
18
+ function bridgeBinding(args, options) {
19
+ if (!Array.isArray(args) || typeof args[0] !== 'string' || !nodeCommand(args[0]) || typeof args[1] !== 'string') return '';
20
+ if (args.length !== 2 && !(args.length === 4 && args[2] === '--source' && ['claude-code', 'claude', 'zcode', 'codex', 'opencode'].includes(args[3]))) return '';
21
+ const actual = normalized(args[1]);
22
+ const current = normalized(path.join(options.bridgeHomeDir, 'bin', 'bridge-hook.cjs'));
23
+ const legacy = normalized(path.join(options.homeDir || os.homedir(), '.ai-coding-event-bridge', 'bin', 'bridge-hook.cjs'));
24
+ return actual === current ? 'current' : actual === legacy ? 'legacy' : '';
25
+ }
26
+
27
+ function hookArgs(hook) {
28
+ if (!hook || typeof hook !== 'object') return null;
29
+ if (hook.type === 'process' && Array.isArray(hook.args)) return [hook.command, ...hook.args];
30
+ if (hook.type !== 'command' || typeof hook.command !== 'string') return null;
31
+ const match = hook.command.trim().match(/^(?:"([^"]+)"|'([^']+)'|(\S+))\s+(?:"([^"]+)"|'([^']+)'|(\S+))(?:\s+--source\s+(claude-code|claude|zcode|codex|opencode))?$/);
32
+ return match ? [match[1] || match[2] || match[3], match[4] || match[5] || match[6], ...(match[7] ? ['--source', match[7]] : [])] : null;
33
+ }
34
+
35
+ function configuration(file) {
36
+ const source = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
37
+ let config;
38
+ try { config = source ? JSON.parse(source) : {}; } catch { throw new Error('Agent 配置不是有效 JSON,未修改。'); }
39
+ if (!config || typeof config !== 'object' || Array.isArray(config)) throw new Error('Agent 配置必须是 JSON 对象,未修改。');
40
+ return { source, config };
41
+ }
42
+
43
+ function planCaptureHooks(file, client, options, removeCurrent = false) {
44
+ if (!['claude', 'zcode'].includes(client)) throw new Error('不支持的采集配置类型。');
45
+ const { source, config } = configuration(file);
46
+ const hooks = config.hooks;
47
+ if (hooks != null && (typeof hooks !== 'object' || Array.isArray(hooks))) throw new Error('Agent hooks 配置格式异常,未修改。');
48
+ const events = client === 'zcode' ? hooks?.events : hooks;
49
+ if (events != null && (typeof events !== 'object' || Array.isArray(events))) throw new Error('Agent 事件配置格式异常,未修改。');
50
+ const removed = [], counts = {};
51
+ for (const event of EVENTS) {
52
+ const groups = events?.[event];
53
+ if (groups == null) { counts[event] = { current: 0, legacy: 0 }; continue; }
54
+ if (!Array.isArray(groups)) throw new Error('Agent 事件列表格式异常,未修改。');
55
+ const count = { current: 0, legacy: 0 };
56
+ for (const group of groups) {
57
+ if (group?.hooks != null && !Array.isArray(group.hooks)) throw new Error('Agent Hook 列表格式异常,未修改。');
58
+ for (const hook of group?.hooks || []) { const binding = bridgeBinding(hookArgs(hook), options); if (binding) count[binding]++; }
59
+ }
60
+ counts[event] = count;
61
+ // Materialize the replacement first; never remove the only working old
62
+ // capture when installing the new runtime has failed.
63
+ if (!count.current) continue;
64
+ let keptCurrent = false;
65
+ const removedGroup = Symbol('empty-owned-group');
66
+ events[event] = groups.map((group, groupIndex) => {
67
+ if (!Array.isArray(group?.hooks)) return group;
68
+ const kept = group.hooks.filter((hook, hookIndex) => {
69
+ const binding = bridgeBinding(hookArgs(hook), options);
70
+ if (!binding) return true;
71
+ if (removeCurrent && binding === 'legacy') return true;
72
+ if (!removeCurrent && binding === 'current' && !keptCurrent) { keptCurrent = true; return true; }
73
+ // Back up only owned invocation fields, never unrelated config/env.
74
+ const saved = Object.fromEntries(['type', 'command', 'args', 'timeout', 'timeoutMs'].filter(key => hook[key] !== undefined).map(key => [key, hook[key]]));
75
+ removed.push({ event, groupIndex, hookIndex, hook: saved, binding });
76
+ return false;
77
+ });
78
+ return kept.length === group.hooks.length ? group : kept.length ? { ...group, hooks: kept } : removedGroup;
79
+ }).filter(group => group !== removedGroup);
80
+ }
81
+ return { source, config, counts, removed, repairNeeded: Object.values(counts).some(count => count.legacy > 0 || count.current > 1) };
82
+ }
83
+
84
+ function writePlan(file, client, plan) {
85
+ if (!plan.removed.length) return { changed: false, removedLegacy: 0, removedDuplicates: 0 };
86
+ const assertUnchanged = () => {
87
+ if (fs.readFileSync(file, 'utf8') !== plan.source) throw Object.assign(new Error('Agent 配置已被其他程序修改,请重试接入。'), { code: 'CONFIG_CHANGED' });
88
+ };
89
+ assertUnchanged();
90
+ const backupFile = `${file}.project-knowledge-capture-${Date.now()}-${crypto.randomBytes(4).toString('hex')}.bak`;
91
+ writeFileAtomic(backupFile, JSON.stringify({ schema: 'project-knowledge/capture-backup/v1', client, removed: plan.removed }, null, 2) + '\n');
92
+ const eol = plan.source.includes('\r\n') ? '\r\n' : '\n';
93
+ writeFileAtomic(file, (JSON.stringify(plan.config, null, 2) + '\n').replace(/\n/g, eol), { beforeRename: assertUnchanged });
94
+ return { changed: true, backupFile, removedLegacy: plan.removed.filter(row => row.binding === 'legacy').length, removedDuplicates: plan.removed.filter(row => row.binding === 'current').length };
95
+ }
96
+
97
+ function repairCaptureHooks(file, client, options) {
98
+ const plan = planCaptureHooks(file, client, options);
99
+ if (EVENTS.some(event => !plan.counts[event].current)) throw new Error('当前 Bridge 入口未完整建立,保留旧采集接线。');
100
+ if (!fs.existsSync(path.join(options.bridgeHomeDir, 'bin', 'bridge-hook.cjs'))) throw new Error('当前 Bridge 运行入口缺失,保留旧采集接线。');
101
+ return writePlan(file, client, plan);
102
+ }
103
+
104
+ function removeCaptureHooks(file, client, options) {
105
+ return writePlan(file, client, planCaptureHooks(file, client, options, true));
106
+ }
107
+
108
+ module.exports = { bridgeBinding, planCaptureHooks, repairCaptureHooks, removeCaptureHooks };
@@ -188,12 +188,12 @@ function toPersistedSession(session) {
188
188
  aiProfileId: session.aiProfileId || null,
189
189
  claudeSessionId: session.claudeSessionId,
190
190
  source: session.source || 'manual',
191
- automation: !!session.automation,
192
- automationRunId: session.automationRunId || null,
193
- metadata: session.metadata || {},
194
- legacyProjectSlug: session.legacyProjectSlug || null,
195
- legacySource: session.legacySource === true,
196
- permissionMode: session.permissionMode || 'default',
191
+ automation: !!session.automation,
192
+ automationRunId: session.automationRunId || null,
193
+ metadata: session.metadata || {},
194
+ legacyProjectSlug: session.legacyProjectSlug || null,
195
+ legacySource: session.legacySource === true,
196
+ permissionMode: session.permissionMode || 'default',
197
197
  startedAt: session.startedAt,
198
198
  endedAt: session.endedAt,
199
199
  exitCode: session.exitCode,
@@ -229,63 +229,63 @@ function readPersistedRecord(file) {
229
229
  return null;
230
230
  }
231
231
 
232
- function scanPersistedRecords(projectSlug = null) {
233
- let projectDirs = [];
234
- try {
235
- const registry = projectRegistryStore.read({ allowMissing: true });
236
- projectDirs = registry.projectOrder
237
- .filter(projectId => !projectSlug || projectId === projectSlug)
238
- .map(projectId => {
239
- const config = projectStore.readConfig(projectId);
240
- const legacySlug = config.legacyExtensions && typeof config.legacyExtensions.slug === 'string'
241
- ? config.legacyExtensions.slug.trim()
242
- : '';
243
- return {
244
- name: projectId,
245
- kbPath: config.knowledgePath,
246
- legacySlugs: legacySlug && legacySlug !== projectId ? [legacySlug] : [],
247
- };
248
- });
249
- } catch { return []; }
250
- const records = new Map();
251
- for (const entry of projectDirs) {
252
- const dirs = [
253
- { path: storageLayout.getRuntimePath('claude-sessions', entry.name), legacySlug: null },
254
- { path: path.join(entry.kbPath, '_ai', WORKBENCH_DIR), legacySlug: null },
255
- ];
256
- try { dirs.push({ path: path.join(aiWorkspace.projectAIPath(entry.name), WORKBENCH_DIR), legacySlug: null }); } catch {
257
- // The legacy workspace location is optional during v2 session discovery.
258
- }
259
- for (const legacySlug of entry.legacySlugs) {
260
- try { dirs.push({ path: path.join(aiWorkspace.projectAIPath(legacySlug), WORKBENCH_DIR), legacySlug }); } catch {
261
- // Invalid migration metadata is ignored without hiding canonical v2 sessions.
262
- }
263
- }
264
- const visited = new Set();
265
- for (const source of dirs) {
266
- const dir = path.resolve(source.path);
267
- if (visited.has(dir)) continue;
268
- visited.add(dir);
269
- let files = [];
270
- try { files = fs.readdirSync(dir); } catch { continue; }
271
- for (const file of files) {
272
- if (!file.endsWith('.json')) continue;
273
- const record = readPersistedRecord(path.join(dir, file));
274
- if (!record || records.has(record.sessionId)) continue;
275
- const acceptedSlugs = new Set([entry.name, ...entry.legacySlugs]);
276
- if (record.projectSlug && !acceptedSlugs.has(record.projectSlug)) continue;
277
- const originalProjectSlug = record.projectSlug || '';
278
- records.set(record.sessionId, {
279
- ...record,
280
- projectSlug: entry.name,
281
- legacyProjectSlug: source.legacySlug || (originalProjectSlug !== entry.name ? originalProjectSlug : '') || record.legacyProjectSlug || null,
282
- legacySource: source.legacySlug != null || originalProjectSlug !== entry.name || record.legacySource === true,
283
- });
284
- }
285
- }
286
- }
287
- return [...records.values()].sort((a, b) => String(b.updatedAt || b.startedAt || '').localeCompare(String(a.updatedAt || a.startedAt || '')));
288
- }
232
+ function scanPersistedRecords(projectSlug = null) {
233
+ let projectDirs = [];
234
+ try {
235
+ const registry = projectRegistryStore.read({ allowMissing: true });
236
+ projectDirs = registry.projectOrder
237
+ .filter(projectId => !projectSlug || projectId === projectSlug)
238
+ .map(projectId => {
239
+ const config = projectStore.readConfig(projectId);
240
+ const legacySlug = config.legacyExtensions && typeof config.legacyExtensions.slug === 'string'
241
+ ? config.legacyExtensions.slug.trim()
242
+ : '';
243
+ return {
244
+ name: projectId,
245
+ kbPath: config.knowledgePath,
246
+ legacySlugs: legacySlug && legacySlug !== projectId ? [legacySlug] : [],
247
+ };
248
+ });
249
+ } catch { return []; }
250
+ const records = new Map();
251
+ for (const entry of projectDirs) {
252
+ const dirs = [
253
+ { path: storageLayout.getRuntimePath('claude-sessions', entry.name), legacySlug: null },
254
+ { path: path.join(entry.kbPath, '_ai', WORKBENCH_DIR), legacySlug: null },
255
+ ];
256
+ try { dirs.push({ path: path.join(aiWorkspace.projectAIPath(entry.name), WORKBENCH_DIR), legacySlug: null }); } catch {
257
+ // The legacy workspace location is optional during v2 session discovery.
258
+ }
259
+ for (const legacySlug of entry.legacySlugs) {
260
+ try { dirs.push({ path: path.join(aiWorkspace.projectAIPath(legacySlug), WORKBENCH_DIR), legacySlug }); } catch {
261
+ // Invalid migration metadata is ignored without hiding canonical v2 sessions.
262
+ }
263
+ }
264
+ const visited = new Set();
265
+ for (const source of dirs) {
266
+ const dir = path.resolve(source.path);
267
+ if (visited.has(dir)) continue;
268
+ visited.add(dir);
269
+ let files = [];
270
+ try { files = fs.readdirSync(dir); } catch { continue; }
271
+ for (const file of files) {
272
+ if (!file.endsWith('.json')) continue;
273
+ const record = readPersistedRecord(path.join(dir, file));
274
+ if (!record || records.has(record.sessionId)) continue;
275
+ const acceptedSlugs = new Set([entry.name, ...entry.legacySlugs]);
276
+ if (record.projectSlug && !acceptedSlugs.has(record.projectSlug)) continue;
277
+ const originalProjectSlug = record.projectSlug || '';
278
+ records.set(record.sessionId, {
279
+ ...record,
280
+ projectSlug: entry.name,
281
+ legacyProjectSlug: source.legacySlug || (originalProjectSlug !== entry.name ? originalProjectSlug : '') || record.legacyProjectSlug || null,
282
+ legacySource: source.legacySlug != null || originalProjectSlug !== entry.name || record.legacySource === true,
283
+ });
284
+ }
285
+ }
286
+ }
287
+ return [...records.values()].sort((a, b) => String(b.updatedAt || b.startedAt || '').localeCompare(String(a.updatedAt || a.startedAt || '')));
288
+ }
289
289
 
290
290
  function findPersistedRecord(sessionId) {
291
291
  return scanPersistedRecords().find(record => record.sessionId === sessionId) || null;
@@ -343,13 +343,13 @@ function restoreSessionFromDisk(sessionId) {
343
343
  pendingToolApproval: null,
344
344
  restored: true,
345
345
  historyCleared: !inheritConversation,
346
- source: record.source || 'manual',
347
- metadata: record.metadata || {},
348
- automation: !!record.automation,
349
- automationRunId: record.automationRunId || null,
350
- legacyProjectSlug: record.legacyProjectSlug || null,
351
- legacySource: record.legacySource === true,
352
- permissionMode: record.permissionMode || 'default',
346
+ source: record.source || 'manual',
347
+ metadata: record.metadata || {},
348
+ automation: !!record.automation,
349
+ automationRunId: record.automationRunId || null,
350
+ legacyProjectSlug: record.legacyProjectSlug || null,
351
+ legacySource: record.legacySource === true,
352
+ permissionMode: record.permissionMode || 'default',
353
353
  safetyPolicy: null,
354
354
  };
355
355
  sessions.set(sessionId, session);
@@ -491,12 +491,12 @@ function sessionSummary(s) {
491
491
  model: s.model,
492
492
  aiProfileId: s.aiProfileId || null,
493
493
  claudeSessionId: s.claudeSessionId,
494
- source: s.source || 'manual',
495
- automation: !!s.automation,
496
- automationRunId: s.automationRunId || null,
497
- legacyProjectSlug: s.legacyProjectSlug || null,
498
- legacySource: s.legacySource === true,
499
- permissionMode: s.permissionMode || 'default',
494
+ source: s.source || 'manual',
495
+ automation: !!s.automation,
496
+ automationRunId: s.automationRunId || null,
497
+ legacyProjectSlug: s.legacyProjectSlug || null,
498
+ legacySource: s.legacySource === true,
499
+ permissionMode: s.permissionMode || 'default',
500
500
  startedAt: s.startedAt,
501
501
  endedAt: s.endedAt,
502
502
  exitCode: s.exitCode,
@@ -1325,12 +1325,12 @@ function getState(sessionId) {
1325
1325
  model: s.model,
1326
1326
  aiProfileId: s.aiProfileId || null,
1327
1327
  claudeSessionId: s.claudeSessionId,
1328
- source: s.source || 'manual',
1329
- automation: !!s.automation,
1330
- automationRunId: s.automationRunId || null,
1331
- legacyProjectSlug: s.legacyProjectSlug || null,
1332
- legacySource: s.legacySource === true,
1333
- permissionMode: s.permissionMode || 'default',
1328
+ source: s.source || 'manual',
1329
+ automation: !!s.automation,
1330
+ automationRunId: s.automationRunId || null,
1331
+ legacyProjectSlug: s.legacyProjectSlug || null,
1332
+ legacySource: s.legacySource === true,
1333
+ permissionMode: s.permissionMode || 'default',
1334
1334
  startedAt: s.startedAt,
1335
1335
  endedAt: s.endedAt,
1336
1336
  exitCode: s.exitCode,
@@ -1506,13 +1506,13 @@ function demoteStaleActiveSessions(opts = {}) {
1506
1506
  pendingToolApproval: null,
1507
1507
  restored: true,
1508
1508
  historyCleared: true,
1509
- source: record.source || 'manual',
1510
- metadata: record.metadata || {},
1511
- automation: !!record.automation,
1512
- automationRunId: record.automationRunId || null,
1513
- legacyProjectSlug: record.legacyProjectSlug || null,
1514
- legacySource: record.legacySource === true,
1515
- permissionMode: record.permissionMode || 'default',
1509
+ source: record.source || 'manual',
1510
+ metadata: record.metadata || {},
1511
+ automation: !!record.automation,
1512
+ automationRunId: record.automationRunId || null,
1513
+ legacyProjectSlug: record.legacyProjectSlug || null,
1514
+ legacySource: record.legacySource === true,
1515
+ permissionMode: record.permissionMode || 'default',
1516
1516
  safetyPolicy: null,
1517
1517
  };
1518
1518
  sessions.set(record.sessionId, rehydrated);
@@ -1,43 +1,43 @@
1
- 'use strict';
2
- const path = require('node:path');
3
- const { execFileSync } = require('node:child_process');
4
- const control = require('./runtime-control');
5
-
6
- function url(info) { return `http://${info.host === '::1' ? '[::1]' : info.host}:${info.port}`; }
7
- async function current(dataDir) {
8
- try { const response = await control.request(dataDir, { action: 'status' }, 1500); return { ...response.instance, status: response.status }; }
9
- catch (error) { if (['ENOENT', 'ECONNREFUSED', 'EPIPE'].includes(error.code)) return null; throw error; }
10
- }
11
- async function atPort(port) {
12
- const response = await fetch(`http://127.0.0.1:${port}/api/runtime/identity`, { signal: AbortSignal.timeout(1200), headers: { Connection: 'close' } });
13
- if (!response.ok) throw new Error(`端口 ${port} 不是可控制的知识库实例(HTTP ${response.status})。`);
14
- const info = await response.json();
15
- if (info.schema !== control.SCHEMA || info.name !== 'project-knowledge' || info.port !== port || !path.isAbsolute(info.dataDir || '')) throw new Error(`端口 ${port} 的实例身份无效,已拒绝控制。`);
16
- const verified = await current(info.dataDir);
17
- if (!verified || verified.pid !== info.pid || verified.port !== port || verified.startedAt !== info.startedAt) throw new Error(`端口 ${port} 的实例已变化,已拒绝控制。`);
18
- return verified;
19
- }
20
- function listeningPorts() {
21
- let output;
22
- if (process.platform === 'win32') {
23
- output = execFileSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', "Get-NetTCPConnection -State Listen -ErrorAction Stop | Where-Object { $_.LocalAddress -in @('127.0.0.1','0.0.0.0','::1','::') } | Select-Object -ExpandProperty LocalPort -Unique"], { encoding: 'utf8', windowsHide: true, timeout: 10000 });
24
- return [...new Set(output.split(/\s+/).map(Number).filter(n => Number.isInteger(n) && n > 0 && n <= 65535))];
25
- }
26
- if (process.platform === 'darwin') output = execFileSync('lsof', ['-nP', '-iTCP', '-sTCP:LISTEN', '-Fn'], { encoding: 'utf8', timeout: 5000 });
27
- else output = execFileSync('ss', ['-tlnH'], { encoding: 'utf8', timeout: 5000 });
28
- return [...new Set([...output.matchAll(/:(\d+)(?:\s|$)/gm)].map(match => Number(match[1])))];
29
- }
30
- async function all() {
31
- const ports = listeningPorts(), instances = [], seen = new Set();
32
- // Bounded read-only probes; no PID is ever killed by inventory discovery.
33
- for (let i = 0; i < ports.length; i += 8) {
34
- const found = await Promise.all(ports.slice(i, i + 8).map(port => atPort(port).catch(() => null)));
35
- for (const info of found) if (info && !seen.has(info.pid + ':' + info.port)) { seen.add(info.pid + ':' + info.port); instances.push(info); }
36
- }
37
- return instances.sort((a, b) => a.port - b.port);
38
- }
39
- async function stop(info) {
40
- const result = await control.request(info.dataDir, { action: 'stop', pid: info.pid, port: info.port, startedAt: info.startedAt });
41
- if (result.status !== 'stopped') throw new Error('停止尚未确认。');
42
- }
43
- module.exports = { current, atPort, all, stop, url };
1
+ 'use strict';
2
+ const path = require('node:path');
3
+ const { execFileSync } = require('node:child_process');
4
+ const control = require('./runtime-control');
5
+
6
+ function url(info) { return `http://${info.host === '::1' ? '[::1]' : info.host}:${info.port}`; }
7
+ async function current(dataDir) {
8
+ try { const response = await control.request(dataDir, { action: 'status' }, 1500); return { ...response.instance, status: response.status }; }
9
+ catch (error) { if (['ENOENT', 'ECONNREFUSED', 'EPIPE'].includes(error.code)) return null; throw error; }
10
+ }
11
+ async function atPort(port) {
12
+ const response = await fetch(`http://127.0.0.1:${port}/api/runtime/identity`, { signal: AbortSignal.timeout(1200), headers: { Connection: 'close' } });
13
+ if (!response.ok) throw new Error(`端口 ${port} 不是可控制的知识库实例(HTTP ${response.status})。`);
14
+ const info = await response.json();
15
+ if (info.schema !== control.SCHEMA || info.name !== 'project-knowledge' || info.port !== port || !path.isAbsolute(info.dataDir || '')) throw new Error(`端口 ${port} 的实例身份无效,已拒绝控制。`);
16
+ const verified = await current(info.dataDir);
17
+ if (!verified || verified.pid !== info.pid || verified.port !== port || verified.startedAt !== info.startedAt) throw new Error(`端口 ${port} 的实例已变化,已拒绝控制。`);
18
+ return verified;
19
+ }
20
+ function listeningPorts() {
21
+ let output;
22
+ if (process.platform === 'win32') {
23
+ output = execFileSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', "Get-NetTCPConnection -State Listen -ErrorAction Stop | Where-Object { $_.LocalAddress -in @('127.0.0.1','0.0.0.0','::1','::') } | Select-Object -ExpandProperty LocalPort -Unique"], { encoding: 'utf8', windowsHide: true, timeout: 10000 });
24
+ return [...new Set(output.split(/\s+/).map(Number).filter(n => Number.isInteger(n) && n > 0 && n <= 65535))];
25
+ }
26
+ if (process.platform === 'darwin') output = execFileSync('lsof', ['-nP', '-iTCP', '-sTCP:LISTEN', '-Fn'], { encoding: 'utf8', timeout: 5000 });
27
+ else output = execFileSync('ss', ['-tlnH'], { encoding: 'utf8', timeout: 5000 });
28
+ return [...new Set([...output.matchAll(/:(\d+)(?:\s|$)/gm)].map(match => Number(match[1])))];
29
+ }
30
+ async function all() {
31
+ const ports = listeningPorts(), instances = [], seen = new Set();
32
+ // Bounded read-only probes; no PID is ever killed by inventory discovery.
33
+ for (let i = 0; i < ports.length; i += 8) {
34
+ const found = await Promise.all(ports.slice(i, i + 8).map(port => atPort(port).catch(() => null)));
35
+ for (const info of found) if (info && !seen.has(info.pid + ':' + info.port)) { seen.add(info.pid + ':' + info.port); instances.push(info); }
36
+ }
37
+ return instances.sort((a, b) => a.port - b.port);
38
+ }
39
+ async function stop(info) {
40
+ const result = await control.request(info.dataDir, { action: 'stop', pid: info.pid, port: info.port, startedAt: info.startedAt });
41
+ if (result.status !== 'stopped') throw new Error('停止尚未确认。');
42
+ }
43
+ module.exports = { current, atPort, all, stop, url };
@@ -1,17 +1,17 @@
1
- 'use strict';
2
-
3
- // Native read-only discovery, never trust-file edits or bypass flags. Reuse
4
- // the module's verified stdio transport rather than a second RPC lifecycle.
5
- async function readCodexHooks({ command, env, cwd }) {
6
- const { createAppServerBridge } = require('claude-ai-workbench/packages/server/lib/bridges/app-server-bridge');
7
- const { resolveCliInvocation } = require('claude-ai-workbench/packages/server/lib/drivers/cli-json-runner');
8
- const bridge = createAppServerBridge({ command, args: ['app-server'], env, cwd, resolveInvocation: resolveCliInvocation, maxRestarts: 0, idleTimeoutMs: 0, requestTimeoutMs: 10000 });
9
- try {
10
- await bridge.start();
11
- await bridge.request('initialize', { clientInfo: { name: 'project-knowledge-hook-status', version: '1' }, capabilities: { experimentalApi: true } });
12
- await bridge.notify('initialized');
13
- return await bridge.request('hooks/list', { cwds: [cwd] });
14
- } finally { await bridge.close(); }
15
- }
16
-
17
- module.exports = { readCodexHooks };
1
+ 'use strict';
2
+
3
+ // Native read-only discovery, never trust-file edits or bypass flags. Reuse
4
+ // the module's verified stdio transport rather than a second RPC lifecycle.
5
+ async function readCodexHooks({ command, env, cwd }) {
6
+ const { createAppServerBridge } = require('claude-ai-workbench/packages/server/lib/bridges/app-server-bridge');
7
+ const { resolveCliInvocation } = require('claude-ai-workbench/packages/server/lib/drivers/cli-json-runner');
8
+ const bridge = createAppServerBridge({ command, args: ['app-server'], env, cwd, resolveInvocation: resolveCliInvocation, maxRestarts: 0, idleTimeoutMs: 0, requestTimeoutMs: 10000 });
9
+ try {
10
+ await bridge.start();
11
+ await bridge.request('initialize', { clientInfo: { name: 'project-knowledge-hook-status', version: '1' }, capabilities: { experimentalApi: true } });
12
+ await bridge.notify('initialized');
13
+ return await bridge.request('hooks/list', { cwds: [cwd] });
14
+ } finally { await bridge.close(); }
15
+ }
16
+
17
+ module.exports = { readCodexHooks };