@sanqianx/ai-coding-event-bridge-console 0.7.2 → 0.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +81 -81
  3. package/console/index.html +953 -953
  4. package/package.json +26 -26
  5. package/src/agent-detect.js +128 -128
  6. package/src/bridge/bin/commit-boundary.js +64 -64
  7. package/src/bridge/connectors/capture-guard.js +24 -24
  8. package/src/bridge/connectors/claude-code/hook-entry.js +45 -45
  9. package/src/bridge/connectors/claude-code/notify.js +57 -57
  10. package/src/bridge/connectors/codex/cursor-store.js +83 -83
  11. package/src/bridge/connectors/codex/hook-entry.js +215 -215
  12. package/src/bridge/connectors/codex/session-parser.js +100 -100
  13. package/src/bridge/connectors/opencode/hook-entry.js +55 -55
  14. package/src/bridge/core/bridge.js +249 -246
  15. package/src/bridge/core/commit-projection.js +288 -288
  16. package/src/bridge/core/compaction.js +23 -23
  17. package/src/bridge/core/consumer-registry.js +111 -111
  18. package/src/bridge/core/event-schema.js +127 -127
  19. package/src/bridge/core/fs-utils.js +81 -81
  20. package/src/bridge/core/journal-router.js +39 -39
  21. package/src/bridge/core/journal.js +507 -507
  22. package/src/bridge/core/lock.js +96 -96
  23. package/src/bridge/core/normalizer.js +114 -114
  24. package/src/bridge/core/paths.js +12 -12
  25. package/src/bridge/core/project-registry.js +176 -176
  26. package/src/bridge/core/repo-context.js +191 -191
  27. package/src/bridge/core/runtime-home.js +189 -189
  28. package/src/bridge/core/semver.js +23 -23
  29. package/src/bridge/core/turn-identity.js +127 -127
  30. package/src/bridge/index.js +86 -86
  31. package/src/bridge/installers/claude-code/installer.js +181 -181
  32. package/src/bridge/installers/codex/installer.js +114 -114
  33. package/src/bridge/installers/opencode/installer.js +96 -96
  34. package/src/bridge/installers/zcode/installer.js +200 -200
  35. package/src/bridge/query/conversation-query.js +194 -194
  36. package/src/server.js +825 -825
