@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.
- package/.agents/plugins/marketplace.json +20 -0
- package/.claude-plugin/marketplace.json +17 -0
- package/CHANGELOG.md +780 -0
- package/LICENSE +201 -0
- package/NOTICE +5 -0
- package/README.md +266 -0
- package/_site/README.md +30 -0
- package/_site/_test/ai-profile-resolver-test.js +137 -0
- package/_site/_test/ai-profile-test.js +75 -0
- package/_site/_test/ai-vendor-presets-test.js +91 -0
- package/_site/_test/automation-queue-test.js +14 -0
- package/_site/_test/automation-ui-test.js +40 -0
- package/_site/_test/background-task-registry-test.js +43 -0
- package/_site/_test/baseline-schema-test.js +91 -0
- package/_site/_test/bin-cli-test.js +218 -0
- package/_site/_test/bridge-adapter-test.js +95 -0
- package/_site/_test/bridge-consumer-service-test.js +310 -0
- package/_site/_test/cdp-browser-failure-test.js +100 -0
- package/_site/_test/chat-claudecodeui-match-test.js +92 -0
- package/_site/_test/claude-executable-discovery-test.js +90 -0
- package/_site/_test/claude-session-lifecycle-sweep-test.js +252 -0
- package/_site/_test/claude-workbench-test.js +170 -0
- package/_site/_test/client-capture-restart-e2e-test.js +189 -0
- package/_site/_test/codex-conversation-projection-test.js +59 -0
- package/_site/_test/codex-notify-fanout-test.js +45 -0
- package/_site/_test/commit-boundary-freeze-test.js +147 -0
- package/_site/_test/commit-conversation-binding-test.js +99 -0
- package/_site/_test/commit-evidence-test.js +110 -0
- package/_site/_test/context-pack-test.js +198 -0
- package/_site/_test/conversation-api-test.js +119 -0
- package/_site/_test/conversation-store-test.js +136 -0
- package/_site/_test/cross-repo-isolation-e2e-test.js +187 -0
- package/_site/_test/data-dir-migration-test.js +77 -0
- package/_site/_test/desktop-browser-compat-test.js +28 -0
- package/_site/_test/desktop-hook-runtime-regression-test.js +204 -0
- package/_site/_test/embedding-config-test.js +32 -0
- package/_site/_test/explicit-commit-processor-test.js +37 -0
- package/_site/_test/fixtures/fake-browser-exit.js +10 -0
- package/_site/_test/fixtures/fake-browser-hang.js +10 -0
- package/_site/_test/fixtures/make-git-repos.js +89 -0
- package/_site/_test/folder-picker-output-test.js +19 -0
- package/_site/_test/full-integration-e2e-test.js +165 -0
- package/_site/_test/git-validation-test.js +81 -0
- package/_site/_test/hook-runtime-endpoint-test.js +88 -0
- package/_site/_test/hook-status-repair-api-test.js +152 -0
- package/_site/_test/hook-trigger-test.js +85 -0
- package/_site/_test/hook-worktree-test.js +42 -0
- package/_site/_test/import-preflight-api-test.js +233 -0
- package/_site/_test/import-ui-flow-test.js +171 -0
- package/_site/_test/index-builder-test.js +41 -0
- package/_site/_test/index-writer-concurrency-test.js +135 -0
- package/_site/_test/integration-adapters-test.js +243 -0
- package/_site/_test/integration-capture-setup-test.js +108 -0
- package/_site/_test/integration-surface-coverage-test.js +165 -0
- package/_site/_test/kb-framework-test.js +57 -0
- package/_site/_test/kbpath-follow-test.js +63 -0
- package/_site/_test/knowledge-db-test.js +144 -0
- package/_site/_test/knowledge-language-control-test.js +170 -0
- package/_site/_test/knowledge-maintenance-test.js +40 -0
- package/_site/_test/knowledge-migration-test.js +75 -0
- package/_site/_test/knowledge-promotion-recovery-test.js +260 -0
- package/_site/_test/knowledge-query-test.js +54 -0
- package/_site/_test/knowledge-retrieval-service-test.js +77 -0
- package/_site/_test/knowledge-scopes-test.js +37 -0
- package/_site/_test/knowledge-storage-location-test.js +28 -0
- package/_site/_test/knowledge-storage-startup-test.js +52 -0
- package/_site/_test/knowledge-store-logs-supervision-test.js +89 -0
- package/_site/_test/legacy-claude-session-alias-test.js +106 -0
- package/_site/_test/legacy-forward-compat-test.js +257 -0
- package/_site/_test/legacy-project-upgrade-e2e-test.js +363 -0
- package/_site/_test/legacy-routes-removed-test.js +47 -0
- package/_site/_test/legacy-workbench-exclusion-test.js +92 -0
- package/_site/_test/log-redaction-test.js +58 -0
- package/_site/_test/logging-api-test.js +89 -0
- package/_site/_test/logging-sse-no-gap-test.js +88 -0
- package/_site/_test/logging-ui-test.js +104 -0
- package/_site/_test/markdown-delta-overlay-test.js +85 -0
- package/_site/_test/markdown-maintenance-api-test.js +75 -0
- package/_site/_test/markdown-maintenance-test.js +11 -0
- package/_site/_test/mcp-server-test.js +149 -0
- package/_site/_test/model-context-windows-test.js +89 -0
- package/_site/_test/module-bridge-eventbridge-test.js +193 -0
- package/_site/_test/non-release-ci-test.js +34 -0
- package/_site/_test/offline-boundary-isolation-test.js +32 -0
- package/_site/_test/orphaned-claim-recovery-test.js +80 -0
- package/_site/_test/p0-data-migration-characterization-test.js +37 -0
- package/_site/_test/p0-e2e-gate-test.js +347 -0
- package/_site/_test/package-boundary-test.js +42 -0
- package/_site/_test/package-startup-test.js +56 -0
- package/_site/_test/packaged-ui-smoke-test.js +79 -0
- package/_site/_test/path-consistency-test.js +151 -0
- package/_site/_test/pending-sweep-test.js +9 -0
- package/_site/_test/post-commit-automation-test.js +87 -0
- package/_site/_test/pr-consumer-contract-test.js +55 -0
- package/_site/_test/project-control-panel-task14-test.js +63 -0
- package/_site/_test/project-delete-recovery-test.js +64 -0
- package/_site/_test/project-goal-editor-test.js +145 -0
- package/_site/_test/project-layout-v2-migration-test.js +127 -0
- package/_site/_test/project-lifecycle-transaction-test.js +106 -0
- package/_site/_test/project-remove-running-guard-test.js +79 -0
- package/_site/_test/project-store-test.js +90 -0
- package/_site/_test/prompt-settings-test.js +115 -0
- package/_site/_test/protected-architecture-gate-test.js +123 -0
- package/_site/_test/refactor-characterization-test.js +36 -0
- package/_site/_test/release-version-sync-test.js +98 -0
- package/_site/_test/requirement-binding-test.js +113 -0
- package/_site/_test/requirement-recorder-test.js +173 -0
- package/_site/_test/run-all-tests.js +156 -0
- package/_site/_test/runtime-endpoint-test.js +64 -0
- package/_site/_test/scanner-test.js +83 -0
- package/_site/_test/server-runtime-migration-safety-test.js +27 -0
- package/_site/_test/server-security-test.js +96 -0
- package/_site/_test/sessions-stream-test.js +92 -0
- package/_site/_test/shared-contracts-test.js +58 -0
- package/_site/_test/simple-import-test.js +97 -0
- package/_site/_test/startup-analysis-disabled-test.js +16 -0
- package/_site/_test/storage-foundation-test.js +78 -0
- package/_site/_test/structured-logger-test.js +101 -0
- package/_site/_test/task15-20-ui-flow-test.js +148 -0
- package/_site/_test/team-knowledge-store-sparse-checkout-test.js +196 -0
- package/_site/_test/team-knowledge-store-test.js +131 -0
- package/_site/_test/tracking-start-test.js +143 -0
- package/_site/_test/ui-i18n-toggle-test.js +114 -0
- package/_site/_test/ui-smoke-test.js +73 -0
- package/_site/_test/v4122-upgrade-data-contract-test.js +56 -0
- package/_site/_test/workbench-permission-test.js +118 -0
- package/_site/_test/workspace-ui-contract-test.js +43 -0
- package/_site/lib/ai-profile-resolver.js +78 -0
- package/_site/lib/ai-vendor-presets.js +83 -0
- package/_site/lib/ai-workspace.js +104 -0
- package/_site/lib/atomic-file.js +204 -0
- package/_site/lib/automation-config.js +140 -0
- package/_site/lib/bridge-adapter.js +256 -0
- package/_site/lib/bridge-consumer-service.js +402 -0
- package/_site/lib/claude-cli-runner.js +1599 -0
- package/_site/lib/commit-conversation-binder.js +246 -0
- package/_site/lib/commit-processing-ledger.js +27 -0
- package/_site/lib/commit-prompt.js +202 -0
- package/_site/lib/commit-reconciler.js +444 -0
- package/_site/lib/context-pack-builder.js +299 -0
- package/_site/lib/contracts.js +279 -0
- package/_site/lib/conversation-exclusions.js +204 -0
- package/_site/lib/conversation-query-service.js +161 -0
- package/_site/lib/conversation-store.js +385 -0
- package/_site/lib/data-dir.js +81 -0
- package/_site/lib/data-state-classifier.js +40 -0
- package/_site/lib/embedding-service.js +85 -0
- package/_site/lib/evidence-bundle.js +228 -0
- package/_site/lib/folder-picker-output.js +24 -0
- package/_site/lib/git-runner.js +62 -0
- package/_site/lib/github-team-store.js +1175 -0
- package/_site/lib/hook-manager.js +202 -0
- package/_site/lib/index-builder.js +157 -0
- package/_site/lib/index-service.js +173 -0
- package/_site/lib/integration-installer.js +907 -0
- package/_site/lib/kb-framework.js +191 -0
- package/_site/lib/kb-validator.js +124 -0
- package/_site/lib/knowledge-db.js +363 -0
- package/_site/lib/knowledge-promotion.js +435 -0
- package/_site/lib/knowledge-retrieval-service.js +401 -0
- package/_site/lib/knowledge-schema.js +121 -0
- package/_site/lib/knowledge-tool-runtime.js +359 -0
- package/_site/lib/legacy-data-manifest.js +26 -0
- package/_site/lib/llm-client.js +173 -0
- package/_site/lib/markdown-knowledge-indexer.js +230 -0
- package/_site/lib/migration-service.js +442 -0
- package/_site/lib/model-context-windows.js +82 -0
- package/_site/lib/module-bridge.js +496 -0
- package/_site/lib/post-commit-automation.js +141 -0
- package/_site/lib/project-lifecycle-service.js +511 -0
- package/_site/lib/project-registry-store.js +129 -0
- package/_site/lib/project-store.js +262 -0
- package/_site/lib/prompt-registry.js +148 -0
- package/_site/lib/requirement-adapters.js +10 -0
- package/_site/lib/requirement-binder.js +112 -0
- package/_site/lib/requirement-recorder.js +278 -0
- package/_site/lib/runtime-endpoint.js +160 -0
- package/_site/lib/scanner.js +298 -0
- package/_site/lib/server-app.js +1520 -0
- package/_site/lib/settings-store.js +113 -0
- package/_site/lib/storage-layout.js +162 -0
- package/_site/lib/structured-logger.js +574 -0
- package/_site/scripts/codex-notify-fanout.cjs +51 -0
- package/_site/scripts/folder-picker.ps1 +156 -0
- package/_site/scripts/hook-trigger.js +156 -0
- package/_site/scripts/sync-release-version.js +158 -0
- package/_site/server.js +12 -0
- package/_site/start.bat +26 -0
- package/_site/stop.bat +11 -0
- package/bin/project-knowledge-integrations.js +241 -0
- package/bin/project-knowledge-kb.js +65 -0
- package/bin/project-knowledge-mcp.js +195 -0
- package/bin/project-knowledge.js +439 -0
- package/claude-prompts.json +21 -0
- package/docs/README.zh-CN.md +175 -0
- package/docs/assets/architecture.svg +182 -0
- package/docs/assets/dashboard.png +0 -0
- package/docs/assets/draft-review.png +0 -0
- package/docs/assets/logo.svg +50 -0
- package/docs/assets/runs-drafts.png +0 -0
- package/docs/assets/settings.png +0 -0
- package/docs/project-registry-schema.md +33 -0
- package/docs/team-knowledge-mode-a-plan.md +73 -0
- package/docs/testing-strategy.md +33 -0
- package/package.json +100 -0
- package/plugins/project-knowledge/.claude-plugin/plugin.json +19 -0
- package/plugins/project-knowledge/.codex-plugin/plugin.json +38 -0
- package/plugins/project-knowledge/.mcp.json +14 -0
- package/plugins/project-knowledge/opencode/project-knowledge.md +3 -0
- package/plugins/project-knowledge/skills/project-knowledge/SKILL.md +28 -0
- package/plugins/project-knowledge/skills/project-knowledge/agents/openai.yaml +11 -0
- package/templates/change.md +30 -0
- package/templates/project-readme.md +52 -0
- package/ui/app.css +58 -0
- package/ui/app.js +607 -0
- package/ui/favicon.svg +38 -0
- package/ui/i18n.js +146 -0
- 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 };
|