@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 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 |
@@ -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') + '">' + (installed ? '已安装' : '未安装') + '</span>'
860
- + '<span class="st-chip ' + (a.hookInstalled ? 'ok' : 'warn') + '">' + (a.hookInstalled ? '已接入采集' : '未接入') + '</span>';
861
- var note = a.detail || (a.hookInstalled ? '' : installed ? '需要通过安装器 / consumer 注册 hook 后才会采集' : '未检测到该工具');
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.0",
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,
@@ -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) || mentionsShim(zcodeConfigText),
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 --path-format=absolute --git-common-dir`
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', '--path-format=absolute', '--git-common-dir'], cwd);
139
- if (rawCommonDir) commonDir = path.resolve(realpathBestEffort(rawCommonDir) || 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.join(os.homedir(), '.ai-coding-event-bridge');
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
- return claudeEntry.mainFailOpen({ home, payload, argv, env });
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
- const alreadyManaged = events[eventName].some(
73
- (group) => group && Array.isArray(group.hooks) && group.hooks.some((hook) => isManagedHook(hook, shimPath))
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
- const kept = groups.filter(
91
- (group) =>
92
- !(group && Array.isArray(group.hooks) && group.hooks.some((hook) => isManagedHook(hook, shimPath)))
93
- );
94
- if (kept.length !== groups.length) removed.push(eventName);
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');