@@ -1,96 +1,96 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const os = require('os');
5
- const path = require('path');
6
- const { ensureRuntimeHome } = require('../../core/runtime-home');
7
- const { ConsumerRegistry } = require('../../core/consumer-registry');
8
-
9
- const PLUGIN_FILE_NAME = 'ai-coding-event-bridge.js';
10
-
11
- function shimPathFor(homeDir) {
12
- return path.join(homeDir, 'bin', 'bridge-hook.cjs');
13
- }
14
-
15
- function pluginSource(shimPath) {
16
- return `// Installed by @sanqianx/ai-coding-event-bridge. Captures user/assistant
17
- // lifecycle events into the local durable Bridge journal via the stable shim.
18
- // No HTTP endpoint is required; removing the file removes the integration.
19
- const { spawn } = require('child_process');
20
- module.exports = {
21
- id: 'ai-coding-event-bridge',
22
- init() {
23
- return {
24
- event(event) {
25
- if (!event || (event.type !== 'user' && event.type !== 'assistant')) return;
26
- const payload = {
27
- type: event.type,
28
- sessionId: event.sessionID || event.sessionId || null,
29
- turnId: event.turnID || event.turnId || null,
30
- cwd: event.cwd || process.cwd(),
31
- text: typeof event.text === 'string' ? event.text : null
32
- };
33
- const child = spawn('node', [${JSON.stringify(shimPath)}], { stdio: ['pipe', 'ignore', 'ignore'] });
34
- child.on('error', () => {});
35
- child.stdin.write(JSON.stringify(payload));
36
- child.stdin.end();
37
- }
38
- };
39
- }
40
- };
41
- `;
42
- }
43
-
44
- // Registry mutations are host-owned; registerConsumer/unregisterConsumer are
45
- // explicit opt-ins. Uninstall removes only the managed plugin file and never
46
- // touches third-party plugins or the shared runtime home.
47
- async function installOpenCodePlugin({ homeDir, consumerName, consumerMeta = {}, registerConsumer = false, pluginsDir, version } = {}) {
48
- const home = homeDir || path.join(os.homedir(), '.ai-coding-event-bridge');
49
- const target = pluginsDir || path.join(os.homedir(), '.config', 'opencode', 'plugin');
50
- const runtime = await ensureRuntimeHome({ homeDir: home, version });
51
- const registry = new ConsumerRegistry(home);
52
- if (consumerName && registerConsumer) await registry.registerConsumer(consumerName, consumerMeta);
53
- fs.mkdirSync(target, { recursive: true });
54
- const file = path.join(target, PLUGIN_FILE_NAME);
55
- fs.writeFileSync(file, pluginSource(shimPathFor(home)));
56
- return { installed: true, pluginFile: file, runtime };
57
- }
58
-
59
- async function statusOpenCodePlugin({ homeDir, pluginsDir } = {}) {
60
- const home = homeDir || path.join(os.homedir(), '.ai-coding-event-bridge');
61
- const target = pluginsDir || path.join(os.homedir(), '.config', 'opencode', 'plugin');
62
- const file = path.join(target, PLUGIN_FILE_NAME);
63
- const exists = fs.existsSync(file);
64
- let managed = false;
65
- let thirdPartyFiles = 0;
66
- if (fs.existsSync(target)) {
67
- for (const entry of fs.readdirSync(target)) {
68
- if (entry === PLUGIN_FILE_NAME) continue;
69
- if (entry.endsWith('.js') || entry.endsWith('.ts')) thirdPartyFiles++;
70
- }
71
- }
72
- if (exists) {
73
- managed = fs.readFileSync(file, 'utf8').includes('ai-coding-event-bridge');
74
- }
75
- return { installed: managed && exists, pluginFile: file, thirdPartyFiles };
76
- }
77
-
78
- async function uninstallOpenCodePlugin({ homeDir, consumerName, unregisterConsumer = false, pluginsDir } = {}) {
79
- const home = homeDir || path.join(os.homedir(), '.ai-coding-event-bridge');
80
- const target = pluginsDir || path.join(os.homedir(), '.config', 'opencode', 'plugin');
81
- const registry = new ConsumerRegistry(home);
82
- const file = path.join(target, PLUGIN_FILE_NAME);
83
- try {
84
- fs.rmSync(file, { force: true });
85
- } catch (_) {
86
- // Already gone.
87
- }
88
- let consumerUnregistered = false;
89
- if (consumerName && unregisterConsumer) {
90
- await registry.unregisterConsumer(consumerName);
91
- consumerUnregistered = true;
92
- }
93
- return { removed: true, consumerUnregistered, consumers: (await registry.getConsumers()).map(c => c.name) };
94
- }
95
-
96
- module.exports = { installOpenCodePlugin, statusOpenCodePlugin, uninstallOpenCodePlugin, PLUGIN_FILE_NAME };
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const { ensureRuntimeHome } = require('../../core/runtime-home');
7
+ const { ConsumerRegistry } = require('../../core/consumer-registry');
8
+
9
+ const PLUGIN_FILE_NAME = 'ai-coding-event-bridge.js';
10
+
11
+ function shimPathFor(homeDir) {
12
+ return path.join(homeDir, 'bin', 'bridge-hook.cjs');
13
+ }
14
+
15
+ function pluginSource(shimPath) {
16
+ return `// Installed by @sanqianx/ai-coding-event-bridge. Captures user/assistant
17
+ // lifecycle events into the local durable Bridge journal via the stable shim.
18
+ // No HTTP endpoint is required; removing the file removes the integration.
19
+ const { spawn } = require('child_process');
20
+ module.exports = {
21
+ id: 'ai-coding-event-bridge',
22
+ init() {
23
+ return {
24
+ event(event) {
25
+ if (!event || (event.type !== 'user' && event.type !== 'assistant')) return;
26
+ const payload = {
27
+ type: event.type,
28
+ sessionId: event.sessionID || event.sessionId || null,
29
+ turnId: event.turnID || event.turnId || null,
30
+ cwd: event.cwd || process.cwd(),
31
+ text: typeof event.text === 'string' ? event.text : null
32
+ };
33
+ const child = spawn('node', [${JSON.stringify(shimPath)}], { stdio: ['pipe', 'ignore', 'ignore'] });
34
+ child.on('error', () => {});
35
+ child.stdin.write(JSON.stringify(payload));
36
+ child.stdin.end();
37
+ }
38
+ };
39
+ }
40
+ };
41
+ `;
42
+ }
43
+
44
+ // Registry mutations are host-owned; registerConsumer/unregisterConsumer are
45
+ // explicit opt-ins. Uninstall removes only the managed plugin file and never
46
+ // touches third-party plugins or the shared runtime home.
47
+ async function installOpenCodePlugin({ homeDir, consumerName, consumerMeta = {}, registerConsumer = false, pluginsDir, version } = {}) {
48
+ const home = homeDir || path.join(os.homedir(), '.ai-coding-event-bridge');
49
+ const target = pluginsDir || path.join(os.homedir(), '.config', 'opencode', 'plugin');
50
+ const runtime = await ensureRuntimeHome({ homeDir: home, version });
51
+ const registry = new ConsumerRegistry(home);
52
+ if (consumerName && registerConsumer) await registry.registerConsumer(consumerName, consumerMeta);
53
+ fs.mkdirSync(target, { recursive: true });
54
+ const file = path.join(target, PLUGIN_FILE_NAME);
55
+ fs.writeFileSync(file, pluginSource(shimPathFor(home)));
56
+ return { installed: true, pluginFile: file, runtime };
57
+ }
58
+
59
+ async function statusOpenCodePlugin({ homeDir, pluginsDir } = {}) {
60
+ const home = homeDir || path.join(os.homedir(), '.ai-coding-event-bridge');
61
+ const target = pluginsDir || path.join(os.homedir(), '.config', 'opencode', 'plugin');
62
+ const file = path.join(target, PLUGIN_FILE_NAME);
63
+ const exists = fs.existsSync(file);
64
+ let managed = false;
65
+ let thirdPartyFiles = 0;
66
+ if (fs.existsSync(target)) {
67
+ for (const entry of fs.readdirSync(target)) {
68
+ if (entry === PLUGIN_FILE_NAME) continue;
69
+ if (entry.endsWith('.js') || entry.endsWith('.ts')) thirdPartyFiles++;
70
+ }
71
+ }
72
+ if (exists) {
73
+ managed = fs.readFileSync(file, 'utf8').includes('ai-coding-event-bridge');
74
+ }
75
+ return { installed: managed && exists, pluginFile: file, thirdPartyFiles };
76
+ }
77
+
78
+ async function uninstallOpenCodePlugin({ homeDir, consumerName, unregisterConsumer = false, pluginsDir } = {}) {
79
+ const home = homeDir || path.join(os.homedir(), '.ai-coding-event-bridge');
80
+ const target = pluginsDir || path.join(os.homedir(), '.config', 'opencode', 'plugin');
81
+ const registry = new ConsumerRegistry(home);
82
+ const file = path.join(target, PLUGIN_FILE_NAME);
83
+ try {
84
+ fs.rmSync(file, { force: true });
85
+ } catch (_) {
86
+ // Already gone.
87
+ }
88
+ let consumerUnregistered = false;
89
+ if (consumerName && unregisterConsumer) {
90
+ await registry.unregisterConsumer(consumerName);
91
+ consumerUnregistered = true;
92
+ }
93
+ return { removed: true, consumerUnregistered, consumers: (await registry.getConsumers()).map(c => c.name) };
94
+ }
95
+
96
+ module.exports = { installOpenCodePlugin, statusOpenCodePlugin, uninstallOpenCodePlugin, PLUGIN_FILE_NAME };
@@ -1,200 +1,200 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const os = require('os');
5
- const path = require('path');
6
- const { ensureRuntimeHome } = require('../../core/runtime-home');
7
- const { ConsumerRegistry } = require('../../core/consumer-registry');
8
- const { readJsonIfExists, writeJsonAtomicSync } = require('../../core/fs-utils');
9
-
10
- const HOOK_EVENTS = ['UserPromptSubmit', 'Stop'];
11
-
12
- // ZCode configuration-file hooks are disabled by default: the runner only
13
- // exists when hooks.enabled is true (or a plugin contributes hooks, which we
14
- // must not rely on). The managed entries therefore force it on; uninstall
15
- // leaves the flag alone because third-party config hooks may depend on it.
16
- class InstallerError extends Error {
17
- constructor(message) {
18
- super(message);
19
- this.name = 'InstallerError';
20
- }
21
- }
22
-
23
- function shimPathFor(homeDir) {
24
- return path.join(homeDir, 'bin', 'bridge-hook.cjs');
25
- }
26
-
27
- // process hooks (argument vector, no shell) are the portable form; bare
28
- // `node` survives Node upgrades, unlike a baked-in absolute executable path.
29
- function managedHook(shimPath) {
30
- return { type: 'process', command: 'node', args: [shimPath, '--source', 'zcode'], timeoutMs: 15000 };
31
- }
32
-
33
- function isManagedHook(hook, shimPath) {
34
- if (!hook || typeof hook !== 'object') return false;
35
- if (hook.type === 'process') {
36
- return Array.isArray(hook.args) && hook.args.some((arg) => typeof arg === 'string' && arg.includes(shimPath));
37
- }
38
- return typeof hook.command === 'string' && hook.command.includes(shimPath);
39
- }
40
-
41
- function readConfig(configFile) {
42
- const config = readJsonIfExists(configFile);
43
- if (config === null) {
44
- if (fs.existsSync(configFile)) {
45
- throw new InstallerError(`config file is not valid JSON: ${configFile}`);
46
- }
47
- return {};
48
- }
49
- if (typeof config !== 'object' || config === null || Array.isArray(config)) {
50
- throw new InstallerError(`config file must contain a JSON object: ${configFile}`);
51
- }
52
- return config;
53
- }
54
-
55
- function eventsOf(config) {
56
- if (!config.hooks || typeof config.hooks !== 'object' || Array.isArray(config.hooks)) {
57
- config.hooks = {};
58
- }
59
- if (!config.hooks.events || typeof config.hooks.events !== 'object' || Array.isArray(config.hooks.events)) {
60
- config.hooks.events = {};
61
- }
62
- return config.hooks.events;
63
- }
64
-
65
- function upsertManagedEntries(config, shimPath) {
66
- const events = eventsOf(config);
67
- const changed = [];
68
- for (const eventName of HOOK_EVENTS) {
69
- if (!Array.isArray(events[eventName])) {
70
- events[eventName] = [];
71
- }
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
- }
81
- if (alreadyManaged) continue;
82
- events[eventName].push({ hooks: [managedHook(shimPath)] });
83
- changed.push(eventName);
84
- }
85
- if (config.hooks.enabled !== true) {
86
- config.hooks.enabled = true;
87
- }
88
- return changed;
89
- }
90
-
91
- function removeManagedEntries(config, shimPath) {
92
- const removed = [];
93
- for (const eventName of Object.keys(config.hooks.events)) {
94
- const groups = config.hooks.events[eventName];
95
- if (!Array.isArray(groups)) continue;
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);
105
- if (kept.length === 0) {
106
- delete config.hooks.events[eventName];
107
- } else {
108
- config.hooks.events[eventName] = kept;
109
- }
110
- }
111
- return removed;
112
- }
113
-
114
- // Host-level consumer registration is owned by the host; the installer mutates
115
- // the registry only on explicit opt-in (same contract as the other connectors).
116
- async function installZcodeHook({ homeDir, consumerName, consumerMeta = {}, registerConsumer = false, configFile, version } = {}) {
117
- const home = homeDir || path.join(os.homedir(), '.ai-coding-event-bridge');
118
- const target = configFile || path.join(os.homedir(), '.zcode', 'cli', 'config.json');
119
- const runtime = await ensureRuntimeHome({ homeDir: home, version });
120
- if (runtime.conflict) throw new InstallerError('Bridge 运行版本冲突,未修改 ZCode 配置。');
121
- const registry = new ConsumerRegistry(home);
122
- if (consumerName && registerConsumer) {
123
- await registry.registerConsumer(consumerName, consumerMeta);
124
- }
125
- const config = readConfig(target);
126
- const added = upsertManagedEntries(config, shimPathFor(home));
127
- fs.mkdirSync(path.dirname(target), { recursive: true });
128
- writeJsonAtomicSync(target, config);
129
- return { added, configFile: target, runtime, consumers: (await registry.getConsumers()).map((c) => c.name) };
130
- }
131
-
132
- async function statusZcodeHook({ homeDir, configFile } = {}) {
133
- const home = homeDir || path.join(os.homedir(), '.ai-coding-event-bridge');
134
- const target = configFile || path.join(os.homedir(), '.zcode', 'cli', 'config.json');
135
- const config = readConfig(target);
136
- const events = config.hooks && typeof config.hooks === 'object' ? config.hooks.events || {} : {};
137
- const shimPath = shimPathFor(home);
138
- const managedEvents = [];
139
- let thirdPartyCount = 0;
140
- for (const eventName of Object.keys(events)) {
141
- const groups = events[eventName];
142
- if (!Array.isArray(groups)) continue;
143
- let hasManaged = false;
144
- for (const group of groups) {
145
- for (const hook of (group && group.hooks) || []) {
146
- if (isManagedHook(hook, shimPath)) {
147
- hasManaged = true;
148
- } else {
149
- thirdPartyCount++;
150
- }
151
- }
152
- }
153
- if (hasManaged) managedEvents.push(eventName);
154
- }
155
- const registry = new ConsumerRegistry(home);
156
- return {
157
- installed:
158
- HOOK_EVENTS.every((eventName) => managedEvents.includes(eventName)) &&
159
- Boolean(config.hooks && config.hooks.enabled),
160
- managedEvents,
161
- thirdPartyCount,
162
- consumers: (await registry.getConsumers()).map((c) => c.name)
163
- };
164
- }
165
-
166
- async function repairZcodeHook({ homeDir, consumerName, consumerMeta = {}, registerConsumer = false, configFile, version } = {}) {
167
- return installZcodeHook({ homeDir, consumerName, consumerMeta, registerConsumer, configFile, version });
168
- }
169
-
170
- async function uninstallZcodeHook({ homeDir, consumerName, unregisterConsumer = false, configFile } = {}) {
171
- const home = homeDir || path.join(os.homedir(), '.ai-coding-event-bridge');
172
- const target = configFile || path.join(os.homedir(), '.zcode', 'cli', 'config.json');
173
- const registry = new ConsumerRegistry(home);
174
- const config = readConfig(target);
175
- const removed = config.hooks && config.hooks.events ? removeManagedEntries(config, shimPathFor(home)) : [];
176
- if (removed.length) writeJsonAtomicSync(target, config);
177
- let consumerUnregistered = false;
178
- if (consumerName && unregisterConsumer) {
179
- await registry.unregisterConsumer(consumerName);
180
- consumerUnregistered = true;
181
- }
182
- return {
183
- removed: true,
184
- removedEvents: removed,
185
- configFile: target,
186
- consumerUnregistered,
187
- consumers: (await registry.getConsumers()).map((c) => c.name)
188
- };
189
- }
190
-
191
- module.exports = {
192
- HOOK_EVENTS,
193
- InstallerError,
194
- installZcodeHook,
195
- statusZcodeHook,
196
- repairZcodeHook,
197
- uninstallZcodeHook,
198
- managedHook,
199
- isManagedHook
200
- };
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const { ensureRuntimeHome } = require('../../core/runtime-home');
7
+ const { ConsumerRegistry } = require('../../core/consumer-registry');
8
+ const { readJsonIfExists, writeJsonAtomicSync } = require('../../core/fs-utils');
9
+
10
+ const HOOK_EVENTS = ['UserPromptSubmit', 'Stop'];
11
+
12
+ // ZCode configuration-file hooks are disabled by default: the runner only
13
+ // exists when hooks.enabled is true (or a plugin contributes hooks, which we
14
+ // must not rely on). The managed entries therefore force it on; uninstall
15
+ // leaves the flag alone because third-party config hooks may depend on it.
16
+ class InstallerError extends Error {
17
+ constructor(message) {
18
+ super(message);
19
+ this.name = 'InstallerError';
20
+ }
21
+ }
22
+
23
+ function shimPathFor(homeDir) {
24
+ return path.join(homeDir, 'bin', 'bridge-hook.cjs');
25
+ }
26
+
27
+ // process hooks (argument vector, no shell) are the portable form; bare
28
+ // `node` survives Node upgrades, unlike a baked-in absolute executable path.
29
+ function managedHook(shimPath) {
30
+ return { type: 'process', command: 'node', args: [shimPath, '--source', 'zcode'], timeoutMs: 15000 };
31
+ }
32
+
33
+ function isManagedHook(hook, shimPath) {
34
+ if (!hook || typeof hook !== 'object') return false;
35
+ if (hook.type === 'process') {
36
+ return Array.isArray(hook.args) && hook.args.some((arg) => typeof arg === 'string' && arg.includes(shimPath));
37
+ }
38
+ return typeof hook.command === 'string' && hook.command.includes(shimPath);
39
+ }
40
+
41
+ function readConfig(configFile) {
42
+ const config = readJsonIfExists(configFile);
43
+ if (config === null) {
44
+ if (fs.existsSync(configFile)) {
45
+ throw new InstallerError(`config file is not valid JSON: ${configFile}`);
46
+ }
47
+ return {};
48
+ }
49
+ if (typeof config !== 'object' || config === null || Array.isArray(config)) {
50
+ throw new InstallerError(`config file must contain a JSON object: ${configFile}`);
51
+ }
52
+ return config;
53
+ }
54
+
55
+ function eventsOf(config) {
56
+ if (!config.hooks || typeof config.hooks !== 'object' || Array.isArray(config.hooks)) {
57
+ config.hooks = {};
58
+ }
59
+ if (!config.hooks.events || typeof config.hooks.events !== 'object' || Array.isArray(config.hooks.events)) {
60
+ config.hooks.events = {};
61
+ }
62
+ return config.hooks.events;
63
+ }
64
+
65
+ function upsertManagedEntries(config, shimPath) {
66
+ const events = eventsOf(config);
67
+ const changed = [];
68
+ for (const eventName of HOOK_EVENTS) {
69
+ if (!Array.isArray(events[eventName])) {
70
+ events[eventName] = [];
71
+ }
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
+ }
81
+ if (alreadyManaged) continue;
82
+ events[eventName].push({ hooks: [managedHook(shimPath)] });
83
+ changed.push(eventName);
84
+ }
85
+ if (config.hooks.enabled !== true) {
86
+ config.hooks.enabled = true;
87
+ }
88
+ return changed;
89
+ }
90
+
91
+ function removeManagedEntries(config, shimPath) {
92
+ const removed = [];
93
+ for (const eventName of Object.keys(config.hooks.events)) {
94
+ const groups = config.hooks.events[eventName];
95
+ if (!Array.isArray(groups)) continue;
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);
105
+ if (kept.length === 0) {
106
+ delete config.hooks.events[eventName];
107
+ } else {
108
+ config.hooks.events[eventName] = kept;
109
+ }
110
+ }
111
+ return removed;
112
+ }
113
+
114
+ // Host-level consumer registration is owned by the host; the installer mutates
115
+ // the registry only on explicit opt-in (same contract as the other connectors).
116
+ async function installZcodeHook({ homeDir, consumerName, consumerMeta = {}, registerConsumer = false, configFile, version } = {}) {
117
+ const home = homeDir || path.join(os.homedir(), '.ai-coding-event-bridge');
118
+ const target = configFile || path.join(os.homedir(), '.zcode', 'cli', 'config.json');
119
+ const runtime = await ensureRuntimeHome({ homeDir: home, version });
120
+ if (runtime.conflict) throw new InstallerError('Bridge 运行版本冲突,未修改 ZCode 配置。');
121
+ const registry = new ConsumerRegistry(home);
122
+ if (consumerName && registerConsumer) {
123
+ await registry.registerConsumer(consumerName, consumerMeta);
124
+ }
125
+ const config = readConfig(target);
126
+ const added = upsertManagedEntries(config, shimPathFor(home));
127
+ fs.mkdirSync(path.dirname(target), { recursive: true });
128
+ writeJsonAtomicSync(target, config);
129
+ return { added, configFile: target, runtime, consumers: (await registry.getConsumers()).map((c) => c.name) };
130
+ }
131
+
132
+ async function statusZcodeHook({ homeDir, configFile } = {}) {
133
+ const home = homeDir || path.join(os.homedir(), '.ai-coding-event-bridge');
134
+ const target = configFile || path.join(os.homedir(), '.zcode', 'cli', 'config.json');
135
+ const config = readConfig(target);
136
+ const events = config.hooks && typeof config.hooks === 'object' ? config.hooks.events || {} : {};
137
+ const shimPath = shimPathFor(home);
138
+ const managedEvents = [];
139
+ let thirdPartyCount = 0;
140
+ for (const eventName of Object.keys(events)) {
141
+ const groups = events[eventName];
142
+ if (!Array.isArray(groups)) continue;
143
+ let hasManaged = false;
144
+ for (const group of groups) {
145
+ for (const hook of (group && group.hooks) || []) {
146
+ if (isManagedHook(hook, shimPath)) {
147
+ hasManaged = true;
148
+ } else {
149
+ thirdPartyCount++;
150
+ }
151
+ }
152
+ }
153
+ if (hasManaged) managedEvents.push(eventName);
154
+ }
155
+ const registry = new ConsumerRegistry(home);
156
+ return {
157
+ installed:
158
+ HOOK_EVENTS.every((eventName) => managedEvents.includes(eventName)) &&
159
+ Boolean(config.hooks && config.hooks.enabled),
160
+ managedEvents,
161
+ thirdPartyCount,
162
+ consumers: (await registry.getConsumers()).map((c) => c.name)
163
+ };
164
+ }
165
+
166
+ async function repairZcodeHook({ homeDir, consumerName, consumerMeta = {}, registerConsumer = false, configFile, version } = {}) {
167
+ return installZcodeHook({ homeDir, consumerName, consumerMeta, registerConsumer, configFile, version });
168
+ }
169
+
170
+ async function uninstallZcodeHook({ homeDir, consumerName, unregisterConsumer = false, configFile } = {}) {
171
+ const home = homeDir || path.join(os.homedir(), '.ai-coding-event-bridge');
172
+ const target = configFile || path.join(os.homedir(), '.zcode', 'cli', 'config.json');
173
+ const registry = new ConsumerRegistry(home);
174
+ const config = readConfig(target);
175
+ const removed = config.hooks && config.hooks.events ? removeManagedEntries(config, shimPathFor(home)) : [];
176
+ if (removed.length) writeJsonAtomicSync(target, config);
177
+ let consumerUnregistered = false;
178
+ if (consumerName && unregisterConsumer) {
179
+ await registry.unregisterConsumer(consumerName);
180
+ consumerUnregistered = true;
181
+ }
182
+ return {
183
+ removed: true,
184
+ removedEvents: removed,
185
+ configFile: target,
186
+ consumerUnregistered,
187
+ consumers: (await registry.getConsumers()).map((c) => c.name)
188
+ };
189
+ }
190
+
191
+ module.exports = {
192
+ HOOK_EVENTS,
193
+ InstallerError,
194
+ installZcodeHook,
195
+ statusZcodeHook,
196
+ repairZcodeHook,
197
+ uninstallZcodeHook,
198
+ managedHook,
199
+ isManagedHook
200
+ };