@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 path = require('path');
5
- const { ensureDirSync } = require('./fs-utils');
6
-
7
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
8
-
9
- class LockError extends Error {
10
- constructor(message) {
11
- super(message);
12
- this.name = 'LockError';
13
- }
14
- }
15
-
16
- class CrossProcessLock {
17
- constructor(lockFile, { staleMs = 30000, pollMs = 15, timeoutMs = 60000 } = {}) {
18
- this.lockFile = lockFile;
19
- this.staleMs = staleMs;
20
- this.pollMs = pollMs;
21
- this.timeoutMs = timeoutMs;
22
- this._held = false;
23
- this._chain = Promise.resolve();
24
- }
25
-
26
- async acquire() {
27
- if (this._held) throw new LockError('lock already held by this handle');
28
- // Materialize the lock directory on first real use so that merely
29
- // constructing a registry/journal never creates a Bridge home.
30
- ensureDirSync(path.dirname(this.lockFile));
31
- const deadline = Date.now() + this.timeoutMs;
32
- for (;;) {
33
- try {
34
- const fd = fs.openSync(this.lockFile, 'wx');
35
- try {
36
- fs.writeSync(fd, `${JSON.stringify({ pid: process.pid, at: Date.now() })}\n`);
37
- } finally {
38
- fs.closeSync(fd);
39
- }
40
- this._held = true;
41
- return;
42
- } catch (err) {
43
- // EEXIST: lock file present. EPERM/EBUSY: Windows reports these when
44
- // another process holds the file without a sharing mode — still "locked".
45
- if (!err || !['EEXIST', 'EPERM', 'EBUSY'].includes(err.code)) throw err;
46
- }
47
- try {
48
- const st = fs.statSync(this.lockFile);
49
- if (Date.now() - st.mtimeMs > this.staleMs) {
50
- fs.unlinkSync(this.lockFile);
51
- continue;
52
- }
53
- } catch (_) {
54
- // Lock vanished between open failure and stat; retry immediately.
55
- }
56
- if (Date.now() > deadline) {
57
- throw new LockError(`timed out acquiring lock ${this.lockFile}`);
58
- }
59
- await sleep(this.pollMs);
60
- }
61
- }
62
-
63
- release() {
64
- if (!this._held) return;
65
- this._held = false;
66
- try {
67
- // A lock is one file, never a directory. unlink also avoids Node 24's
68
- // Windows rm(force) no-op on some non-ASCII filenames.
69
- fs.unlinkSync(this.lockFile);
70
- } catch (error) {
71
- if (!error || error.code !== 'ENOENT') throw error;
72
- }
73
- }
74
-
75
- async withLock(fn) {
76
- let release = null;
77
- const run = async () => {
78
- await this.acquire();
79
- release = () => this.release();
80
- try {
81
- return await fn();
82
- } finally {
83
- release();
84
- }
85
- };
86
- const result = this._chain.then(run, run);
87
- this._chain = result.then(() => undefined, () => undefined);
88
- return result;
89
- }
90
- }
91
-
92
- function lockPathFor(homeDir, name) {
93
- return path.join(homeDir, 'locks', name);
94
- }
95
-
96
- module.exports = { CrossProcessLock, lockPathFor, LockError };
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { ensureDirSync } = require('./fs-utils');
6
+
7
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
8
+
9
+ class LockError extends Error {
10
+ constructor(message) {
11
+ super(message);
12
+ this.name = 'LockError';
13
+ }
14
+ }
15
+
16
+ class CrossProcessLock {
17
+ constructor(lockFile, { staleMs = 30000, pollMs = 15, timeoutMs = 60000 } = {}) {
18
+ this.lockFile = lockFile;
19
+ this.staleMs = staleMs;
20
+ this.pollMs = pollMs;
21
+ this.timeoutMs = timeoutMs;
22
+ this._held = false;
23
+ this._chain = Promise.resolve();
24
+ }
25
+
26
+ async acquire() {
27
+ if (this._held) throw new LockError('lock already held by this handle');
28
+ // Materialize the lock directory on first real use so that merely
29
+ // constructing a registry/journal never creates a Bridge home.
30
+ ensureDirSync(path.dirname(this.lockFile));
31
+ const deadline = Date.now() + this.timeoutMs;
32
+ for (;;) {
33
+ try {
34
+ const fd = fs.openSync(this.lockFile, 'wx');
35
+ try {
36
+ fs.writeSync(fd, `${JSON.stringify({ pid: process.pid, at: Date.now() })}\n`);
37
+ } finally {
38
+ fs.closeSync(fd);
39
+ }
40
+ this._held = true;
41
+ return;
42
+ } catch (err) {
43
+ // EEXIST: lock file present. EPERM/EBUSY: Windows reports these when
44
+ // another process holds the file without a sharing mode — still "locked".
45
+ if (!err || !['EEXIST', 'EPERM', 'EBUSY'].includes(err.code)) throw err;
46
+ }
47
+ try {
48
+ const st = fs.statSync(this.lockFile);
49
+ if (Date.now() - st.mtimeMs > this.staleMs) {
50
+ fs.unlinkSync(this.lockFile);
51
+ continue;
52
+ }
53
+ } catch (_) {
54
+ // Lock vanished between open failure and stat; retry immediately.
55
+ }
56
+ if (Date.now() > deadline) {
57
+ throw new LockError(`timed out acquiring lock ${this.lockFile}`);
58
+ }
59
+ await sleep(this.pollMs);
60
+ }
61
+ }
62
+
63
+ release() {
64
+ if (!this._held) return;
65
+ this._held = false;
66
+ try {
67
+ // A lock is one file, never a directory. unlink also avoids Node 24's
68
+ // Windows rm(force) no-op on some non-ASCII filenames.
69
+ fs.unlinkSync(this.lockFile);
70
+ } catch (error) {
71
+ if (!error || error.code !== 'ENOENT') throw error;
72
+ }
73
+ }
74
+
75
+ async withLock(fn) {
76
+ let release = null;
77
+ const run = async () => {
78
+ await this.acquire();
79
+ release = () => this.release();
80
+ try {
81
+ return await fn();
82
+ } finally {
83
+ release();
84
+ }
85
+ };
86
+ const result = this._chain.then(run, run);
87
+ this._chain = result.then(() => undefined, () => undefined);
88
+ return result;
89
+ }
90
+ }
91
+
92
+ function lockPathFor(homeDir, name) {
93
+ return path.join(homeDir, 'locks', name);
94
+ }
95
+
96
+ module.exports = { CrossProcessLock, lockPathFor, LockError };
@@ -1,114 +1,114 @@
1
- 'use strict';
2
-
3
- const { randomUUID } = require('crypto');
4
- const { validateAndNormalizeEvent } = require('./event-schema');
5
-
6
- function assignTurnIdForPrompt() {
7
- // A real captured user prompt owns a fresh turn identity. This is identity
8
- // assignment for captured evidence, never content fabrication.
9
- return `turn_${randomUUID()}`;
10
- }
11
-
12
- /**
13
- * Claude Code hook payload -> normalized event.
14
- * Claude Code ships the event name as hook_event_name; hookName/hook_name
15
- * variants are accepted for compatibility. UserPromptSubmit carries the exact
16
- * user prompt; Stop yields an assistant_response only when
17
- * last_assistant_message exists — an empty Stop is not conversation evidence
18
- * and produces no event. Turn closure binding happens in the journal's
19
- * canonical append path.
20
- */
21
- function hookEventName(raw) {
22
- return raw.hook_event_name || raw.hookName || raw.hook_name || null;
23
- }
24
-
25
- function normalizeClaudeCode(raw, source = 'claude-code') {
26
- if (!raw || typeof raw !== 'object') return null;
27
- const base = { source: source === 'zcode' ? 'zcode' : 'claude-code' };
28
- if (hookEventName(raw) === 'UserPromptSubmit' || raw.eventType === 'user_prompt') {
29
- const content = typeof raw.prompt === 'string' ? raw.prompt : null;
30
- const event = {
31
- ...base,
32
- eventType: 'user_prompt',
33
- role: 'user',
34
- content,
35
- sessionId: raw.session_id || null,
36
- turnId: raw.turnId || assignTurnIdForPrompt(),
37
- repoIdentity: raw.repoIdentity,
38
- projectPath: raw.cwd,
39
- identityConfidence: content === null ? 'partial' : 'exact',
40
- captureStatus: content === null ? 'partial' : 'complete',
41
- rawEventType: hookEventName(raw) || 'user_prompt'
42
- };
43
- return validateAndNormalizeEvent(event);
44
- }
45
- if (hookEventName(raw) === 'Stop' || raw.eventType === 'assistant_response') {
46
- if (typeof raw.last_assistant_message !== 'string' || !raw.last_assistant_message) {
47
- return null;
48
- }
49
- return validateAndNormalizeEvent({
50
- ...base,
51
- eventType: 'assistant_response',
52
- role: 'assistant',
53
- content: raw.last_assistant_message,
54
- sessionId: raw.session_id || null,
55
- turnId: raw.turnId || null,
56
- repoIdentity: raw.repoIdentity,
57
- projectPath: raw.cwd,
58
- identityConfidence: 'partial',
59
- captureStatus: 'complete',
60
- rawEventType: hookEventName(raw) || 'assistant_response'
61
- });
62
- }
63
- return null;
64
- }
65
-
66
- /**
67
- * Codex events arrive via notify + session parsing (BR05 owns the parser).
68
- * The normalizer only maps a parsed turn record to the standard event shape.
69
- */
70
- function normalizeCodex(raw) {
71
- if (!raw || typeof raw !== 'object') return null;
72
- if (raw.eventType === 'user_prompt' || raw.eventType === 'assistant_response') {
73
- return validateAndNormalizeEvent({
74
- source: 'codex',
75
- eventType: raw.eventType,
76
- role: raw.eventType === 'user_prompt' ? 'user' : 'assistant',
77
- content: typeof raw.content === 'string' ? raw.content : null,
78
- sessionId: raw.sessionId || null,
79
- turnId: raw.turnId || (raw.eventType === 'user_prompt' ? assignTurnIdForPrompt() : null),
80
- repoIdentity: raw.repoIdentity,
81
- projectPath: raw.projectPath || raw.cwd,
82
- identityConfidence: raw.identityConfidence,
83
- captureStatus: raw.captureStatus,
84
- rawEventType: raw.rawEventType
85
- });
86
- }
87
- return null;
88
- }
89
-
90
- /**
91
- * OpenCode plugin events: user/assistant lifecycle from the official plugin.
92
- */
93
- function normalizeOpenCode(raw) {
94
- if (!raw || typeof raw !== 'object') return null;
95
- if (raw.type === 'user' || raw.type === 'assistant') {
96
- const isUser = raw.type === 'user';
97
- return validateAndNormalizeEvent({
98
- source: 'opencode',
99
- eventType: isUser ? 'user_prompt' : 'assistant_response',
100
- role: isUser ? 'user' : 'assistant',
101
- content: typeof raw.text === 'string' ? raw.text : null,
102
- sessionId: raw.sessionId || null,
103
- turnId: isUser ? (raw.turnId || assignTurnIdForPrompt()) : raw.turnId || null,
104
- repoIdentity: raw.repoIdentity,
105
- projectPath: raw.cwd || raw.projectPath,
106
- identityConfidence: raw.identityConfidence,
107
- captureStatus: raw.captureStatus,
108
- rawEventType: raw.type
109
- });
110
- }
111
- return null;
112
- }
113
-
114
- module.exports = { normalizeClaudeCode, normalizeCodex, normalizeOpenCode };
1
+ 'use strict';
2
+
3
+ const { randomUUID } = require('crypto');
4
+ const { validateAndNormalizeEvent } = require('./event-schema');
5
+
6
+ function assignTurnIdForPrompt() {
7
+ // A real captured user prompt owns a fresh turn identity. This is identity
8
+ // assignment for captured evidence, never content fabrication.
9
+ return `turn_${randomUUID()}`;
10
+ }
11
+
12
+ /**
13
+ * Claude Code hook payload -> normalized event.
14
+ * Claude Code ships the event name as hook_event_name; hookName/hook_name
15
+ * variants are accepted for compatibility. UserPromptSubmit carries the exact
16
+ * user prompt; Stop yields an assistant_response only when
17
+ * last_assistant_message exists — an empty Stop is not conversation evidence
18
+ * and produces no event. Turn closure binding happens in the journal's
19
+ * canonical append path.
20
+ */
21
+ function hookEventName(raw) {
22
+ return raw.hook_event_name || raw.hookName || raw.hook_name || null;
23
+ }
24
+
25
+ function normalizeClaudeCode(raw, source = 'claude-code') {
26
+ if (!raw || typeof raw !== 'object') return null;
27
+ const base = { source: source === 'zcode' ? 'zcode' : 'claude-code' };
28
+ if (hookEventName(raw) === 'UserPromptSubmit' || raw.eventType === 'user_prompt') {
29
+ const content = typeof raw.prompt === 'string' ? raw.prompt : null;
30
+ const event = {
31
+ ...base,
32
+ eventType: 'user_prompt',
33
+ role: 'user',
34
+ content,
35
+ sessionId: raw.session_id || null,
36
+ turnId: raw.turnId || assignTurnIdForPrompt(),
37
+ repoIdentity: raw.repoIdentity,
38
+ projectPath: raw.cwd,
39
+ identityConfidence: content === null ? 'partial' : 'exact',
40
+ captureStatus: content === null ? 'partial' : 'complete',
41
+ rawEventType: hookEventName(raw) || 'user_prompt'
42
+ };
43
+ return validateAndNormalizeEvent(event);
44
+ }
45
+ if (hookEventName(raw) === 'Stop' || raw.eventType === 'assistant_response') {
46
+ if (typeof raw.last_assistant_message !== 'string' || !raw.last_assistant_message) {
47
+ return null;
48
+ }
49
+ return validateAndNormalizeEvent({
50
+ ...base,
51
+ eventType: 'assistant_response',
52
+ role: 'assistant',
53
+ content: raw.last_assistant_message,
54
+ sessionId: raw.session_id || null,
55
+ turnId: raw.turnId || null,
56
+ repoIdentity: raw.repoIdentity,
57
+ projectPath: raw.cwd,
58
+ identityConfidence: 'partial',
59
+ captureStatus: 'complete',
60
+ rawEventType: hookEventName(raw) || 'assistant_response'
61
+ });
62
+ }
63
+ return null;
64
+ }
65
+
66
+ /**
67
+ * Codex events arrive via notify + session parsing (BR05 owns the parser).
68
+ * The normalizer only maps a parsed turn record to the standard event shape.
69
+ */
70
+ function normalizeCodex(raw) {
71
+ if (!raw || typeof raw !== 'object') return null;
72
+ if (raw.eventType === 'user_prompt' || raw.eventType === 'assistant_response') {
73
+ return validateAndNormalizeEvent({
74
+ source: 'codex',
75
+ eventType: raw.eventType,
76
+ role: raw.eventType === 'user_prompt' ? 'user' : 'assistant',
77
+ content: typeof raw.content === 'string' ? raw.content : null,
78
+ sessionId: raw.sessionId || null,
79
+ turnId: raw.turnId || (raw.eventType === 'user_prompt' ? assignTurnIdForPrompt() : null),
80
+ repoIdentity: raw.repoIdentity,
81
+ projectPath: raw.projectPath || raw.cwd,
82
+ identityConfidence: raw.identityConfidence,
83
+ captureStatus: raw.captureStatus,
84
+ rawEventType: raw.rawEventType
85
+ });
86
+ }
87
+ return null;
88
+ }
89
+
90
+ /**
91
+ * OpenCode plugin events: user/assistant lifecycle from the official plugin.
92
+ */
93
+ function normalizeOpenCode(raw) {
94
+ if (!raw || typeof raw !== 'object') return null;
95
+ if (raw.type === 'user' || raw.type === 'assistant') {
96
+ const isUser = raw.type === 'user';
97
+ return validateAndNormalizeEvent({
98
+ source: 'opencode',
99
+ eventType: isUser ? 'user_prompt' : 'assistant_response',
100
+ role: isUser ? 'user' : 'assistant',
101
+ content: typeof raw.text === 'string' ? raw.text : null,
102
+ sessionId: raw.sessionId || null,
103
+ turnId: isUser ? (raw.turnId || assignTurnIdForPrompt()) : raw.turnId || null,
104
+ repoIdentity: raw.repoIdentity,
105
+ projectPath: raw.cwd || raw.projectPath,
106
+ identityConfidence: raw.identityConfidence,
107
+ captureStatus: raw.captureStatus,
108
+ rawEventType: raw.type
109
+ });
110
+ }
111
+ return null;
112
+ }
113
+
114
+ module.exports = { normalizeClaudeCode, normalizeCodex, normalizeOpenCode };
@@ -1,12 +1,12 @@
1
- 'use strict';
2
-
3
- const os = require('os');
4
- const path = require('path');
5
-
6
- function bridgeHome(explicit) {
7
- if (explicit) return explicit;
8
- if (process.env.AI_CODING_EVENT_BRIDGE_HOME) return process.env.AI_CODING_EVENT_BRIDGE_HOME;
9
- return path.join(os.homedir(), '.ai-coding-event-bridge');
10
- }
11
-
12
- module.exports = { bridgeHome };
1
+ 'use strict';
2
+
3
+ const os = require('os');
4
+ const path = require('path');
5
+
6
+ function bridgeHome(explicit) {
7
+ if (explicit) return explicit;
8
+ if (process.env.AI_CODING_EVENT_BRIDGE_HOME) return process.env.AI_CODING_EVENT_BRIDGE_HOME;
9
+ return path.join(os.homedir(), '.ai-coding-event-bridge');
10
+ }
11
+
12
+ module.exports = { bridgeHome };