@sanqianx/ai-coding-event-bridge-console 0.7.0 → 0.7.2
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/README.md +7 -0
- package/console/index.html +8 -3
- package/package.json +1 -1
- package/src/agent-detect.js +6 -6
- package/src/bridge/connectors/claude-code/hook-entry.js +4 -4
- package/src/bridge/core/normalizer.js +2 -2
- package/src/bridge/core/repo-context.js +6 -3
- package/src/bridge/core/runtime-home.js +4 -3
- package/src/bridge/installers/zcode/installer.js +22 -10
- package/src/server.js +1 -1
package/README.md
CHANGED
|
@@ -39,6 +39,13 @@ otherwise.
|
|
|
39
39
|
| --- | --- |
|
|
40
40
|
| `GET /api/health` | `{ ok, home, registeredProjects, first/lastSequence }` |
|
|
41
41
|
| `GET /api/system/agents` | Detect installed agent tools (Claude Code / Codex / OpenCode: CLI, config, hook wiring) + bridge runtime info + consumers |
|
|
42
|
+
|
|
43
|
+
Agent detection distinguishes a found command/config from installed capture.
|
|
44
|
+
Consumer registration never installs an Agent hook. In a host-embedded console,
|
|
45
|
+
“管理 Agent 采集” opens the host's connector installer. ZCode status requires both
|
|
46
|
+
managed events and `hooks.enabled`; hooks installed under an explicit Bridge
|
|
47
|
+
home read that home's runtime even when the external Agent inherits no home
|
|
48
|
+
environment variable. ZCode's Claude-compatible payloads retain source `zcode`.
|
|
42
49
|
| `POST /api/system/upgrade-runtime` | Materialize/activate the runtime at the core package version (designed same-major upgrade path) |
|
|
43
50
|
| `GET /api/system/pick-folder` | Native folder dialog (win32; 501 elsewhere) |
|
|
44
51
|
| `GET /api/projects` | Registered + auto-discovered projects with turn counts |
|
package/console/index.html
CHANGED
|
@@ -856,9 +856,9 @@ function renderSettings() {
|
|
|
856
856
|
var agentsHtml = payload.agents.map(function (a) {
|
|
857
857
|
var installed = a.cliFound || a.configFound;
|
|
858
858
|
var chips =
|
|
859
|
-
'<span class="st-chip ' + (installed ? 'ok' : 'off') + '">' + (
|
|
860
|
-
+ '<span class="st-chip ' + (a.hookInstalled ? 'ok' : 'warn') + '">' + (a.hookInstalled ? '
|
|
861
|
-
var note = a.detail || (a.hookInstalled ? '' : installed ? '
|
|
859
|
+
'<span class="st-chip ' + (installed ? 'ok' : 'off') + '">' + (a.cliFound ? '检测到命令' : a.configFound ? '检测到配置' : '未检测到') + '</span>'
|
|
860
|
+
+ '<span class="st-chip ' + (a.hookInstalled ? 'ok' : 'warn') + '">' + (a.hookInstalled ? '已配置采集' : '未接入') + '</span>';
|
|
861
|
+
var note = a.detail || (a.hookInstalled ? '采集配置已存在;实际记录请查看开发会话' : installed ? '需要安装采集 Hook;注册消费者不能代替 Hook' : '后台未发现命令或默认配置,请检查安装路径与运行用户');
|
|
862
862
|
return '<div class="agent-row"><span class="an">' + escapeText(a.label) + '</span>' + chips
|
|
863
863
|
+ '<span class="note">' + escapeText(note) + '</span></div>';
|
|
864
864
|
}).join('');
|
|
@@ -881,9 +881,13 @@ function renderSettings() {
|
|
|
881
881
|
|
|
882
882
|
body.innerHTML =
|
|
883
883
|
'<div class="set-section">AI 编程工具</div>' + agentsHtml
|
|
884
|
+
+ (embedded ? '<button class="btn-upg" id="capture-setup-btn">管理 Agent 采集</button>' : '')
|
|
884
885
|
+ '<div class="set-section">Bridge 运行时</div>' + runtimeHtml
|
|
885
886
|
+ '<div class="set-section">消费方</div>' + consumersHtml;
|
|
886
887
|
|
|
888
|
+
var captureSetup = document.getElementById('capture-setup-btn');
|
|
889
|
+
if (captureSetup) captureSetup.addEventListener('click', function () { settingsModal.classList.remove('open'); window.parent.postMessage({ type: 'kb:open-integrations', app: 'event-bridge' }, location.origin); });
|
|
890
|
+
|
|
887
891
|
var upg = document.getElementById('upgrade-btn');
|
|
888
892
|
if (upg) {
|
|
889
893
|
upg.addEventListener('click', function () {
|
|
@@ -926,6 +930,7 @@ window.addEventListener('message', async function (ev) {
|
|
|
926
930
|
if (d.type === 'kb:theme') { document.body.dataset.kbTheme = d.theme === 'dark' ? 'dark' : 'light'; return; }
|
|
927
931
|
if (d.type === 'kb:visibility') { embedVisible = d.visible === true; return; }
|
|
928
932
|
if (d.type === 'kb:refresh' && embedVisible && state.project) { fetchTurns(); refreshHist(); return; }
|
|
933
|
+
if (d.type === 'kb:refresh-agents') { if (settingsModal.classList.contains('open')) renderSettings(); return; }
|
|
929
934
|
if (d.type === 'kb:select-project') {
|
|
930
935
|
var requestEpoch = ++selectionEpoch;
|
|
931
936
|
var find = function () { return state.projects.find(function (p) { return p.id === d.project || p.path === d.project; }); };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanqianx/ai-coding-event-bridge-console",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "Durable AI coding event bridge, all in one: capture connectors and installers (Claude Code, Codex, OpenCode, ZCode), journal with commit boundaries and per-commit conversation sealing, headless query, and a local conversation explorer console.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|
package/src/agent-detect.js
CHANGED
|
@@ -44,7 +44,7 @@ function mentionsShim(text) {
|
|
|
44
44
|
return typeof text === 'string' && (text.includes('bridge-hook.cjs') || text.includes('--bridge-base64'));
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
async function detectAgents({ claudeSettingsFile, codexConfigFile, opencodePluginsDir, zcodeConfigFile } = {}) {
|
|
47
|
+
async function detectAgents({ homeDir, claudeSettingsFile, codexConfigFile, opencodePluginsDir, zcodeConfigFile } = {}) {
|
|
48
48
|
const home = os.homedir();
|
|
49
49
|
|
|
50
50
|
const claudeFiles = [
|
|
@@ -52,18 +52,19 @@ async function detectAgents({ claudeSettingsFile, codexConfigFile, opencodePlugi
|
|
|
52
52
|
path.join(path.dirname(claudeSettingsFile || path.join(home, '.claude', 'settings.json')), 'settings.local.json')
|
|
53
53
|
];
|
|
54
54
|
const claudeStatus = await core.installers.claudeCode.statusClaudeCodeHook({
|
|
55
|
+
homeDir,
|
|
55
56
|
settingsFile: claudeFiles[0]
|
|
56
57
|
});
|
|
57
58
|
const claudeConfigFound = claudeFiles.some(fileExists);
|
|
58
59
|
const claudeTextHooked = claudeFiles.some((file) => mentionsShim(readTextIfAny(file)));
|
|
59
60
|
|
|
60
61
|
const codexFile = codexConfigFile || path.join(home, '.codex', 'config.toml');
|
|
61
|
-
const codexStatus = await core.installers.codex.statusCodexNotify({ configFile: codexFile });
|
|
62
|
+
const codexStatus = await core.installers.codex.statusCodexNotify({ homeDir, configFile: codexFile });
|
|
62
63
|
const codexConfigFound = fileExists(codexFile);
|
|
63
64
|
const codexTextHooked = mentionsShim(readTextIfAny(codexFile));
|
|
64
65
|
|
|
65
66
|
const opencodeDir = opencodePluginsDir || path.join(home, '.config', 'opencode', 'plugin');
|
|
66
|
-
const opencodeStatus = await core.installers.openCode.statusOpenCodePlugin({ pluginsDir: opencodeDir });
|
|
67
|
+
const opencodeStatus = await core.installers.openCode.statusOpenCodePlugin({ homeDir, pluginsDir: opencodeDir });
|
|
67
68
|
let opencodePluginHooked = Boolean(opencodeStatus.installed);
|
|
68
69
|
if (!opencodePluginHooked) {
|
|
69
70
|
try {
|
|
@@ -79,8 +80,7 @@ async function detectAgents({ claudeSettingsFile, codexConfigFile, opencodePlugi
|
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
const zcodeFile = zcodeConfigFile || path.join(home, '.zcode', 'cli', 'config.json');
|
|
82
|
-
const zcodeStatus = await core.installers.zcode.statusZcodeHook({ configFile: zcodeFile });
|
|
83
|
-
const zcodeConfigText = readTextIfAny(zcodeFile);
|
|
83
|
+
const zcodeStatus = await core.installers.zcode.statusZcodeHook({ homeDir, configFile: zcodeFile });
|
|
84
84
|
|
|
85
85
|
return [
|
|
86
86
|
{
|
|
@@ -118,7 +118,7 @@ async function detectAgents({ claudeSettingsFile, codexConfigFile, opencodePlugi
|
|
|
118
118
|
label: 'ZCode',
|
|
119
119
|
cliFound: commandExists('zcode'),
|
|
120
120
|
configFound: fileExists(zcodeFile),
|
|
121
|
-
hookInstalled: Boolean(zcodeStatus.installed)
|
|
121
|
+
hookInstalled: Boolean(zcodeStatus.installed),
|
|
122
122
|
managed: Boolean(zcodeStatus.installed),
|
|
123
123
|
detail: zcodeStatus.thirdPartyCount ? `配置中另有 ${zcodeStatus.thirdPartyCount} 条第三方 hook(保留不动)` : null
|
|
124
124
|
}
|
|
@@ -13,14 +13,14 @@ const { notifyConsumers } = require('./notify');
|
|
|
13
13
|
* Internal host SDK sessions (AI_CODING_EVENT_BRIDGE_CAPTURE=0) are ignored
|
|
14
14
|
* before any Git/journal work happens.
|
|
15
15
|
*/
|
|
16
|
-
async function main({ home, payload }) {
|
|
16
|
+
async function main({ home, payload, source }) {
|
|
17
17
|
if (isCaptureDisabled(process.env, payload)) {
|
|
18
18
|
return captureDisabledResult();
|
|
19
19
|
}
|
|
20
20
|
if (!payload || typeof payload !== 'object') {
|
|
21
21
|
return { status: 'ignored' };
|
|
22
22
|
}
|
|
23
|
-
const event = normalizeClaudeCode(payload);
|
|
23
|
+
const event = normalizeClaudeCode(payload, source);
|
|
24
24
|
if (!event) {
|
|
25
25
|
return { status: 'ignored' };
|
|
26
26
|
}
|
|
@@ -38,8 +38,8 @@ async function main({ home, payload }) {
|
|
|
38
38
|
return { status: 'captured', sequence: appended.sequence, turnId: appended.turnId };
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
function mainFailOpen({ home, payload }) {
|
|
42
|
-
return main({ home, payload }).catch(() => ({ status: 'fail-open' }));
|
|
41
|
+
function mainFailOpen({ home, payload, source }) {
|
|
42
|
+
return main({ home, payload, source }).catch(() => ({ status: 'fail-open' }));
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
module.exports = { main, mainFailOpen };
|
|
@@ -22,9 +22,9 @@ function hookEventName(raw) {
|
|
|
22
22
|
return raw.hook_event_name || raw.hookName || raw.hook_name || null;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
function normalizeClaudeCode(raw) {
|
|
25
|
+
function normalizeClaudeCode(raw, source = 'claude-code') {
|
|
26
26
|
if (!raw || typeof raw !== 'object') return null;
|
|
27
|
-
const base = { source: 'claude-code' };
|
|
27
|
+
const base = { source: source === 'zcode' ? 'zcode' : 'claude-code' };
|
|
28
28
|
if (hookEventName(raw) === 'UserPromptSubmit' || raw.eventType === 'user_prompt') {
|
|
29
29
|
const content = typeof raw.prompt === 'string' ? raw.prompt : null;
|
|
30
30
|
const event = {
|
|
@@ -110,7 +110,7 @@ function buildRepoIdentityV1({ workspaceRoot, commonDir = null, remote = null })
|
|
|
110
110
|
* 1. `git rev-parse --show-toplevel` (authoritative Git working tree root)
|
|
111
111
|
* 2. realpath + canonical separators for the display root
|
|
112
112
|
* 3. workspaceId = sha256 over the comparison form (case-insensitive on win32)
|
|
113
|
-
* 4. commonDir from `git rev-parse --
|
|
113
|
+
* 4. commonDir from `git rev-parse --git-common-dir`, resolved against cwd
|
|
114
114
|
* (metadata: worktrees share it, but their workspaceIds differ)
|
|
115
115
|
* 5. normalized origin URL as metadata only — never the primary identity
|
|
116
116
|
*
|
|
@@ -135,8 +135,11 @@ async function resolveRepoContext(cwd, { runGit = runGitDefault } = {}) {
|
|
|
135
135
|
|
|
136
136
|
let commonDir = null;
|
|
137
137
|
try {
|
|
138
|
-
const rawCommonDir = await runGit(['rev-parse', '--
|
|
139
|
-
if (rawCommonDir
|
|
138
|
+
const rawCommonDir = String(await runGit(['rev-parse', '--git-common-dir'], cwd)).trim();
|
|
139
|
+
if (rawCommonDir && !/[\r\n\0]/.test(rawCommonDir) && !rawCommonDir.startsWith('--')) {
|
|
140
|
+
const absoluteCommonDir = path.resolve(cwd, rawCommonDir);
|
|
141
|
+
commonDir = realpathBestEffort(absoluteCommonDir) || absoluteCommonDir;
|
|
142
|
+
}
|
|
140
143
|
} catch (_) {
|
|
141
144
|
commonDir = null;
|
|
142
145
|
}
|
|
@@ -15,10 +15,9 @@ const SHIM_SOURCE = `#!/usr/bin/env node
|
|
|
15
15
|
'use strict';
|
|
16
16
|
const fs = require('fs');
|
|
17
17
|
const path = require('path');
|
|
18
|
-
const os = require('os');
|
|
19
18
|
|
|
20
19
|
function home() {
|
|
21
|
-
return process.env.AI_CODING_EVENT_BRIDGE_HOME || path.
|
|
20
|
+
return process.env.AI_CODING_EVENT_BRIDGE_HOME || path.resolve(__dirname, '..');
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
function readStdin() {
|
|
@@ -76,7 +75,9 @@ async function main({ payload, home, argv, env }) {
|
|
|
76
75
|
// connector — never fall through to Codex just because session_id exists.
|
|
77
76
|
if (payload.hookName || payload.hook_event_name || payload.hook_name) {
|
|
78
77
|
const claudeEntry = require('./bridge/connectors/claude-code/hook-entry.js');
|
|
79
|
-
|
|
78
|
+
const sourceIndex = Array.isArray(argv) ? argv.indexOf('--source') : -1;
|
|
79
|
+
const source = sourceIndex >= 0 && argv[sourceIndex + 1] === 'zcode' ? 'zcode' : 'claude-code';
|
|
80
|
+
return claudeEntry.mainFailOpen({ home, payload, source, argv, env });
|
|
80
81
|
}
|
|
81
82
|
if (payload.type === 'user' || payload.type === 'assistant') {
|
|
82
83
|
const opencodeEntry = require('./bridge/connectors/opencode/hook-entry.js');
|
|
@@ -27,7 +27,7 @@ function shimPathFor(homeDir) {
|
|
|
27
27
|
// process hooks (argument vector, no shell) are the portable form; bare
|
|
28
28
|
// `node` survives Node upgrades, unlike a baked-in absolute executable path.
|
|
29
29
|
function managedHook(shimPath) {
|
|
30
|
-
return { type: 'process', command: 'node', args: [shimPath], timeoutMs: 15000 };
|
|
30
|
+
return { type: 'process', command: 'node', args: [shimPath, '--source', 'zcode'], timeoutMs: 15000 };
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
function isManagedHook(hook, shimPath) {
|
|
@@ -69,9 +69,15 @@ function upsertManagedEntries(config, shimPath) {
|
|
|
69
69
|
if (!Array.isArray(events[eventName])) {
|
|
70
70
|
events[eventName] = [];
|
|
71
71
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
let alreadyManaged = false;
|
|
73
|
+
for (const group of events[eventName]) {
|
|
74
|
+
if (!group || !Array.isArray(group.hooks)) continue;
|
|
75
|
+
group.hooks = group.hooks.map(hook => {
|
|
76
|
+
if (!isManagedHook(hook, shimPath)) return hook;
|
|
77
|
+
alreadyManaged = true;
|
|
78
|
+
return managedHook(shimPath);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
75
81
|
if (alreadyManaged) continue;
|
|
76
82
|
events[eventName].push({ hooks: [managedHook(shimPath)] });
|
|
77
83
|
changed.push(eventName);
|
|
@@ -87,11 +93,15 @@ function removeManagedEntries(config, shimPath) {
|
|
|
87
93
|
for (const eventName of Object.keys(config.hooks.events)) {
|
|
88
94
|
const groups = config.hooks.events[eventName];
|
|
89
95
|
if (!Array.isArray(groups)) continue;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
96
|
+
let changed = false;
|
|
97
|
+
const kept = groups.map(group => {
|
|
98
|
+
if (!group || !Array.isArray(group.hooks)) return group;
|
|
99
|
+
const hooks = group.hooks.filter(hook => !isManagedHook(hook, shimPath));
|
|
100
|
+
if (hooks.length === group.hooks.length) return group;
|
|
101
|
+
changed = true;
|
|
102
|
+
return hooks.length ? { ...group, hooks } : null;
|
|
103
|
+
}).filter(group => group !== null);
|
|
104
|
+
if (changed) removed.push(eventName);
|
|
95
105
|
if (kept.length === 0) {
|
|
96
106
|
delete config.hooks.events[eventName];
|
|
97
107
|
} else {
|
|
@@ -107,12 +117,14 @@ async function installZcodeHook({ homeDir, consumerName, consumerMeta = {}, regi
|
|
|
107
117
|
const home = homeDir || path.join(os.homedir(), '.ai-coding-event-bridge');
|
|
108
118
|
const target = configFile || path.join(os.homedir(), '.zcode', 'cli', 'config.json');
|
|
109
119
|
const runtime = await ensureRuntimeHome({ homeDir: home, version });
|
|
120
|
+
if (runtime.conflict) throw new InstallerError('Bridge 运行版本冲突,未修改 ZCode 配置。');
|
|
110
121
|
const registry = new ConsumerRegistry(home);
|
|
111
122
|
if (consumerName && registerConsumer) {
|
|
112
123
|
await registry.registerConsumer(consumerName, consumerMeta);
|
|
113
124
|
}
|
|
114
125
|
const config = readConfig(target);
|
|
115
126
|
const added = upsertManagedEntries(config, shimPathFor(home));
|
|
127
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
116
128
|
writeJsonAtomicSync(target, config);
|
|
117
129
|
return { added, configFile: target, runtime, consumers: (await registry.getConsumers()).map((c) => c.name) };
|
|
118
130
|
}
|
|
@@ -161,7 +173,7 @@ async function uninstallZcodeHook({ homeDir, consumerName, unregisterConsumer =
|
|
|
161
173
|
const registry = new ConsumerRegistry(home);
|
|
162
174
|
const config = readConfig(target);
|
|
163
175
|
const removed = config.hooks && config.hooks.events ? removeManagedEntries(config, shimPathFor(home)) : [];
|
|
164
|
-
writeJsonAtomicSync(target, config);
|
|
176
|
+
if (removed.length) writeJsonAtomicSync(target, config);
|
|
165
177
|
let consumerUnregistered = false;
|
|
166
178
|
if (consumerName && unregisterConsumer) {
|
|
167
179
|
await registry.unregisterConsumer(consumerName);
|
package/src/server.js
CHANGED
|
@@ -536,7 +536,7 @@ function createConsoleServer({ home, runtimeToken }) {
|
|
|
536
536
|
}
|
|
537
537
|
|
|
538
538
|
if (route === 'GET /api/system/agents') {
|
|
539
|
-
const agents = await detectAgents();
|
|
539
|
+
const agents = await detectAgents({ homeDir: home });
|
|
540
540
|
let activeRuntimeVersion = null;
|
|
541
541
|
let runtimeVersions = [];
|
|
542
542
|
const runtimeRoot = path.join(home, 'runtime');
|