@sanqianx/project-knowledge 5.0.18 → 5.0.20-rc.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 +3 -3
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +14 -0
- package/LICENSE +201 -201
- package/NOTICE +5 -5
- package/README.md +349 -349
- package/_modules/knowledge-engine/README.md +4 -0
- package/_modules/knowledge-engine/src/analysis-contract.js +4 -3
- package/_modules/knowledge-engine/src/analysis-failures.js +39 -0
- package/_modules/knowledge-engine/src/analysis-worker.js +6 -2
- package/_modules/knowledge-engine/src/coordinator.js +8 -0
- package/_modules/knowledge-engine/src/headless-analyzer.js +2 -1
- package/_modules/knowledge-engine/src/memories.js +10 -5
- package/_modules/knowledge-engine/src/memory-eligibility.js +74 -0
- package/_modules/knowledge-engine/src/retrieval-ranking.js +19 -1
- package/_modules/knowledge-engine/src/runtime/commit-conversation-binder.js +1 -0
- package/_modules/knowledge-engine/src/runtime/commit-prompt.js +12 -9
- package/_modules/knowledge-engine/src/runtime/conversation-store.js +2 -0
- package/_modules/knowledge-engine/src/server.js +1 -1
- package/_site/_test/agent-capture-mcp-ui-test.js +67 -67
- package/_site/_test/ai-profile-test.js +75 -75
- package/_site/_test/ai-vendor-presets-test.js +91 -91
- package/_site/_test/automatic-context-test.js +70 -70
- package/_site/_test/bin-cli-test.js +230 -230
- package/_site/_test/bridge-adapter-test.js +95 -95
- package/_site/_test/bridge-consumer-service-test.js +310 -310
- package/_site/_test/candidate-bin-content-test.js +35 -0
- package/_site/_test/candidate-product-assets-test.js +13 -0
- package/_site/_test/candidate-release-contract-test.js +14 -14
- package/_site/_test/capture-hook-dedup-test.js +158 -158
- package/_site/_test/claude-executable-discovery-test.js +28 -28
- package/_site/_test/claude-session-lifecycle-sweep-test.js +252 -252
- package/_site/_test/cli-instance-management-test.js +62 -62
- package/_site/_test/client-capture-restart-e2e-test.js +189 -189
- package/_site/_test/codex-conversation-projection-test.js +60 -60
- package/_site/_test/codex-marketplace-scoped-path-test.js +186 -186
- package/_site/_test/codex-notify-capture-e2e-test.js +101 -101
- package/_site/_test/codex-notify-fanout-test.js +134 -134
- package/_site/_test/codex-notify-upgrade-test.js +253 -253
- package/_site/_test/commit-boundary-freeze-test.js +147 -147
- package/_site/_test/commit-conversation-binding-test.js +4 -4
- package/_site/_test/configuration-generation-test.js +22 -22
- package/_site/_test/context-pack-test.js +200 -200
- package/_site/_test/conversation-api-test.js +10 -10
- package/_site/_test/conversation-origin-test.js +15 -0
- package/_site/_test/cross-repo-isolation-e2e-test.js +187 -187
- package/_site/_test/embedding-config-test.js +32 -32
- package/_site/_test/fixtures/fake-browser-exit.js +10 -10
- package/_site/_test/fixtures/fake-browser-hang.js +10 -10
- package/_site/_test/fixtures/integration-busy-worker.cjs +6 -6
- package/_site/_test/fixtures/make-git-repos.js +92 -92
- package/_site/_test/fixtures/retrieval-gold-queries.json +32 -0
- package/_site/_test/fresh-hook-reimport-test.js +53 -53
- package/_site/_test/fresh-product-data-test.js +56 -56
- package/_site/_test/git-path-compatibility-test.js +80 -80
- package/_site/_test/git-validation-test.js +81 -81
- package/_site/_test/hook-worktree-test.js +42 -42
- package/_site/_test/index-builder-test.js +41 -41
- package/_site/_test/index-error-display-contract-test.js +10 -10
- package/_site/_test/integration-adapters-test.js +67 -67
- package/_site/_test/integration-capture-setup-test.js +118 -118
- package/_site/_test/integration-runtime-binding-a2-test.js +566 -566
- package/_site/_test/integration-runtime-binding-a3-test.js +227 -227
- package/_site/_test/integration-runtime-binding-test.js +473 -473
- package/_site/_test/integration-status-scan-test.js +29 -29
- package/_site/_test/kb-framework-test.js +57 -57
- package/_site/_test/kbpath-follow-test.js +63 -63
- package/_site/_test/knowledge-db-test.js +144 -144
- package/_site/_test/knowledge-engine-client-test.js +109 -109
- package/_site/_test/knowledge-maintenance-test.js +40 -40
- package/_site/_test/knowledge-panel-ui-contract-test.js +112 -0
- package/_site/_test/knowledge-scopes-test.js +37 -37
- package/_site/_test/knowledge-storage-location-test.js +28 -28
- package/_site/_test/launcher-readiness-test.js +80 -80
- package/_site/_test/legacy-claude-session-alias-test.js +106 -106
- package/_site/_test/legacy-routes-removed-test.js +47 -47
- package/_site/_test/legacy-workbench-exclusion-test.js +92 -92
- package/_site/_test/live-gateway-test.js +48 -48
- package/_site/_test/local-marketplace-test.js +16 -16
- package/_site/_test/log-redaction-test.js +58 -58
- package/_site/_test/markdown-maintenance-test.js +11 -11
- package/_site/_test/model-context-windows-test.js +89 -89
- package/_site/_test/module-agent-start-proxy-test.js +65 -65
- package/_site/_test/module-artifact-integrity-test.js +27 -27
- package/_site/_test/module-boundary-test.js +30 -30
- package/_site/_test/module-bridge-eventbridge-test.js +202 -202
- package/_site/_test/module-dynamic-port-test.js +118 -118
- package/_site/_test/module-index-proxy-test.js +33 -0
- package/_site/_test/module-model-configuration-test.js +44 -44
- package/_site/_test/module-process-lifecycle-test.js +35 -35
- package/_site/_test/module-stream-proxy-test.js +34 -34
- package/_site/_test/module-terminal-restart-test.js +63 -0
- package/_site/_test/module-wiring-unbind-race-test.js +59 -59
- package/_site/_test/native-context-integrations-test.js +86 -86
- package/_site/_test/orphaned-claim-recovery-test.js +80 -80
- package/_site/_test/package-boundary-test.js +43 -42
- package/_site/_test/package-startup-test.js +56 -56
- package/_site/_test/packaged-ui-smoke-test.js +88 -88
- package/_site/_test/pr-consumer-contract-test.js +55 -55
- package/_site/_test/product-brand-contract-test.js +25 -25
- package/_site/_test/product-diagnostics-ui-test.js +93 -93
- package/_site/_test/product-import-ui-test.js +104 -104
- package/_site/_test/product-progress-status-test.js +12 -1
- package/_site/_test/project-remove-running-guard-test.js +79 -79
- package/_site/_test/project-store-test.js +90 -90
- package/_site/_test/registry-events-test.js +38 -0
- package/_site/_test/release-version-sync-test.js +115 -115
- package/_site/_test/requirement-binding-test.js +113 -113
- package/_site/_test/runtime-endpoint-test.js +64 -64
- package/_site/_test/scanner-test.js +83 -83
- package/_site/_test/server-security-test.js +96 -96
- package/_site/_test/sessions-stream-test.js +93 -93
- package/_site/_test/shared-scope-root-binding-test.js +67 -67
- package/_site/_test/simple-import-test.js +98 -98
- package/_site/_test/startup-analysis-disabled-test.js +6 -6
- package/_site/_test/team-knowledge-store-sparse-checkout-test.js +196 -196
- package/_site/_test/team-knowledge-store-test.js +132 -132
- package/_site/_test/test-server-cleanup-test.js +35 -35
- package/_site/_test/test-server-module-isolation-test.js +37 -37
- package/_site/_test/tracking-start-test.js +35 -35
- package/_site/_test/workbench-context-receipts-test.js +22 -0
- package/_site/_test/workspace-ui-contract-test.js +31 -31
- package/_site/_test/zcode-capture-integration-test.js +108 -108
- package/_site/lib/ai-vendor-presets.js +83 -83
- package/_site/lib/ai-workspace.js +3 -3
- package/_site/lib/atomic-file.js +204 -204
- package/_site/lib/bridge-adapter.js +260 -260
- package/_site/lib/capture-hook-config.js +108 -108
- package/_site/lib/claude-cli-runner.js +89 -89
- package/_site/lib/cli-instances.js +43 -43
- package/_site/lib/codex-context-status.js +17 -17
- package/_site/lib/codex-notify-config.js +193 -193
- package/_site/lib/commit-conversation-binder.js +13 -12
- package/_site/lib/commit-reconciler.js +38 -38
- package/_site/lib/context-pack-builder.js +299 -299
- package/_site/lib/conversation-exclusions.js +204 -204
- package/_site/lib/conversation-query-service.js +17 -17
- package/_site/lib/conversation-store.js +2 -0
- package/_site/lib/data-dir.js +13 -13
- package/_site/lib/embedding-service.js +85 -85
- package/_site/lib/engine-connection.js +93 -90
- package/_site/lib/engine-knowledge-client.js +72 -72
- package/_site/lib/folder-picker-output.js +3 -3
- package/_site/lib/git-path.js +14 -14
- package/_site/lib/github-team-store.js +3 -3
- package/_site/lib/index-builder.js +157 -157
- package/_site/lib/integration-status-scan.js +99 -99
- package/_site/lib/integration-status-worker.js +14 -14
- package/_site/lib/kb-framework.js +15 -15
- package/_site/lib/knowledge-db.js +363 -363
- package/_site/lib/knowledge-schema.js +121 -121
- package/_site/lib/live-gateway.js +78 -76
- package/_site/lib/llm-client.js +12 -12
- package/_site/lib/model-context-windows.js +82 -82
- package/_site/lib/module-bridge.js +874 -865
- package/_site/lib/module-port-resolver.js +57 -57
- package/_site/lib/post-commit-automation.js +54 -54
- package/_site/lib/product-data.js +35 -35
- package/_site/lib/project-lifecycle-service.js +14 -14
- package/_site/lib/project-registry-store.js +129 -129
- package/_site/lib/prompt-registry.js +148 -148
- package/_site/lib/registry-events.js +16 -0
- package/_site/lib/requirement-adapters.js +10 -10
- package/_site/lib/requirement-binder.js +112 -112
- package/_site/lib/runtime-control.js +89 -89
- package/_site/lib/runtime-endpoint.js +5 -5
- package/_site/lib/server-app.js +10 -1
- package/_site/lib/zcode-context-integration.js +60 -60
- package/_site/scripts/codex-notify-fanout.cjs +95 -95
- package/_site/scripts/install-module-candidates.js +69 -69
- package/_site/scripts/pack-module-candidates.js +89 -72
- package/_site/scripts/sync-product-content.js +28 -28
- package/_site/scripts/sync-release-version.js +164 -164
- package/_site/scripts/vendor-modules.js +49 -49
- package/_site/scripts/verify-module-runtime.js +50 -50
- package/_site/scripts/verify-product-runtime.js +36 -36
- package/_site/scripts/verify-retrieval-gold.js +83 -0
- package/_site/server.js +15 -15
- package/_site/start.bat +26 -26
- package/_site/stop.bat +11 -11
- package/bin/project-knowledge-integrations.js +254 -254
- package/bin/project-knowledge-kb.js +67 -67
- package/bin/project-knowledge-mcp.js +8 -8
- package/bin/project-knowledge.js +170 -170
- package/claude-prompts.json +21 -21
- package/docs/README.zh-CN.md +205 -205
- package/docs/assets/architecture.svg +181 -181
- package/docs/assets/logo.svg +49 -49
- package/docs/fresh-v5-start.md +42 -42
- package/docs/leader-glm-workbench-playbook.md +122 -119
- package/docs/project-registry-schema.md +11 -11
- package/docs/release-5.0.16-acceptance.md +176 -176
- package/docs/release-5.0.17-acceptance.md +117 -117
- package/docs/release-5.0.19-acceptance.md +39 -0
- package/docs/team-knowledge-mode-a-plan.md +73 -73
- package/module-runtime-manifest.json +126 -104
- package/package.json +110 -108
- package/plugins/project-knowledge/.claude-plugin/plugin.json +1 -1
- package/plugins/project-knowledge/.codex-plugin/plugin.json +1 -1
- package/plugins/project-knowledge/.mcp.json +1 -1
- package/plugins/project-knowledge/opencode/project-knowledge.md +3 -3
- package/plugins/project-knowledge/skills/project-knowledge/SKILL.md +28 -28
- package/templates/change.md +30 -30
- package/templates/project-readme.md +18 -18
- package/ui/index.html +1 -1
- package/ui/knowledge-panel.html +118 -0
- package/ui/product.css +8 -0
- package/ui/product.js +175 -8
- package/vendor-manifest.json +55 -53
|
@@ -1,89 +1,89 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
const net = require('node:net');
|
|
3
|
-
const fs = require('node:fs');
|
|
4
|
-
const path = require('node:path');
|
|
5
|
-
const crypto = require('node:crypto');
|
|
6
|
-
|
|
7
|
-
const SCHEMA = 'project-knowledge/runtime-control/v1';
|
|
8
|
-
function normalizeDataDir(value) {
|
|
9
|
-
const absolute = path.resolve(value);
|
|
10
|
-
return process.platform === 'win32' ? absolute.toLowerCase() : absolute;
|
|
11
|
-
}
|
|
12
|
-
function controlPath(dataDir) {
|
|
13
|
-
const hash = crypto.createHash('sha256').update(normalizeDataDir(dataDir)).digest('hex').slice(0, 32);
|
|
14
|
-
return process.platform === 'win32' ? `\\\\.\\pipe\\project-knowledge-${hash}` : path.join(dataDir, 'runtime-control.sock');
|
|
15
|
-
}
|
|
16
|
-
function identity(dataDir, endpoint = {}) {
|
|
17
|
-
return { schema: SCHEMA, name: 'project-knowledge', pid: process.pid, dataDir: path.resolve(dataDir), ...endpoint };
|
|
18
|
-
}
|
|
19
|
-
function request(dataDir, message, timeoutMs = 45000) {
|
|
20
|
-
return new Promise((resolve, reject) => {
|
|
21
|
-
const socket = net.createConnection(controlPath(dataDir));
|
|
22
|
-
let buffer = '', done = false;
|
|
23
|
-
const timer = setTimeout(() => socket.destroy(new Error('知识库控制请求超时,停止尚未确认。')), timeoutMs);
|
|
24
|
-
const finish = (error, result) => {
|
|
25
|
-
if (done) return; done = true;
|
|
26
|
-
clearTimeout(timer); socket.destroy();
|
|
27
|
-
if (error) reject(error); else resolve(result);
|
|
28
|
-
};
|
|
29
|
-
socket.once('connect', () => socket.write(JSON.stringify(message) + '\n'));
|
|
30
|
-
socket.on('data', chunk => {
|
|
31
|
-
buffer += chunk.toString();
|
|
32
|
-
if (buffer.length > 65536) return finish(new Error('无效的实例响应。'));
|
|
33
|
-
if (!buffer.includes('\n')) return;
|
|
34
|
-
try {
|
|
35
|
-
const value = JSON.parse(buffer.slice(0, buffer.indexOf('\n')));
|
|
36
|
-
if (!value.ok) return finish(new Error(value.message || '实例控制失败。'));
|
|
37
|
-
if (value.instance?.schema !== SCHEMA || normalizeDataDir(value.instance.dataDir) !== normalizeDataDir(dataDir)) return finish(new Error('实例身份不匹配,已拒绝控制。'));
|
|
38
|
-
finish(null, value);
|
|
39
|
-
} catch (error) { finish(error); }
|
|
40
|
-
});
|
|
41
|
-
socket.once('error', error => finish(error));
|
|
42
|
-
socket.once('end', () => { if (!buffer.includes('\n')) finish(new Error('实例断开连接,停止尚未确认。')); });
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async function listen(dataDir, info, stop) {
|
|
47
|
-
const address = controlPath(dataDir);
|
|
48
|
-
if (process.platform !== 'win32' && fs.existsSync(address)) {
|
|
49
|
-
try { await request(dataDir, { action: 'status' }, 1000); throw new Error('另一个知识库实例已占用控制通道。'); }
|
|
50
|
-
catch (error) {
|
|
51
|
-
if (!['ECONNREFUSED', 'ENOENT'].includes(error.code)) throw error;
|
|
52
|
-
fs.unlinkSync(address);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
const sockets = new Set();
|
|
56
|
-
let stopping = false, stoppingSocket;
|
|
57
|
-
const server = net.createServer(socket => {
|
|
58
|
-
sockets.add(socket); socket.once('close', () => sockets.delete(socket));
|
|
59
|
-
socket.on('error', () => {});
|
|
60
|
-
socket.setTimeout(45000, () => socket.destroy());
|
|
61
|
-
let buffer = '';
|
|
62
|
-
socket.on('data', chunk => {
|
|
63
|
-
buffer += chunk.toString();
|
|
64
|
-
if (buffer.length > 16384) return socket.destroy();
|
|
65
|
-
if (!buffer.includes('\n')) return;
|
|
66
|
-
socket.removeAllListeners('data');
|
|
67
|
-
const respond = value => { if (!socket.destroyed) socket.end(JSON.stringify({ ...value, instance: info }) + '\n'); };
|
|
68
|
-
let message;
|
|
69
|
-
try { message = JSON.parse(buffer.slice(0, buffer.indexOf('\n'))); }
|
|
70
|
-
catch { return respond({ ok: false, message: '无效的控制请求。' }); }
|
|
71
|
-
if (message.action === 'status') return respond({ ok: true, status: stopping ? 'stopping' : 'running' });
|
|
72
|
-
// A stale endpoint/port must never become a signal to an unrelated PID.
|
|
73
|
-
if (message.action !== 'stop' || message.pid !== info.pid || message.port !== info.port || message.startedAt !== info.startedAt) return respond({ ok: false, message: '实例身份已变化,已拒绝停止。' });
|
|
74
|
-
if (stopping) return respond({ ok: false, message: '实例正在停止,请稍后查询状态。' });
|
|
75
|
-
stopping = true; stoppingSocket = socket;
|
|
76
|
-
Promise.resolve().then(stop).then(() => respond({ ok: true, status: 'stopped' })).catch(error => respond({ ok: false, message: error.message }));
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
await new Promise((resolve, reject) => { server.once('error', reject); server.listen(address, resolve); });
|
|
80
|
-
if (process.platform !== 'win32') fs.chmodSync(address, 0o600);
|
|
81
|
-
return {
|
|
82
|
-
close() {
|
|
83
|
-
// Keep only the requesting socket for the final shutdown acknowledgement.
|
|
84
|
-
return new Promise(resolve => { server.close(resolve); for (const socket of sockets) if (socket !== stoppingSocket) socket.destroy(); if (stopping) resolve(); });
|
|
85
|
-
},
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
module.exports = { SCHEMA, normalizeDataDir, identity, controlPath, request, listen };
|
|
1
|
+
'use strict';
|
|
2
|
+
const net = require('node:net');
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const crypto = require('node:crypto');
|
|
6
|
+
|
|
7
|
+
const SCHEMA = 'project-knowledge/runtime-control/v1';
|
|
8
|
+
function normalizeDataDir(value) {
|
|
9
|
+
const absolute = path.resolve(value);
|
|
10
|
+
return process.platform === 'win32' ? absolute.toLowerCase() : absolute;
|
|
11
|
+
}
|
|
12
|
+
function controlPath(dataDir) {
|
|
13
|
+
const hash = crypto.createHash('sha256').update(normalizeDataDir(dataDir)).digest('hex').slice(0, 32);
|
|
14
|
+
return process.platform === 'win32' ? `\\\\.\\pipe\\project-knowledge-${hash}` : path.join(dataDir, 'runtime-control.sock');
|
|
15
|
+
}
|
|
16
|
+
function identity(dataDir, endpoint = {}) {
|
|
17
|
+
return { schema: SCHEMA, name: 'project-knowledge', pid: process.pid, dataDir: path.resolve(dataDir), ...endpoint };
|
|
18
|
+
}
|
|
19
|
+
function request(dataDir, message, timeoutMs = 45000) {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
const socket = net.createConnection(controlPath(dataDir));
|
|
22
|
+
let buffer = '', done = false;
|
|
23
|
+
const timer = setTimeout(() => socket.destroy(new Error('知识库控制请求超时,停止尚未确认。')), timeoutMs);
|
|
24
|
+
const finish = (error, result) => {
|
|
25
|
+
if (done) return; done = true;
|
|
26
|
+
clearTimeout(timer); socket.destroy();
|
|
27
|
+
if (error) reject(error); else resolve(result);
|
|
28
|
+
};
|
|
29
|
+
socket.once('connect', () => socket.write(JSON.stringify(message) + '\n'));
|
|
30
|
+
socket.on('data', chunk => {
|
|
31
|
+
buffer += chunk.toString();
|
|
32
|
+
if (buffer.length > 65536) return finish(new Error('无效的实例响应。'));
|
|
33
|
+
if (!buffer.includes('\n')) return;
|
|
34
|
+
try {
|
|
35
|
+
const value = JSON.parse(buffer.slice(0, buffer.indexOf('\n')));
|
|
36
|
+
if (!value.ok) return finish(new Error(value.message || '实例控制失败。'));
|
|
37
|
+
if (value.instance?.schema !== SCHEMA || normalizeDataDir(value.instance.dataDir) !== normalizeDataDir(dataDir)) return finish(new Error('实例身份不匹配,已拒绝控制。'));
|
|
38
|
+
finish(null, value);
|
|
39
|
+
} catch (error) { finish(error); }
|
|
40
|
+
});
|
|
41
|
+
socket.once('error', error => finish(error));
|
|
42
|
+
socket.once('end', () => { if (!buffer.includes('\n')) finish(new Error('实例断开连接,停止尚未确认。')); });
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function listen(dataDir, info, stop) {
|
|
47
|
+
const address = controlPath(dataDir);
|
|
48
|
+
if (process.platform !== 'win32' && fs.existsSync(address)) {
|
|
49
|
+
try { await request(dataDir, { action: 'status' }, 1000); throw new Error('另一个知识库实例已占用控制通道。'); }
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (!['ECONNREFUSED', 'ENOENT'].includes(error.code)) throw error;
|
|
52
|
+
fs.unlinkSync(address);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const sockets = new Set();
|
|
56
|
+
let stopping = false, stoppingSocket;
|
|
57
|
+
const server = net.createServer(socket => {
|
|
58
|
+
sockets.add(socket); socket.once('close', () => sockets.delete(socket));
|
|
59
|
+
socket.on('error', () => {});
|
|
60
|
+
socket.setTimeout(45000, () => socket.destroy());
|
|
61
|
+
let buffer = '';
|
|
62
|
+
socket.on('data', chunk => {
|
|
63
|
+
buffer += chunk.toString();
|
|
64
|
+
if (buffer.length > 16384) return socket.destroy();
|
|
65
|
+
if (!buffer.includes('\n')) return;
|
|
66
|
+
socket.removeAllListeners('data');
|
|
67
|
+
const respond = value => { if (!socket.destroyed) socket.end(JSON.stringify({ ...value, instance: info }) + '\n'); };
|
|
68
|
+
let message;
|
|
69
|
+
try { message = JSON.parse(buffer.slice(0, buffer.indexOf('\n'))); }
|
|
70
|
+
catch { return respond({ ok: false, message: '无效的控制请求。' }); }
|
|
71
|
+
if (message.action === 'status') return respond({ ok: true, status: stopping ? 'stopping' : 'running' });
|
|
72
|
+
// A stale endpoint/port must never become a signal to an unrelated PID.
|
|
73
|
+
if (message.action !== 'stop' || message.pid !== info.pid || message.port !== info.port || message.startedAt !== info.startedAt) return respond({ ok: false, message: '实例身份已变化,已拒绝停止。' });
|
|
74
|
+
if (stopping) return respond({ ok: false, message: '实例正在停止,请稍后查询状态。' });
|
|
75
|
+
stopping = true; stoppingSocket = socket;
|
|
76
|
+
Promise.resolve().then(stop).then(() => respond({ ok: true, status: 'stopped' })).catch(error => respond({ ok: false, message: error.message }));
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
await new Promise((resolve, reject) => { server.once('error', reject); server.listen(address, resolve); });
|
|
80
|
+
if (process.platform !== 'win32') fs.chmodSync(address, 0o600);
|
|
81
|
+
return {
|
|
82
|
+
close() {
|
|
83
|
+
// Keep only the requesting socket for the final shutdown acknowledgement.
|
|
84
|
+
return new Promise(resolve => { server.close(resolve); for (const socket of sockets) if (socket !== stoppingSocket) socket.destroy(); if (stopping) resolve(); });
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = { SCHEMA, normalizeDataDir, identity, controlPath, request, listen };
|
|
@@ -88,8 +88,8 @@ function writeEndpoint(dataDir, value) {
|
|
|
88
88
|
} catch (error) {
|
|
89
89
|
// Windows cannot always replace an existing file with renameSync.
|
|
90
90
|
try { fs.unlinkSync(target); } catch {
|
|
91
|
-
// The following rename is authoritative and reports endpoint replacement failure.
|
|
92
|
-
}
|
|
91
|
+
// The following rename is authoritative and reports endpoint replacement failure.
|
|
92
|
+
}
|
|
93
93
|
fs.renameSync(temp, target);
|
|
94
94
|
}
|
|
95
95
|
return endpoint;
|
|
@@ -122,9 +122,9 @@ function claimEndpoint(dataDir, value) {
|
|
|
122
122
|
try {
|
|
123
123
|
const ageMs = Date.now() - fs.statSync(target).mtimeMs;
|
|
124
124
|
if (!existing && ageMs < 2000) return { claimed: false, endpoint: null };
|
|
125
|
-
} catch {
|
|
126
|
-
// clearEndpoint performs the ownership-safe final check when stat is unavailable.
|
|
127
|
-
}
|
|
125
|
+
} catch {
|
|
126
|
+
// clearEndpoint performs the ownership-safe final check when stat is unavailable.
|
|
127
|
+
}
|
|
128
128
|
clearEndpoint(dataDir, existing ? { pid: existing.pid } : {});
|
|
129
129
|
}
|
|
130
130
|
}
|
package/_site/lib/server-app.js
CHANGED
|
@@ -512,6 +512,7 @@ function createRuntime(options = {}) {
|
|
|
512
512
|
analysisOwner: options.analysisOwner || 'legacy', ownership: { owner: options.analysisOwner || 'legacy', ready: false },
|
|
513
513
|
rootDir, dataPath, layout, settingsStore, registryStore, projectStore, logger, logRepository,
|
|
514
514
|
activeTasks, bridgeAdapter, bridgeConsumerService, conversationStore, conversationQuery, indexAdapter, indexService, promotionService, reconciler, lifecycleService, requirementRecorder, knowledgeRuntime, moduleBridge,
|
|
515
|
+
registryEvents: new (require('./registry-events').RegistryEvents)(),
|
|
515
516
|
};
|
|
516
517
|
}
|
|
517
518
|
|
|
@@ -867,6 +868,9 @@ function createRequestHandler(runtime, options = {}) {
|
|
|
867
868
|
res.setHeader('X-Operation-Id', requestOperationId);
|
|
868
869
|
res.once('finish', () => {
|
|
869
870
|
if (requestFailed || res.statusCode >= 400) return;
|
|
871
|
+
if (['POST', 'PATCH', 'PUT', 'DELETE'].includes(req.method)
|
|
872
|
+
&& /^\/api\/(?:projects(?:\/|$)|domains(?:\/|$)|brain$|settings$|modules\/configure$)/.test(requestPathname)
|
|
873
|
+
&& requestPathname !== '/api/projects/preflight-import') runtime.registryEvents?.publish();
|
|
870
874
|
Promise.resolve(runtime.logger.info('http.request_completed', 'HTTP request completed.', {
|
|
871
875
|
operationId: requestOperationId,
|
|
872
876
|
phase: 'completed',
|
|
@@ -890,6 +894,9 @@ function createRequestHandler(runtime, options = {}) {
|
|
|
890
894
|
const url = new URL(req.url, `http://${req.headers.host || `${host}:${port}`}`);
|
|
891
895
|
const method = req.method || 'GET';
|
|
892
896
|
const pathname = url.pathname;
|
|
897
|
+
if (method === 'GET' && pathname === '/api/registry/events') {
|
|
898
|
+
return streamSse(res, write => runtime.registryEvents.subscribe(write));
|
|
899
|
+
}
|
|
893
900
|
|
|
894
901
|
const moduleUi = pathname.match(/^\/modules\/(terminal|vectorhub|eventbridge|engine)\/(.*)$/);
|
|
895
902
|
if (moduleUi && method === 'GET') return runtime.moduleBridge.proxyModuleUi(req, res, moduleUi[1], moduleUi[2], url.search);
|
|
@@ -1542,7 +1549,8 @@ async function startServer(options = {}) {
|
|
|
1542
1549
|
const configurationTimer = setInterval(() => { connectEngine(runtime).catch(() => {}); }, 30000);
|
|
1543
1550
|
configurationTimer.unref?.();
|
|
1544
1551
|
let readyTimer;
|
|
1545
|
-
runtime.moduleBridge.onReady = () => { clearTimeout(readyTimer); readyTimer = setTimeout(() => { runtime.projectConnections?.clear(); connectEngine(runtime).catch(() => {}); }, 100); };
|
|
1552
|
+
runtime.moduleBridge.onReady = () => { runtime.registryEvents?.publish(['health', 'projects']); clearTimeout(readyTimer); readyTimer = setTimeout(() => { runtime.projectConnections?.clear(); connectEngine(runtime).catch(() => {}); }, 100); };
|
|
1553
|
+
runtime.moduleBridge.onStateChange = () => runtime.registryEvents?.publish(['health']);
|
|
1546
1554
|
connectEngine(runtime).catch(() => {});
|
|
1547
1555
|
|
|
1548
1556
|
// T3.5 进程编排:拉起包内自带的模块服务并守护(默认开启,KB_MODULES_AUTOSTART=0 关闭)
|
|
@@ -1582,6 +1590,7 @@ async function startServer(options = {}) {
|
|
|
1582
1590
|
clearInterval(maintenanceTimer);
|
|
1583
1591
|
clearInterval(configurationTimer);
|
|
1584
1592
|
clearTimeout(readyTimer); runtime.moduleBridge.onReady = null;
|
|
1593
|
+
runtime.moduleBridge.onStateChange = null;
|
|
1585
1594
|
runtime.engineConnectionClosed = true;
|
|
1586
1595
|
runtime.liveGateway?.close();
|
|
1587
1596
|
await runtime.logger.info('server.shutdown_started', 'Server shutdown started.', { phase: 'shutdown', context: { reason } });
|
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
const fs = require('node:fs');
|
|
3
|
-
const path = require('node:path');
|
|
4
|
-
const AtomicFile = require('./atomic-file');
|
|
5
|
-
|
|
6
|
-
function configuration(file) {
|
|
7
|
-
if (!fs.existsSync(file)) return {};
|
|
8
|
-
let value; try { value = JSON.parse(fs.readFileSync(file, 'utf8')); } catch { throw new Error('ZCode 配置不是有效 JSON,未覆盖原文件。'); }
|
|
9
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('ZCode 配置必须是 JSON 对象,未覆盖原文件。');
|
|
10
|
-
return value;
|
|
11
|
-
}
|
|
12
|
-
const scriptFor = root => path.join(root, 'plugins/project-knowledge/scripts/context-hook.cjs');
|
|
13
|
-
const normalize = file => { const value = path.resolve(file).replace(/\\/g, '/'); return process.platform === 'win32' ? value.toLowerCase() : value; };
|
|
14
|
-
function owned(hook, root) {
|
|
15
|
-
if (hook?.type !== 'process' || hook.command !== 'node' || !Array.isArray(hook.args) || hook.args.length !== 3 || typeof hook.args[0] !== 'string' || !path.isAbsolute(hook.args[0]) || hook.args[1] !== '--client' || hook.args[2] !== 'zcode') return false;
|
|
16
|
-
const file = normalize(hook.args[0]);
|
|
17
|
-
// Only the exact current script or a canonical managed npm installation is
|
|
18
|
-
// ours. An unrelated script with a similar basename remains untouched.
|
|
19
|
-
return file === normalize(scriptFor(root)) || /\/node_modules\/(?:@sanqianx\/)?project-knowledge\/plugins\/project-knowledge\/scripts\/context-hook\.cjs$/i.test(file);
|
|
20
|
-
}
|
|
21
|
-
function status({ configFile, rootDir }) {
|
|
22
|
-
const config = configuration(configFile);
|
|
23
|
-
const entries = (config.hooks?.events?.UserPromptSubmit || []).flatMap(g => Array.isArray(g?.hooks) ? g.hooks.filter(h => owned(h, rootDir)) : []);
|
|
24
|
-
const installed = entries.length === 1 && normalize(entries[0].args[0]) === normalize(scriptFor(rootDir)) && fs.existsSync(scriptFor(rootDir));
|
|
25
|
-
const disabled = config.hooks?.enabled === false;
|
|
26
|
-
return { state: disabled ? 'disabled' : installed && config.hooks?.enabled === true ? 'installed' : entries.length ? 'needs-repair' : 'not-installed', supported: true, installed: installed && !disabled && config.hooks?.enabled === true, detail: disabled ? 'ZCode 配置 Hook 已被用户禁用,未擅自启用。' : installed ? '原生 UserPromptSubmit 只读上下文入口已配置;MCP 插件状态不作已安装承诺。' : entries.length ? '知识读取入口仍指向旧安装或重复配置,请接入/修复;未误报就绪。' : '开发前知识读取尚未接入;与会话采集独立。' };
|
|
27
|
-
}
|
|
28
|
-
function install({ configFile, rootDir, dryRun }) {
|
|
29
|
-
const config = configuration(configFile);
|
|
30
|
-
if (config.hooks?.enabled === false) throw new Error('ZCode Hook 已被用户禁用,请在客户端明确启用后重试;未覆盖配置。');
|
|
31
|
-
if (!fs.existsSync(scriptFor(rootDir))) throw new Error('缺少已安装的知识读取脚本。');
|
|
32
|
-
if (!dryRun) {
|
|
33
|
-
if (config.hooks && (typeof config.hooks !== 'object' || Array.isArray(config.hooks))) throw new Error('ZCode Hook 配置格式无效,未覆盖。');
|
|
34
|
-
config.hooks ||= {}; config.hooks.events ||= {};
|
|
35
|
-
if (typeof config.hooks.events !== 'object' || Array.isArray(config.hooks.events)) throw new Error('ZCode Hook 事件配置格式无效,未覆盖。');
|
|
36
|
-
const groups = config.hooks.events.UserPromptSubmit || [];
|
|
37
|
-
if (!Array.isArray(groups)) throw new Error('ZCode UserPromptSubmit 配置格式无效,未覆盖。');
|
|
38
|
-
const hook = { type: 'process', command: 'node', args: [scriptFor(rootDir), '--client', 'zcode'], timeoutMs: 12000 };
|
|
39
|
-
let found = false;
|
|
40
|
-
const updated = groups.map(group => {
|
|
41
|
-
if (!Array.isArray(group?.hooks)) return group;
|
|
42
|
-
const hooks = group.hooks.flatMap(h => { if (!owned(h, rootDir)) return [h]; if (found) return []; found = true; return [hook]; });
|
|
43
|
-
return hooks.length || !group.hooks.length ? { ...group, hooks } : null;
|
|
44
|
-
}).filter(Boolean);
|
|
45
|
-
if (!found) updated.push({ hooks: [hook] });
|
|
46
|
-
config.hooks.events.UserPromptSubmit = updated; config.hooks.enabled = true;
|
|
47
|
-
AtomicFile.writeJsonAtomic(configFile, config);
|
|
48
|
-
}
|
|
49
|
-
return { state: dryRun ? 'planned' : 'installed', supported: true, installed: !dryRun, detail: 'ZCode 原生前置读取已配置,保留原模型、第三方 Hook 与采集入口;请重新打开会话。' };
|
|
50
|
-
}
|
|
51
|
-
function uninstall({ configFile, rootDir, dryRun }) {
|
|
52
|
-
const config = configuration(configFile), groups = config.hooks?.events?.UserPromptSubmit;
|
|
53
|
-
if (!dryRun && Array.isArray(groups)) {
|
|
54
|
-
const kept = groups.map(group => { if (!Array.isArray(group?.hooks)) return group; const hooks = group.hooks.filter(h => !owned(h, rootDir)); return hooks.length ? { ...group, hooks } : null; }).filter(Boolean);
|
|
55
|
-
if (kept.length) config.hooks.events.UserPromptSubmit = kept; else delete config.hooks.events.UserPromptSubmit;
|
|
56
|
-
AtomicFile.writeJsonAtomic(configFile, config);
|
|
57
|
-
}
|
|
58
|
-
return { state: dryRun ? 'planned' : 'removed', supported: true, installed: false, detail: '已移除知识上下文入口,采集、模型和第三方设置保留。' };
|
|
59
|
-
}
|
|
60
|
-
module.exports = { install, status, uninstall };
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const AtomicFile = require('./atomic-file');
|
|
5
|
+
|
|
6
|
+
function configuration(file) {
|
|
7
|
+
if (!fs.existsSync(file)) return {};
|
|
8
|
+
let value; try { value = JSON.parse(fs.readFileSync(file, 'utf8')); } catch { throw new Error('ZCode 配置不是有效 JSON,未覆盖原文件。'); }
|
|
9
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('ZCode 配置必须是 JSON 对象,未覆盖原文件。');
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
const scriptFor = root => path.join(root, 'plugins/project-knowledge/scripts/context-hook.cjs');
|
|
13
|
+
const normalize = file => { const value = path.resolve(file).replace(/\\/g, '/'); return process.platform === 'win32' ? value.toLowerCase() : value; };
|
|
14
|
+
function owned(hook, root) {
|
|
15
|
+
if (hook?.type !== 'process' || hook.command !== 'node' || !Array.isArray(hook.args) || hook.args.length !== 3 || typeof hook.args[0] !== 'string' || !path.isAbsolute(hook.args[0]) || hook.args[1] !== '--client' || hook.args[2] !== 'zcode') return false;
|
|
16
|
+
const file = normalize(hook.args[0]);
|
|
17
|
+
// Only the exact current script or a canonical managed npm installation is
|
|
18
|
+
// ours. An unrelated script with a similar basename remains untouched.
|
|
19
|
+
return file === normalize(scriptFor(root)) || /\/node_modules\/(?:@sanqianx\/)?project-knowledge\/plugins\/project-knowledge\/scripts\/context-hook\.cjs$/i.test(file);
|
|
20
|
+
}
|
|
21
|
+
function status({ configFile, rootDir }) {
|
|
22
|
+
const config = configuration(configFile);
|
|
23
|
+
const entries = (config.hooks?.events?.UserPromptSubmit || []).flatMap(g => Array.isArray(g?.hooks) ? g.hooks.filter(h => owned(h, rootDir)) : []);
|
|
24
|
+
const installed = entries.length === 1 && normalize(entries[0].args[0]) === normalize(scriptFor(rootDir)) && fs.existsSync(scriptFor(rootDir));
|
|
25
|
+
const disabled = config.hooks?.enabled === false;
|
|
26
|
+
return { state: disabled ? 'disabled' : installed && config.hooks?.enabled === true ? 'installed' : entries.length ? 'needs-repair' : 'not-installed', supported: true, installed: installed && !disabled && config.hooks?.enabled === true, detail: disabled ? 'ZCode 配置 Hook 已被用户禁用,未擅自启用。' : installed ? '原生 UserPromptSubmit 只读上下文入口已配置;MCP 插件状态不作已安装承诺。' : entries.length ? '知识读取入口仍指向旧安装或重复配置,请接入/修复;未误报就绪。' : '开发前知识读取尚未接入;与会话采集独立。' };
|
|
27
|
+
}
|
|
28
|
+
function install({ configFile, rootDir, dryRun }) {
|
|
29
|
+
const config = configuration(configFile);
|
|
30
|
+
if (config.hooks?.enabled === false) throw new Error('ZCode Hook 已被用户禁用,请在客户端明确启用后重试;未覆盖配置。');
|
|
31
|
+
if (!fs.existsSync(scriptFor(rootDir))) throw new Error('缺少已安装的知识读取脚本。');
|
|
32
|
+
if (!dryRun) {
|
|
33
|
+
if (config.hooks && (typeof config.hooks !== 'object' || Array.isArray(config.hooks))) throw new Error('ZCode Hook 配置格式无效,未覆盖。');
|
|
34
|
+
config.hooks ||= {}; config.hooks.events ||= {};
|
|
35
|
+
if (typeof config.hooks.events !== 'object' || Array.isArray(config.hooks.events)) throw new Error('ZCode Hook 事件配置格式无效,未覆盖。');
|
|
36
|
+
const groups = config.hooks.events.UserPromptSubmit || [];
|
|
37
|
+
if (!Array.isArray(groups)) throw new Error('ZCode UserPromptSubmit 配置格式无效,未覆盖。');
|
|
38
|
+
const hook = { type: 'process', command: 'node', args: [scriptFor(rootDir), '--client', 'zcode'], timeoutMs: 12000 };
|
|
39
|
+
let found = false;
|
|
40
|
+
const updated = groups.map(group => {
|
|
41
|
+
if (!Array.isArray(group?.hooks)) return group;
|
|
42
|
+
const hooks = group.hooks.flatMap(h => { if (!owned(h, rootDir)) return [h]; if (found) return []; found = true; return [hook]; });
|
|
43
|
+
return hooks.length || !group.hooks.length ? { ...group, hooks } : null;
|
|
44
|
+
}).filter(Boolean);
|
|
45
|
+
if (!found) updated.push({ hooks: [hook] });
|
|
46
|
+
config.hooks.events.UserPromptSubmit = updated; config.hooks.enabled = true;
|
|
47
|
+
AtomicFile.writeJsonAtomic(configFile, config);
|
|
48
|
+
}
|
|
49
|
+
return { state: dryRun ? 'planned' : 'installed', supported: true, installed: !dryRun, detail: 'ZCode 原生前置读取已配置,保留原模型、第三方 Hook 与采集入口;请重新打开会话。' };
|
|
50
|
+
}
|
|
51
|
+
function uninstall({ configFile, rootDir, dryRun }) {
|
|
52
|
+
const config = configuration(configFile), groups = config.hooks?.events?.UserPromptSubmit;
|
|
53
|
+
if (!dryRun && Array.isArray(groups)) {
|
|
54
|
+
const kept = groups.map(group => { if (!Array.isArray(group?.hooks)) return group; const hooks = group.hooks.filter(h => !owned(h, rootDir)); return hooks.length ? { ...group, hooks } : null; }).filter(Boolean);
|
|
55
|
+
if (kept.length) config.hooks.events.UserPromptSubmit = kept; else delete config.hooks.events.UserPromptSubmit;
|
|
56
|
+
AtomicFile.writeJsonAtomic(configFile, config);
|
|
57
|
+
}
|
|
58
|
+
return { state: dryRun ? 'planned' : 'removed', supported: true, installed: false, detail: '已移除知识上下文入口,采集、模型和第三方设置保留。' };
|
|
59
|
+
}
|
|
60
|
+
module.exports = { install, status, uninstall };
|
|
@@ -1,95 +1,95 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
// Codex supports one notify command. This fail-open fan-out preserves an
|
|
5
|
-
// existing notifier and invokes the Bridge beside it.
|
|
6
|
-
const { spawn } = require('child_process');
|
|
7
|
-
const os = require('os');
|
|
8
|
-
const path = require('path');
|
|
9
|
-
|
|
10
|
-
function commandFrom(args, marker) {
|
|
11
|
-
const index = args.indexOf(marker);
|
|
12
|
-
if (index < 0 || !args[index + 1]) return null;
|
|
13
|
-
try {
|
|
14
|
-
const command = JSON.parse(Buffer.from(args[index + 1], 'base64').toString('utf8'));
|
|
15
|
-
return Array.isArray(command) && command.length && command.every(part => typeof part === 'string') ? command : null;
|
|
16
|
-
} catch {
|
|
17
|
-
return null;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function readStdin() {
|
|
22
|
-
return new Promise(resolve => {
|
|
23
|
-
let value = '';
|
|
24
|
-
process.stdin.setEncoding('utf8');
|
|
25
|
-
process.stdin.on('data', chunk => { value += chunk; });
|
|
26
|
-
process.stdin.on('end', () => resolve(value));
|
|
27
|
-
process.stdin.on('error', () => resolve(value));
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function payloadArgument(args) {
|
|
32
|
-
const last = args.length - 1;
|
|
33
|
-
if (last < 0 || ['--next-base64', '--bridge-base64'].includes(args[last - 1])) return null;
|
|
34
|
-
try {
|
|
35
|
-
const payload = JSON.parse(args[last]);
|
|
36
|
-
return payload && typeof payload === 'object' && !Array.isArray(payload) ? args[last] : null;
|
|
37
|
-
} catch {
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function bridgePayload(raw) {
|
|
43
|
-
try {
|
|
44
|
-
const payload = JSON.parse(raw);
|
|
45
|
-
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return raw;
|
|
46
|
-
let changed = false;
|
|
47
|
-
if (!payload.session_id && !payload.sessionId && typeof payload['thread-id'] === 'string') {
|
|
48
|
-
payload.session_id = payload['thread-id'];
|
|
49
|
-
changed = true;
|
|
50
|
-
}
|
|
51
|
-
if (!payload.turn_id && !payload.turnId && typeof payload['turn-id'] === 'string') {
|
|
52
|
-
payload.turn_id = payload['turn-id'];
|
|
53
|
-
changed = true;
|
|
54
|
-
}
|
|
55
|
-
// The notification only wakes the authoritative rollout reader. It must
|
|
56
|
-
// never substitute its summary fields for captured conversation evidence.
|
|
57
|
-
return changed ? JSON.stringify(payload) : raw;
|
|
58
|
-
} catch {
|
|
59
|
-
return raw;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function invoke(command, payload, env, transport = 'stdin') {
|
|
64
|
-
if (!command) return;
|
|
65
|
-
try {
|
|
66
|
-
const [executable, ...args] = command;
|
|
67
|
-
const child = spawn(executable, transport === 'argv' ? [...args, payload] : args, {
|
|
68
|
-
detached: true,
|
|
69
|
-
stdio: [transport === 'argv' ? 'ignore' : 'pipe', 'ignore', 'ignore'],
|
|
70
|
-
windowsHide: true,
|
|
71
|
-
env,
|
|
72
|
-
});
|
|
73
|
-
child.on('error', () => {});
|
|
74
|
-
if (child.stdin) {
|
|
75
|
-
child.stdin.on('error', () => {});
|
|
76
|
-
child.stdin.end(payload || '');
|
|
77
|
-
}
|
|
78
|
-
child.unref();
|
|
79
|
-
} catch {
|
|
80
|
-
// A notifier must never block Codex itself.
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
(async () => {
|
|
85
|
-
const args = process.argv.slice(2);
|
|
86
|
-
const argument = payloadArgument(args);
|
|
87
|
-
// Codex passes one JSON argument; legacy callers used stdin. Do not wait for
|
|
88
|
-
// an open stdin in argv mode, and keep the third-party transport unchanged.
|
|
89
|
-
const payload = argument === null ? await readStdin() : argument;
|
|
90
|
-
invoke(commandFrom(args, '--next-base64'), payload, process.env, argument === null ? 'stdin' : 'argv');
|
|
91
|
-
invoke(commandFrom(args, '--bridge-base64'), bridgePayload(payload), {
|
|
92
|
-
...process.env,
|
|
93
|
-
CODEX_SESSIONS_ROOT: process.env.CODEX_SESSIONS_ROOT || path.join(process.env.CODEX_HOME || path.join(os.homedir(), '.codex'), 'sessions'),
|
|
94
|
-
});
|
|
95
|
-
})();
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// Codex supports one notify command. This fail-open fan-out preserves an
|
|
5
|
+
// existing notifier and invokes the Bridge beside it.
|
|
6
|
+
const { spawn } = require('child_process');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
function commandFrom(args, marker) {
|
|
11
|
+
const index = args.indexOf(marker);
|
|
12
|
+
if (index < 0 || !args[index + 1]) return null;
|
|
13
|
+
try {
|
|
14
|
+
const command = JSON.parse(Buffer.from(args[index + 1], 'base64').toString('utf8'));
|
|
15
|
+
return Array.isArray(command) && command.length && command.every(part => typeof part === 'string') ? command : null;
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function readStdin() {
|
|
22
|
+
return new Promise(resolve => {
|
|
23
|
+
let value = '';
|
|
24
|
+
process.stdin.setEncoding('utf8');
|
|
25
|
+
process.stdin.on('data', chunk => { value += chunk; });
|
|
26
|
+
process.stdin.on('end', () => resolve(value));
|
|
27
|
+
process.stdin.on('error', () => resolve(value));
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function payloadArgument(args) {
|
|
32
|
+
const last = args.length - 1;
|
|
33
|
+
if (last < 0 || ['--next-base64', '--bridge-base64'].includes(args[last - 1])) return null;
|
|
34
|
+
try {
|
|
35
|
+
const payload = JSON.parse(args[last]);
|
|
36
|
+
return payload && typeof payload === 'object' && !Array.isArray(payload) ? args[last] : null;
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function bridgePayload(raw) {
|
|
43
|
+
try {
|
|
44
|
+
const payload = JSON.parse(raw);
|
|
45
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return raw;
|
|
46
|
+
let changed = false;
|
|
47
|
+
if (!payload.session_id && !payload.sessionId && typeof payload['thread-id'] === 'string') {
|
|
48
|
+
payload.session_id = payload['thread-id'];
|
|
49
|
+
changed = true;
|
|
50
|
+
}
|
|
51
|
+
if (!payload.turn_id && !payload.turnId && typeof payload['turn-id'] === 'string') {
|
|
52
|
+
payload.turn_id = payload['turn-id'];
|
|
53
|
+
changed = true;
|
|
54
|
+
}
|
|
55
|
+
// The notification only wakes the authoritative rollout reader. It must
|
|
56
|
+
// never substitute its summary fields for captured conversation evidence.
|
|
57
|
+
return changed ? JSON.stringify(payload) : raw;
|
|
58
|
+
} catch {
|
|
59
|
+
return raw;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function invoke(command, payload, env, transport = 'stdin') {
|
|
64
|
+
if (!command) return;
|
|
65
|
+
try {
|
|
66
|
+
const [executable, ...args] = command;
|
|
67
|
+
const child = spawn(executable, transport === 'argv' ? [...args, payload] : args, {
|
|
68
|
+
detached: true,
|
|
69
|
+
stdio: [transport === 'argv' ? 'ignore' : 'pipe', 'ignore', 'ignore'],
|
|
70
|
+
windowsHide: true,
|
|
71
|
+
env,
|
|
72
|
+
});
|
|
73
|
+
child.on('error', () => {});
|
|
74
|
+
if (child.stdin) {
|
|
75
|
+
child.stdin.on('error', () => {});
|
|
76
|
+
child.stdin.end(payload || '');
|
|
77
|
+
}
|
|
78
|
+
child.unref();
|
|
79
|
+
} catch {
|
|
80
|
+
// A notifier must never block Codex itself.
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
(async () => {
|
|
85
|
+
const args = process.argv.slice(2);
|
|
86
|
+
const argument = payloadArgument(args);
|
|
87
|
+
// Codex passes one JSON argument; legacy callers used stdin. Do not wait for
|
|
88
|
+
// an open stdin in argv mode, and keep the third-party transport unchanged.
|
|
89
|
+
const payload = argument === null ? await readStdin() : argument;
|
|
90
|
+
invoke(commandFrom(args, '--next-base64'), payload, process.env, argument === null ? 'stdin' : 'argv');
|
|
91
|
+
invoke(commandFrom(args, '--bridge-base64'), bridgePayload(payload), {
|
|
92
|
+
...process.env,
|
|
93
|
+
CODEX_SESSIONS_ROOT: process.env.CODEX_SESSIONS_ROOT || path.join(process.env.CODEX_HOME || path.join(os.homedir(), '.codex'), 'sessions'),
|
|
94
|
+
});
|
|
95
|
+
})();
|