@sanqianx/project-knowledge 4.6.1

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 (218) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/.claude-plugin/marketplace.json +17 -0
  3. package/CHANGELOG.md +780 -0
  4. package/LICENSE +201 -0
  5. package/NOTICE +5 -0
  6. package/README.md +266 -0
  7. package/_site/README.md +30 -0
  8. package/_site/_test/ai-profile-resolver-test.js +137 -0
  9. package/_site/_test/ai-profile-test.js +75 -0
  10. package/_site/_test/ai-vendor-presets-test.js +91 -0
  11. package/_site/_test/automation-queue-test.js +14 -0
  12. package/_site/_test/automation-ui-test.js +40 -0
  13. package/_site/_test/background-task-registry-test.js +43 -0
  14. package/_site/_test/baseline-schema-test.js +91 -0
  15. package/_site/_test/bin-cli-test.js +218 -0
  16. package/_site/_test/bridge-adapter-test.js +95 -0
  17. package/_site/_test/bridge-consumer-service-test.js +310 -0
  18. package/_site/_test/cdp-browser-failure-test.js +100 -0
  19. package/_site/_test/chat-claudecodeui-match-test.js +92 -0
  20. package/_site/_test/claude-executable-discovery-test.js +90 -0
  21. package/_site/_test/claude-session-lifecycle-sweep-test.js +252 -0
  22. package/_site/_test/claude-workbench-test.js +170 -0
  23. package/_site/_test/client-capture-restart-e2e-test.js +189 -0
  24. package/_site/_test/codex-conversation-projection-test.js +59 -0
  25. package/_site/_test/codex-notify-fanout-test.js +45 -0
  26. package/_site/_test/commit-boundary-freeze-test.js +147 -0
  27. package/_site/_test/commit-conversation-binding-test.js +99 -0
  28. package/_site/_test/commit-evidence-test.js +110 -0
  29. package/_site/_test/context-pack-test.js +198 -0
  30. package/_site/_test/conversation-api-test.js +119 -0
  31. package/_site/_test/conversation-store-test.js +136 -0
  32. package/_site/_test/cross-repo-isolation-e2e-test.js +187 -0
  33. package/_site/_test/data-dir-migration-test.js +77 -0
  34. package/_site/_test/desktop-browser-compat-test.js +28 -0
  35. package/_site/_test/desktop-hook-runtime-regression-test.js +204 -0
  36. package/_site/_test/embedding-config-test.js +32 -0
  37. package/_site/_test/explicit-commit-processor-test.js +37 -0
  38. package/_site/_test/fixtures/fake-browser-exit.js +10 -0
  39. package/_site/_test/fixtures/fake-browser-hang.js +10 -0
  40. package/_site/_test/fixtures/make-git-repos.js +89 -0
  41. package/_site/_test/folder-picker-output-test.js +19 -0
  42. package/_site/_test/full-integration-e2e-test.js +165 -0
  43. package/_site/_test/git-validation-test.js +81 -0
  44. package/_site/_test/hook-runtime-endpoint-test.js +88 -0
  45. package/_site/_test/hook-status-repair-api-test.js +152 -0
  46. package/_site/_test/hook-trigger-test.js +85 -0
  47. package/_site/_test/hook-worktree-test.js +42 -0
  48. package/_site/_test/import-preflight-api-test.js +233 -0
  49. package/_site/_test/import-ui-flow-test.js +171 -0
  50. package/_site/_test/index-builder-test.js +41 -0
  51. package/_site/_test/index-writer-concurrency-test.js +135 -0
  52. package/_site/_test/integration-adapters-test.js +243 -0
  53. package/_site/_test/integration-capture-setup-test.js +108 -0
  54. package/_site/_test/integration-surface-coverage-test.js +165 -0
  55. package/_site/_test/kb-framework-test.js +57 -0
  56. package/_site/_test/kbpath-follow-test.js +63 -0
  57. package/_site/_test/knowledge-db-test.js +144 -0
  58. package/_site/_test/knowledge-language-control-test.js +170 -0
  59. package/_site/_test/knowledge-maintenance-test.js +40 -0
  60. package/_site/_test/knowledge-migration-test.js +75 -0
  61. package/_site/_test/knowledge-promotion-recovery-test.js +260 -0
  62. package/_site/_test/knowledge-query-test.js +54 -0
  63. package/_site/_test/knowledge-retrieval-service-test.js +77 -0
  64. package/_site/_test/knowledge-scopes-test.js +37 -0
  65. package/_site/_test/knowledge-storage-location-test.js +28 -0
  66. package/_site/_test/knowledge-storage-startup-test.js +52 -0
  67. package/_site/_test/knowledge-store-logs-supervision-test.js +89 -0
  68. package/_site/_test/legacy-claude-session-alias-test.js +106 -0
  69. package/_site/_test/legacy-forward-compat-test.js +257 -0
  70. package/_site/_test/legacy-project-upgrade-e2e-test.js +363 -0
  71. package/_site/_test/legacy-routes-removed-test.js +47 -0
  72. package/_site/_test/legacy-workbench-exclusion-test.js +92 -0
  73. package/_site/_test/log-redaction-test.js +58 -0
  74. package/_site/_test/logging-api-test.js +89 -0
  75. package/_site/_test/logging-sse-no-gap-test.js +88 -0
  76. package/_site/_test/logging-ui-test.js +104 -0
  77. package/_site/_test/markdown-delta-overlay-test.js +85 -0
  78. package/_site/_test/markdown-maintenance-api-test.js +75 -0
  79. package/_site/_test/markdown-maintenance-test.js +11 -0
  80. package/_site/_test/mcp-server-test.js +149 -0
  81. package/_site/_test/model-context-windows-test.js +89 -0
  82. package/_site/_test/module-bridge-eventbridge-test.js +193 -0
  83. package/_site/_test/non-release-ci-test.js +34 -0
  84. package/_site/_test/offline-boundary-isolation-test.js +32 -0
  85. package/_site/_test/orphaned-claim-recovery-test.js +80 -0
  86. package/_site/_test/p0-data-migration-characterization-test.js +37 -0
  87. package/_site/_test/p0-e2e-gate-test.js +347 -0
  88. package/_site/_test/package-boundary-test.js +42 -0
  89. package/_site/_test/package-startup-test.js +56 -0
  90. package/_site/_test/packaged-ui-smoke-test.js +79 -0
  91. package/_site/_test/path-consistency-test.js +151 -0
  92. package/_site/_test/pending-sweep-test.js +9 -0
  93. package/_site/_test/post-commit-automation-test.js +87 -0
  94. package/_site/_test/pr-consumer-contract-test.js +55 -0
  95. package/_site/_test/project-control-panel-task14-test.js +63 -0
  96. package/_site/_test/project-delete-recovery-test.js +64 -0
  97. package/_site/_test/project-goal-editor-test.js +145 -0
  98. package/_site/_test/project-layout-v2-migration-test.js +127 -0
  99. package/_site/_test/project-lifecycle-transaction-test.js +106 -0
  100. package/_site/_test/project-remove-running-guard-test.js +79 -0
  101. package/_site/_test/project-store-test.js +90 -0
  102. package/_site/_test/prompt-settings-test.js +115 -0
  103. package/_site/_test/protected-architecture-gate-test.js +123 -0
  104. package/_site/_test/refactor-characterization-test.js +36 -0
  105. package/_site/_test/release-version-sync-test.js +98 -0
  106. package/_site/_test/requirement-binding-test.js +113 -0
  107. package/_site/_test/requirement-recorder-test.js +173 -0
  108. package/_site/_test/run-all-tests.js +156 -0
  109. package/_site/_test/runtime-endpoint-test.js +64 -0
  110. package/_site/_test/scanner-test.js +83 -0
  111. package/_site/_test/server-runtime-migration-safety-test.js +27 -0
  112. package/_site/_test/server-security-test.js +96 -0
  113. package/_site/_test/sessions-stream-test.js +92 -0
  114. package/_site/_test/shared-contracts-test.js +58 -0
  115. package/_site/_test/simple-import-test.js +97 -0
  116. package/_site/_test/startup-analysis-disabled-test.js +16 -0
  117. package/_site/_test/storage-foundation-test.js +78 -0
  118. package/_site/_test/structured-logger-test.js +101 -0
  119. package/_site/_test/task15-20-ui-flow-test.js +148 -0
  120. package/_site/_test/team-knowledge-store-sparse-checkout-test.js +196 -0
  121. package/_site/_test/team-knowledge-store-test.js +131 -0
  122. package/_site/_test/tracking-start-test.js +143 -0
  123. package/_site/_test/ui-i18n-toggle-test.js +114 -0
  124. package/_site/_test/ui-smoke-test.js +73 -0
  125. package/_site/_test/v4122-upgrade-data-contract-test.js +56 -0
  126. package/_site/_test/workbench-permission-test.js +118 -0
  127. package/_site/_test/workspace-ui-contract-test.js +43 -0
  128. package/_site/lib/ai-profile-resolver.js +78 -0
  129. package/_site/lib/ai-vendor-presets.js +83 -0
  130. package/_site/lib/ai-workspace.js +104 -0
  131. package/_site/lib/atomic-file.js +204 -0
  132. package/_site/lib/automation-config.js +140 -0
  133. package/_site/lib/bridge-adapter.js +256 -0
  134. package/_site/lib/bridge-consumer-service.js +402 -0
  135. package/_site/lib/claude-cli-runner.js +1599 -0
  136. package/_site/lib/commit-conversation-binder.js +246 -0
  137. package/_site/lib/commit-processing-ledger.js +27 -0
  138. package/_site/lib/commit-prompt.js +202 -0
  139. package/_site/lib/commit-reconciler.js +444 -0
  140. package/_site/lib/context-pack-builder.js +299 -0
  141. package/_site/lib/contracts.js +279 -0
  142. package/_site/lib/conversation-exclusions.js +204 -0
  143. package/_site/lib/conversation-query-service.js +161 -0
  144. package/_site/lib/conversation-store.js +385 -0
  145. package/_site/lib/data-dir.js +81 -0
  146. package/_site/lib/data-state-classifier.js +40 -0
  147. package/_site/lib/embedding-service.js +85 -0
  148. package/_site/lib/evidence-bundle.js +228 -0
  149. package/_site/lib/folder-picker-output.js +24 -0
  150. package/_site/lib/git-runner.js +62 -0
  151. package/_site/lib/github-team-store.js +1175 -0
  152. package/_site/lib/hook-manager.js +202 -0
  153. package/_site/lib/index-builder.js +157 -0
  154. package/_site/lib/index-service.js +173 -0
  155. package/_site/lib/integration-installer.js +907 -0
  156. package/_site/lib/kb-framework.js +191 -0
  157. package/_site/lib/kb-validator.js +124 -0
  158. package/_site/lib/knowledge-db.js +363 -0
  159. package/_site/lib/knowledge-promotion.js +435 -0
  160. package/_site/lib/knowledge-retrieval-service.js +401 -0
  161. package/_site/lib/knowledge-schema.js +121 -0
  162. package/_site/lib/knowledge-tool-runtime.js +359 -0
  163. package/_site/lib/legacy-data-manifest.js +26 -0
  164. package/_site/lib/llm-client.js +173 -0
  165. package/_site/lib/markdown-knowledge-indexer.js +230 -0
  166. package/_site/lib/migration-service.js +442 -0
  167. package/_site/lib/model-context-windows.js +82 -0
  168. package/_site/lib/module-bridge.js +496 -0
  169. package/_site/lib/post-commit-automation.js +141 -0
  170. package/_site/lib/project-lifecycle-service.js +511 -0
  171. package/_site/lib/project-registry-store.js +129 -0
  172. package/_site/lib/project-store.js +262 -0
  173. package/_site/lib/prompt-registry.js +148 -0
  174. package/_site/lib/requirement-adapters.js +10 -0
  175. package/_site/lib/requirement-binder.js +112 -0
  176. package/_site/lib/requirement-recorder.js +278 -0
  177. package/_site/lib/runtime-endpoint.js +160 -0
  178. package/_site/lib/scanner.js +298 -0
  179. package/_site/lib/server-app.js +1520 -0
  180. package/_site/lib/settings-store.js +113 -0
  181. package/_site/lib/storage-layout.js +162 -0
  182. package/_site/lib/structured-logger.js +574 -0
  183. package/_site/scripts/codex-notify-fanout.cjs +51 -0
  184. package/_site/scripts/folder-picker.ps1 +156 -0
  185. package/_site/scripts/hook-trigger.js +156 -0
  186. package/_site/scripts/sync-release-version.js +158 -0
  187. package/_site/server.js +12 -0
  188. package/_site/start.bat +26 -0
  189. package/_site/stop.bat +11 -0
  190. package/bin/project-knowledge-integrations.js +241 -0
  191. package/bin/project-knowledge-kb.js +65 -0
  192. package/bin/project-knowledge-mcp.js +195 -0
  193. package/bin/project-knowledge.js +439 -0
  194. package/claude-prompts.json +21 -0
  195. package/docs/README.zh-CN.md +175 -0
  196. package/docs/assets/architecture.svg +182 -0
  197. package/docs/assets/dashboard.png +0 -0
  198. package/docs/assets/draft-review.png +0 -0
  199. package/docs/assets/logo.svg +50 -0
  200. package/docs/assets/runs-drafts.png +0 -0
  201. package/docs/assets/settings.png +0 -0
  202. package/docs/project-registry-schema.md +33 -0
  203. package/docs/team-knowledge-mode-a-plan.md +73 -0
  204. package/docs/testing-strategy.md +33 -0
  205. package/package.json +100 -0
  206. package/plugins/project-knowledge/.claude-plugin/plugin.json +19 -0
  207. package/plugins/project-knowledge/.codex-plugin/plugin.json +38 -0
  208. package/plugins/project-knowledge/.mcp.json +14 -0
  209. package/plugins/project-knowledge/opencode/project-knowledge.md +3 -0
  210. package/plugins/project-knowledge/skills/project-knowledge/SKILL.md +28 -0
  211. package/plugins/project-knowledge/skills/project-knowledge/agents/openai.yaml +11 -0
  212. package/templates/change.md +30 -0
  213. package/templates/project-readme.md +52 -0
  214. package/ui/app.css +58 -0
  215. package/ui/app.js +607 -0
  216. package/ui/favicon.svg +38 -0
  217. package/ui/i18n.js +146 -0
  218. package/ui/index.html +162 -0
@@ -0,0 +1,82 @@
1
+ // Per-family context window sizes (in tokens) for common models.
2
+ // Match strategy: lowercase the model name, then prefix-match — first hit wins.
3
+ // Entry order matters: more specific prefixes (e.g. 'claude-opus-4') precede
4
+ // broader ones (e.g. 'claude'). Keep specific entries above generic ones.
5
+ const MODEL_CONTEXT_WINDOWS = [
6
+ // Anthropic Claude — full family 200K
7
+ ['claude-opus-4', 200000],
8
+ ['claude-sonnet-4', 200000],
9
+ ['claude-haiku-4', 200000],
10
+ ['claude-3-7', 200000],
11
+ ['claude-3-5', 200000],
12
+ ['claude-3', 200000],
13
+ ['claude', 200000],
14
+
15
+ // MiniMax
16
+ ['minimax-m3', 1048576],
17
+ ['minimax-m2', 1048576],
18
+ ['minimax', 1048576],
19
+
20
+ // Zhipu GLM
21
+ ['glm-5', 1048576],
22
+ ['glm-4.6', 1048576],
23
+ ['glm-4.5', 128000],
24
+ ['glm-4-plus', 128000],
25
+ ['glm-4', 128000],
26
+ ['glm', 128000],
27
+
28
+ // DeepSeek
29
+ ['deepseek-v3', 128000],
30
+ ['deepseek-r1', 128000],
31
+ ['deepseek', 64000],
32
+
33
+ // OpenAI
34
+ ['gpt-5', 200000],
35
+ ['gpt-4.1', 1047576],
36
+ ['gpt-4o', 128000],
37
+ ['gpt-4-turbo', 128000],
38
+ ['gpt-4', 8192],
39
+ ['o3', 200000],
40
+ ['o4-mini', 200000],
41
+ ['o1', 200000],
42
+
43
+ // Qwen
44
+ ['qwen3', 131072],
45
+ ['qwen-max', 32768],
46
+ ['qwen', 32768],
47
+
48
+ // Google Gemini
49
+ ['gemini-2.5', 1048576],
50
+ ['gemini-1.5', 1048576],
51
+ ['gemini', 32768],
52
+ ];
53
+
54
+ function normalizeModelName(model) {
55
+ return String(model || '').trim().toLowerCase();
56
+ }
57
+
58
+ function lookupContextWindow(modelName) {
59
+ const name = normalizeModelName(modelName);
60
+ if (!name) return null;
61
+ for (const [prefix, size] of MODEL_CONTEXT_WINDOWS) {
62
+ if (name.startsWith(prefix)) return size;
63
+ }
64
+ return null;
65
+ }
66
+
67
+ // Resolution order: explicit profile override → model table → env var → fallback.
68
+ function resolveContextWindow({ profileContextWindow, model, env = process.env, fallback = 200000 } = {}) {
69
+ const explicit = Number(profileContextWindow);
70
+ if (Number.isFinite(explicit) && explicit > 0) return Math.floor(explicit);
71
+ const tableHit = lookupContextWindow(model);
72
+ if (tableHit) return tableHit;
73
+ const envValue = Number(env && env.CLAUDE_CONTEXT_WINDOW);
74
+ if (Number.isFinite(envValue) && envValue > 0) return Math.floor(envValue);
75
+ return fallback;
76
+ }
77
+
78
+ module.exports = {
79
+ MODEL_CONTEXT_WINDOWS,
80
+ lookupContextWindow,
81
+ resolveContextWindow,
82
+ };
@@ -0,0 +1,496 @@
1
+ 'use strict';
2
+ /**
3
+ * Module bridge: connectivity from project-knowledge to the embedded modules
4
+ * (claude-ai-workbench Agent Terminal, vector-hub console, ai-coding-event-bridge
5
+ * conversation console).
6
+ *
7
+ * The shell owns no features — it only connects and projects. This module is
8
+ * that connection on the server side:
9
+ * - reverse proxy for browser calls (keeps the UI same-origin; no CORS on modules)
10
+ * - registration fan-out on project import (the "wiring table" lives in project state)
11
+ * - aggregated project projection for the shell sidebar
12
+ * - module removal with keepSessions semantics
13
+ *
14
+ * Configuration (env, loopback defaults):
15
+ * KB_TERMINAL_URL default http://127.0.0.1:5760
16
+ * KB_VECTORHUB_URL default http://127.0.0.1:8787
17
+ * KB_EVENTBRIDGE_URL default http://127.0.0.1:8790
18
+ * KB_MODULES_AUTOSTART '0' to stop spawning the vendored module services
19
+ * (they start with the shell by default)
20
+ * KB_TERMINAL_COMMAND explicit node command line for the terminal service
21
+ * KB_VECTORHUB_COMMAND explicit node command line for the vector-hub service
22
+ * KB_EVENTBRIDGE_COMMAND explicit node command line for the event-bridge console
23
+ */
24
+
25
+ const { spawn } = require('child_process');
26
+ const path = require('path');
27
+ const fs = require('fs');
28
+
29
+ const PROXY_TIMEOUT_MS = 10000;
30
+ // Registration drives a full embedding pass in vector-hub (remote API); a
31
+ // fresh knowledge folder can easily exceed the interactive proxy budget.
32
+ const REGISTER_TIMEOUT_MS = 60000;
33
+ // Native folder dialogs wait for the user without a bound.
34
+ const PICKER_TIMEOUT_MS = 10 * 60 * 1000;
35
+
36
+ // Vendored module runtimes shipped inside the npm package. Sibling-checkout
37
+ // spawning is gone: the services are bundled, spawned from here, and resolved
38
+ // through normal node_modules resolution
39
+ // node_modules.
40
+ // Module entry scripts resolve through normal node_modules dependencies
41
+ // (claude-ai-workbench, @sanqianx/vector-hub, @sanqianx/ai-coding-event-bridge-
42
+ // console) — the npm packages are the single source of these runtimes.
43
+ const MODULE_ENTRIES = {
44
+ terminal: 'claude-ai-workbench/packages/server/bin/agent-terminal-server.js',
45
+ 'vector-hub': '@sanqianx/vector-hub/bin.js',
46
+ 'event-bridge': '@sanqianx/ai-coding-event-bridge-console/src/bin.js',
47
+ };
48
+
49
+ function resolveModuleEntry(name) {
50
+ try { return require.resolve(MODULE_ENTRIES[name]); } catch { return null; }
51
+ }
52
+
53
+ function urlPort(baseUrl, fallback) {
54
+ try { return new URL(baseUrl).port || String(fallback); } catch { return String(fallback); }
55
+ }
56
+
57
+ function urlHost(baseUrl) {
58
+ try { return new URL(baseUrl).hostname || '127.0.0.1'; } catch { return '127.0.0.1'; }
59
+ }
60
+
61
+ function defaultTerminalUrl() {
62
+ return String(process.env.KB_TERMINAL_URL || 'http://127.0.0.1:5760').replace(/\/+$/, '');
63
+ }
64
+
65
+ function defaultVectorHubUrl() {
66
+ return String(process.env.KB_VECTORHUB_URL || 'http://127.0.0.1:8787').replace(/\/+$/, '');
67
+ }
68
+
69
+ function defaultEventBridgeUrl() {
70
+ return String(process.env.KB_EVENTBRIDGE_URL || 'http://127.0.0.1:8790').replace(/\/+$/, '');
71
+ }
72
+
73
+ class ModuleBridge {
74
+ constructor(options = {}) {
75
+ this.logger = options.logger || { info: () => {}, warn: () => {}, error: () => {} };
76
+ this.terminalUrl = options.terminalUrl || defaultTerminalUrl();
77
+ this.vectorHubUrl = options.vectorHubUrl || defaultVectorHubUrl();
78
+ this.eventBridgeUrl = options.eventBridgeUrl || defaultEventBridgeUrl();
79
+ this.registryStore = options.registryStore || null;
80
+ this.projectStore = options.projectStore || null;
81
+ this.dataDir = options.dataDir || null;
82
+ this.supervised = [];
83
+ }
84
+
85
+ _terminalApi(pathname) {
86
+ // /api/terminal/<rest> -> <terminal>/api/claude-workbench/v1/<rest>
87
+ const rest = pathname.replace(/^\/api\/terminal\/?/, '');
88
+ return `${this.terminalUrl}/api/claude-workbench/v1/${rest}`;
89
+ }
90
+
91
+ _vectorHubApi(pathname) {
92
+ // /api/vectorhub/<rest> -> <vectorhub>/api/<rest>
93
+ const rest = pathname.replace(/^\/api\/vectorhub\/?/, '');
94
+ return `${this.vectorHubUrl}/api/${rest}`;
95
+ }
96
+
97
+ _eventBridgeApi(pathname) {
98
+ // /api/eventbridge/<rest> -> <event-bridge console>/api/<rest>
99
+ const rest = pathname.replace(/^\/api\/eventbridge\/?/, '');
100
+ return `${this.eventBridgeUrl}/api/${rest}`;
101
+ }
102
+
103
+ async _fetch(url, init = {}) {
104
+ const controller = new AbortController();
105
+ const timer = setTimeout(() => controller.abort(), init.timeoutMs || PROXY_TIMEOUT_MS);
106
+ try {
107
+ return await fetch(url, { ...init, signal: controller.signal });
108
+ } finally {
109
+ clearTimeout(timer);
110
+ }
111
+ }
112
+
113
+ /** Same-origin reverse proxy for one browser request. Streams JSON bodies buffered. */
114
+ async proxy(req, res, target) {
115
+ const chunks = [];
116
+ for await (const chunk of req) chunks.push(chunk);
117
+ const body = chunks.length ? Buffer.concat(chunks) : undefined;
118
+ // The native folder pickers hold the request open until the user closes
119
+ // the dialog — they need a human-scale budget, not the API budget.
120
+ const isPicker = /\/(system\/)?pick-folder/.test(target);
121
+ const timeoutMs = isPicker ? PICKER_TIMEOUT_MS : PROXY_TIMEOUT_MS;
122
+ let upstream;
123
+ try {
124
+ upstream = await this._fetch(target, {
125
+ method: req.method,
126
+ timeoutMs,
127
+ headers: body ? { 'Content-Type': req.headers['content-type'] || 'application/json' } : {},
128
+ body: ['GET', 'HEAD'].includes(req.method) ? undefined : body,
129
+ });
130
+ } catch (error) {
131
+ res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
132
+ res.end(JSON.stringify({ ok: false, error: 'MODULE_UNREACHABLE', target }));
133
+ return;
134
+ }
135
+ const buf = Buffer.from(await upstream.arrayBuffer());
136
+ res.writeHead(upstream.status, {
137
+ 'Content-Type': upstream.headers.get('content-type') || 'application/json; charset=utf-8',
138
+ });
139
+ res.end(buf);
140
+ }
141
+
142
+ proxyTerminal(req, res, pathname, search) {
143
+ return this.proxy(req, res, this._terminalApi(pathname) + (search || ''));
144
+ }
145
+
146
+ proxyVectorHub(req, res, pathname, search) {
147
+ return this.proxy(req, res, this._vectorHubApi(pathname) + (search || ''));
148
+ }
149
+
150
+ proxyEventBridge(req, res, pathname, search) {
151
+ return this.proxy(req, res, this._eventBridgeApi(pathname) + (search || ''));
152
+ }
153
+
154
+ async _listTerminalProjects() {
155
+ try {
156
+ const response = await this._fetch(`${this.terminalUrl}/api/claude-workbench/v1/projects`);
157
+ if (!response.ok) return [];
158
+ const payload = await response.json();
159
+ const list = Array.isArray(payload) ? payload : payload.projects;
160
+ return Array.isArray(list) ? list : [];
161
+ } catch { return []; }
162
+ }
163
+
164
+ async _listVectorHubProjects() {
165
+ try {
166
+ const response = await this._fetch(`${this.vectorHubUrl}/api/projects`);
167
+ if (!response.ok) return [];
168
+ const payload = await response.json();
169
+ return Array.isArray(payload.projects) ? payload.projects : [];
170
+ } catch { return []; }
171
+ }
172
+
173
+ async _listEventBridgeProjects() {
174
+ try {
175
+ const response = await this._fetch(`${this.eventBridgeUrl}/api/projects`);
176
+ if (!response.ok) return [];
177
+ const payload = await response.json();
178
+ return Array.isArray(payload.projects) ? payload.projects : [];
179
+ } catch { return []; }
180
+ }
181
+
182
+ /** Aggregated projection for the shell sidebar: pkb registry is the spine. */
183
+ async aggregatedProjects() {
184
+ const [terminalProjects, vectorHubProjects, eventBridgeProjects] = await Promise.all([
185
+ this._listTerminalProjects(),
186
+ this._listVectorHubProjects(),
187
+ this._listEventBridgeProjects(),
188
+ ]);
189
+ const ids = this.registryStore ? this.registryStore.listIds() : [];
190
+ const byWorkspace = new Map(terminalProjects.map(p => [String(p.rootPath || '').toLowerCase(), p]));
191
+ const byName = new Map(vectorHubProjects.map(p => [String(p.name), p]));
192
+ const ebByPath = new Map(eventBridgeProjects.filter(p => p.path).map(p => [String(p.path).toLowerCase(), p]));
193
+ const projects = ids.map(projectId => {
194
+ const config = this.projectStore ? (this.projectStore.readConfig(projectId) || {}) : {};
195
+ const workspacePath = String(config.repoPath || '');
196
+ const knowledgePath = String(config.knowledgePath || '');
197
+ const state = this.projectStore ? this.projectStore.readState(projectId) : {};
198
+ const wiring = state.modules || {};
199
+ const terminal = byWorkspace.get(workspacePath.toLowerCase())
200
+ || (wiring.terminal && wiring.terminal.projectId ? terminalProjects.find(p => p.id === wiring.terminal.projectId) : null)
201
+ || null;
202
+ const vectorHubName = (wiring.vectorHub && wiring.vectorHub.projectName) || projectId;
203
+ const vectorHub = byName.get(vectorHubName) || null;
204
+ const eventBridge = ebByPath.get(workspacePath.toLowerCase())
205
+ || (wiring.eventBridge && wiring.eventBridge.projectId ? eventBridgeProjects.find(p => p.id === wiring.eventBridge.projectId) : null)
206
+ || null;
207
+ return {
208
+ projectId,
209
+ name: config.displayName || projectId,
210
+ workspacePath,
211
+ knowledgePath,
212
+ modules: {
213
+ terminal: terminal ? { registered: true, projectId: terminal.id, name: terminal.name } : { registered: false, status: wiring.terminal && wiring.terminal.status },
214
+ vectorHub: vectorHub ? { registered: true, projectName: vectorHub.name, docCount: vectorHub.docCount, chunkCount: vectorHub.chunkCount, watching: !!vectorHub.watching } : { registered: false, status: wiring.vectorHub && wiring.vectorHub.status },
215
+ eventBridge: eventBridge ? { registered: true, projectId: eventBridge.id, turns: eventBridge.turns, auto: !!eventBridge.auto } : { registered: false, status: wiring.eventBridge && wiring.eventBridge.status },
216
+ },
217
+ };
218
+ });
219
+ return {
220
+ projects,
221
+ terminalAvailable: terminalProjects.length > 0 || await this._probe(this.terminalUrl),
222
+ vectorHubAvailable: vectorHubProjects.length > 0 || await this._probe(this.vectorHubUrl),
223
+ eventBridgeAvailable: eventBridgeProjects.length > 0 || await this._probe(this.eventBridgeUrl),
224
+ };
225
+ }
226
+
227
+ async _probe(base) {
228
+ try { await this._fetch(base, { timeoutMs: 2000 }); return true; } catch { return false; }
229
+ }
230
+
231
+ async modulesHealth() {
232
+ const [terminal, vectorHub, eventBridge] = await Promise.all([
233
+ this._probe(this.terminalUrl),
234
+ this._probe(this.vectorHubUrl),
235
+ this._probe(this.eventBridgeUrl),
236
+ ]);
237
+ return { ok: true, modules: {
238
+ terminal: { url: this.terminalUrl, up: terminal },
239
+ vectorHub: { url: this.vectorHubUrl, up: vectorHub },
240
+ eventBridge: { url: this.eventBridgeUrl, up: eventBridge },
241
+ } };
242
+ }
243
+
244
+ /**
245
+ * Register one pkb project into both modules and persist the wiring table.
246
+ * Best-effort per module: a module that is down is recorded as 'pending'
247
+ * (retry on the next register call or health-check pass), never a hard error.
248
+ * KB_MODULES_REGISTER=0 disables fan-out entirely (test isolation).
249
+ */
250
+ async registerProjectLinks({ projectId, name, workspacePath, knowledgePath }) {
251
+ const result = { terminal: { ok: false }, vectorHub: { ok: false }, eventBridge: { ok: false } };
252
+ if (String(process.env.KB_MODULES_REGISTER || '') === '0') {
253
+ result.terminal = { ok: false, status: 'disabled' };
254
+ result.vectorHub = { ok: false, status: 'disabled' };
255
+ result.eventBridge = { ok: false, status: 'disabled' };
256
+ return result;
257
+ }
258
+ // 幂等登记:终端已有同 rootPath 的项目时复用,绝不重复创建
259
+ const existingTerminal = (await this._listTerminalProjects())
260
+ .find(p => String(p.rootPath || '').toLowerCase() === String(workspacePath || '').toLowerCase());
261
+ if (existingTerminal) {
262
+ result.terminal = { ok: true, projectId: existingTerminal.id, status: 'registered', reused: true };
263
+ } else {
264
+ try {
265
+ const response = await this._fetch(`${this.terminalUrl}/api/claude-workbench/v1/projects`, {
266
+ method: 'POST',
267
+ headers: { 'Content-Type': 'application/json' },
268
+ body: JSON.stringify({ name: name || projectId, rootPath: workspacePath }),
269
+ });
270
+ const payload = response.status === 201 || response.ok ? await response.json().catch(() => ({})) : {};
271
+ result.terminal = { ok: response.ok, projectId: payload.id || payload.projectId || null, status: response.ok ? 'registered' : 'error' };
272
+ } catch {
273
+ result.terminal = { ok: false, status: 'pending' };
274
+ }
275
+ }
276
+ try {
277
+ const response = await this._fetch(`${this.vectorHubUrl}/api/import`, {
278
+ method: 'POST',
279
+ headers: { 'Content-Type': 'application/json' },
280
+ timeoutMs: REGISTER_TIMEOUT_MS,
281
+ body: JSON.stringify({ sourceDir: knowledgePath, projectName: projectId, watch: true }),
282
+ });
283
+ result.vectorHub = { ok: response.ok, projectName: projectId, status: response.ok ? 'registered' : 'error' };
284
+ } catch {
285
+ result.vectorHub = { ok: false, status: 'pending' };
286
+ }
287
+ // 幂等登记:会话控制台按 path(Git work tree)复用已有登记。store 遵循
288
+ // event-bridge 的推荐布局 <知识库>/dev-conversations;无知识库地址时交给
289
+ // 控制台的默认 store(全局 journal),登记本身不受阻。
290
+ const existingEventBridge = (await this._listEventBridgeProjects())
291
+ .find(p => !p.auto && String(p.path || '').toLowerCase() === String(workspacePath || '').toLowerCase());
292
+ if (existingEventBridge) {
293
+ result.eventBridge = { ok: true, projectId: existingEventBridge.id, status: 'registered', reused: true };
294
+ } else {
295
+ try {
296
+ const body = { path: workspacePath };
297
+ if (knowledgePath) body.store = path.join(knowledgePath, 'dev-conversations');
298
+ const response = await this._fetch(`${this.eventBridgeUrl}/api/projects`, {
299
+ method: 'POST',
300
+ headers: { 'Content-Type': 'application/json' },
301
+ body: JSON.stringify(body),
302
+ });
303
+ const payload = response.status === 201 || response.ok ? await response.json().catch(() => ({})) : {};
304
+ const registeredProject = payload.project || {};
305
+ result.eventBridge = { ok: response.ok, projectId: registeredProject.id || null, status: response.ok ? 'registered' : 'error', errorCode: (!response.ok && payload.error && payload.error.code) || undefined };
306
+ } catch {
307
+ result.eventBridge = { ok: false, status: 'pending' };
308
+ }
309
+ }
310
+ await this._writeWiring(projectId, wiring => {
311
+ wiring.terminal = { ...wiring.terminal, ...result.terminal, name: name || projectId, rootPath: workspacePath, updatedAt: new Date().toISOString() };
312
+ wiring.vectorHub = { ...wiring.vectorHub, ...result.vectorHub, sourceDir: knowledgePath, updatedAt: new Date().toISOString() };
313
+ wiring.eventBridge = { ...wiring.eventBridge, ...result.eventBridge, path: workspacePath, store: knowledgePath ? path.join(knowledgePath, 'dev-conversations') : '', updatedAt: new Date().toISOString() };
314
+ });
315
+ this.logger.info('modules.register', 'Module registration finished.', { context: { projectId, terminal: result.terminal.status, vectorHub: result.vectorHub.status, eventBridge: result.eventBridge.status } });
316
+ return result;
317
+ }
318
+
319
+ /** Remove module registrations. Local data is preserved by contract: vector-hub
320
+ * only drops its derived index (source untouched); terminal removes the project
321
+ * with keepSessions=1 so session records survive; the event-bridge console only
322
+ * unregisters the project — journals and sealed commit documents stay on disk. */
323
+ async removeProjectLinks({ projectId }) {
324
+ const wiring = (this.projectStore ? this.projectStore.readState(projectId).modules : null) || {};
325
+ const result = { terminal: { ok: false }, vectorHub: { ok: false }, eventBridge: { ok: false } };
326
+ let terminalId = wiring.terminal && wiring.terminal.projectId;
327
+ if (!terminalId) {
328
+ const workspacePath = wiring.terminal && wiring.terminal.rootPath;
329
+ const match = (await this._listTerminalProjects())
330
+ .find(p => (workspacePath && String(p.rootPath || '').toLowerCase() === String(workspacePath).toLowerCase()) || p.name === projectId);
331
+ terminalId = match && match.id;
332
+ }
333
+ if (terminalId) {
334
+ try {
335
+ const response = await this._fetch(`${this.terminalUrl}/api/claude-workbench/v1/projects/${encodeURIComponent(terminalId)}?keepSessions=1`, { method: 'DELETE' });
336
+ result.terminal = { ok: response.ok || response.status === 204 };
337
+ } catch { result.terminal = { ok: false, status: 'pending' }; }
338
+ } else {
339
+ result.terminal = { ok: true, skipped: 'not-registered' };
340
+ }
341
+ const vectorHubName = (wiring.vectorHub && wiring.vectorHub.projectName) || projectId;
342
+ try {
343
+ const response = await this._fetch(`${this.vectorHubUrl}/api/project?name=${encodeURIComponent(vectorHubName)}`, { method: 'DELETE' });
344
+ result.vectorHub = { ok: response.ok || response.status === 204 || response.status === 404 };
345
+ } catch { result.vectorHub = { ok: false, status: 'pending' }; }
346
+ let eventBridgeId = wiring.eventBridge && wiring.eventBridge.projectId;
347
+ if (!eventBridgeId) {
348
+ const workspacePath = wiring.eventBridge && wiring.eventBridge.path;
349
+ const match = (await this._listEventBridgeProjects())
350
+ .find(p => !p.auto && workspacePath && String(p.path || '').toLowerCase() === String(workspacePath).toLowerCase());
351
+ eventBridgeId = match && match.id;
352
+ }
353
+ if (eventBridgeId) {
354
+ try {
355
+ const response = await this._fetch(`${this.eventBridgeUrl}/api/projects?id=${encodeURIComponent(eventBridgeId)}`, { method: 'DELETE' });
356
+ // 404 means already unregistered (or auto entry) — removal goal reached.
357
+ result.eventBridge = { ok: response.ok || response.status === 204 || response.status === 404 };
358
+ } catch { result.eventBridge = { ok: false, status: 'pending' }; }
359
+ } else {
360
+ result.eventBridge = { ok: true, skipped: 'not-registered' };
361
+ }
362
+ await this._writeWiring(projectId, wiring => {
363
+ if (result.terminal.ok) wiring.terminal = { status: 'removed', updatedAt: new Date().toISOString() };
364
+ if (result.vectorHub.ok) wiring.vectorHub = { status: 'removed', updatedAt: new Date().toISOString() };
365
+ if (result.eventBridge.ok) wiring.eventBridge = { status: 'removed', updatedAt: new Date().toISOString() };
366
+ });
367
+ this.logger.info('modules.remove', 'Module de-registration finished.', { context: { projectId, terminal: result.terminal, vectorHub: result.vectorHub, eventBridge: result.eventBridge } });
368
+ return result;
369
+ }
370
+
371
+ async _writeWiring(projectId, mutator) {
372
+ if (!this.projectStore) return;
373
+ await this.projectStore.updateState(projectId, state => {
374
+ state.modules = state.modules || {};
375
+ mutator(state.modules);
376
+ });
377
+ }
378
+
379
+ /**
380
+ * T3.5 process orchestration: spawn the vendored module services as
381
+ * supervised child processes. ON by default now that the services ship in
382
+ * the package — opt out with KB_MODULES_AUTOSTART=0. A service that is
383
+ * already listening on its URL is never spawned twice.
384
+ */
385
+ async startSupervised() {
386
+ if (String(process.env.KB_MODULES_AUTOSTART || '') === '0') return;
387
+ for (const spec of this._supervisedSpecs()) {
388
+ if (await this._probe(spec.url)) {
389
+ this.logger.info('modules.supervisor', 'Module service already up; not spawning.', { context: { name: spec.name, url: spec.url } });
390
+ continue;
391
+ }
392
+ this._spawnSupervised(spec);
393
+ }
394
+ }
395
+
396
+ _supervisedSpecs() {
397
+ const specs = [];
398
+ if (process.env.KB_TERMINAL_COMMAND) {
399
+ specs.push({ name: 'terminal', command: process.env.KB_TERMINAL_COMMAND, url: this.terminalUrl, env: {} });
400
+ } else {
401
+ const terminalEntry = resolveModuleEntry('terminal');
402
+ if (terminalEntry) {
403
+ specs.push({
404
+ name: 'terminal',
405
+ command: `node ${JSON.stringify(terminalEntry)}`,
406
+ url: this.terminalUrl,
407
+ env: { AGENT_TERMINAL_PORT: urlPort(this.terminalUrl, 5760), AGENT_TERMINAL_HOST: urlHost(this.terminalUrl) },
408
+ });
409
+ } else {
410
+ this.logger.warn('modules.supervisor', 'claude-ai-workbench runtime not installed; expecting an external service.', {});
411
+ }
412
+ }
413
+ // vector-hub declares engines >= 22; spawning it on older runtimes would
414
+ // crash-loop the supervisor for nothing.
415
+ if (process.env.KB_VECTORHUB_COMMAND) {
416
+ specs.push({ name: 'vector-hub', command: process.env.KB_VECTORHUB_COMMAND, url: this.vectorHubUrl, env: {} });
417
+ } else if (parseInt(process.versions.node, 10) < 22) {
418
+ this.logger.warn('modules.supervisor', 'vector-hub needs Node >= 22; not spawning the vendored runtime.', { context: { node: process.versions.node } });
419
+ } else {
420
+ const vectorHubEntry = resolveModuleEntry('vector-hub');
421
+ if (!vectorHubEntry) {
422
+ this.logger.warn('modules.supervisor', '@sanqianx/vector-hub runtime not installed; expecting an external service.', {});
423
+ } else {
424
+ const dataDir = this.dataDir || require('./data-dir').getDataDir();
425
+ const root = path.join(dataDir, 'vectorhub');
426
+ specs.push({
427
+ name: 'vector-hub',
428
+ command: `node ${JSON.stringify(vectorHubEntry)} serve --port ${urlPort(this.vectorHubUrl, 8787)} --root ${JSON.stringify(root)}`,
429
+ url: this.vectorHubUrl,
430
+ env: { VECTOR_HUB_ROOT: root },
431
+ });
432
+ }
433
+ }
434
+ // ai-coding-event-bridge console: plain-JS CommonJS on Node >= 18 — no
435
+ // build gate, no runtime floor beyond the shell's own. The home flag keeps
436
+ // the console on the same bridge home the in-process BridgeAdapter uses,
437
+ // so conversations captured by PK show up in the embedded explorer. The
438
+ // folder-picker warm-up is an interactive-only feature the shell never
439
+ // routes here (import uses the terminal/vector-hub pickers) — skip it.
440
+ if (process.env.KB_EVENTBRIDGE_COMMAND) {
441
+ specs.push({ name: 'event-bridge', command: process.env.KB_EVENTBRIDGE_COMMAND, url: this.eventBridgeUrl, env: {} });
442
+ } else {
443
+ const eventBridgeEntry = resolveModuleEntry('event-bridge');
444
+ if (!eventBridgeEntry) {
445
+ this.logger.warn('modules.supervisor', '@sanqianx/ai-coding-event-bridge-console runtime not installed; expecting an external service.', {});
446
+ } else {
447
+ const homeArgs = process.env.AI_CODING_EVENT_BRIDGE_HOME
448
+ ? ` --home ${JSON.stringify(process.env.AI_CODING_EVENT_BRIDGE_HOME)}`
449
+ : '';
450
+ specs.push({
451
+ name: 'event-bridge',
452
+ command: `node ${JSON.stringify(eventBridgeEntry)} serve --host ${urlHost(this.eventBridgeUrl)} --port ${urlPort(this.eventBridgeUrl, 8790)}${homeArgs}`,
453
+ url: this.eventBridgeUrl,
454
+ env: { BRIDGE_CONSOLE_PICK_FOLDER: '0' },
455
+ });
456
+ }
457
+ }
458
+ return specs;
459
+ }
460
+
461
+ _spawnSupervised(spec) {
462
+ // Strip the quoting: spawn takes argv, not a shell line — a literal
463
+ // `"path"` argument makes node load a file whose name contains quotes.
464
+ const parts = (spec.command.match(/"[^"]+"|\S+/g) || [])
465
+ .map(part => (part.startsWith('"') && part.endsWith('"') ? part.slice(1, -1) : part));
466
+ const child = spawn(parts[0], parts.slice(1), {
467
+ // Module banners (ports, data roots) are diagnostics — inherit stdout
468
+ // so they land on the console in --fg mode and in launcher.log when the
469
+ // shell itself runs detached.
470
+ stdio: ['ignore', 'inherit', 'inherit'],
471
+ windowsHide: true,
472
+ env: { ...process.env, ...(spec.env || {}) },
473
+ });
474
+ let attempts = 0;
475
+ child.on('exit', code => {
476
+ attempts += 1;
477
+ if (attempts > 5 || this.stopped) {
478
+ this.logger.warn('modules.supervisor', 'Module service stopped.', { context: { name: spec.name, code, attempts } });
479
+ return;
480
+ }
481
+ const delay = Math.min(1000 * 2 ** attempts, 15000);
482
+ this.logger.warn('modules.supervisor', 'Module service exited; restarting.', { context: { name: spec.name, code, retryInMs: delay } });
483
+ setTimeout(() => this._spawnSupervised({ ...spec, _attempts: attempts }), delay);
484
+ });
485
+ this.supervised.push(child);
486
+ this.logger.info('modules.supervisor', 'Module service spawned.', { context: { name: spec.name, pid: child.pid } });
487
+ }
488
+
489
+ stopSupervised() {
490
+ this.stopped = true;
491
+ for (const child of this.supervised) { try { child.kill(); } catch { /* already gone */ } }
492
+ this.supervised = [];
493
+ }
494
+ }
495
+
496
+ module.exports = { ModuleBridge, defaultTerminalUrl, defaultVectorHubUrl, defaultEventBridgeUrl };