@sanqianx/ai-coding-event-bridge-console 0.7.2 → 0.7.3
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/LICENSE +21 -21
- package/README.md +81 -81
- package/console/index.html +953 -953
- package/package.json +26 -26
- package/src/agent-detect.js +128 -128
- package/src/bin.js +80 -80
- package/src/bridge/bin/commit-boundary.js +64 -64
- package/src/bridge/connectors/capture-guard.js +24 -24
- package/src/bridge/connectors/claude-code/hook-entry.js +45 -45
- package/src/bridge/connectors/claude-code/notify.js +57 -57
- package/src/bridge/connectors/codex/cursor-store.js +83 -83
- package/src/bridge/connectors/codex/hook-entry.js +215 -215
- package/src/bridge/connectors/codex/session-parser.js +100 -100
- package/src/bridge/connectors/opencode/hook-entry.js +55 -55
- package/src/bridge/core/bridge.js +249 -246
- package/src/bridge/core/commit-projection.js +288 -288
- package/src/bridge/core/compaction.js +23 -23
- package/src/bridge/core/consumer-registry.js +111 -111
- package/src/bridge/core/event-schema.js +127 -127
- package/src/bridge/core/fs-utils.js +81 -81
- package/src/bridge/core/journal-router.js +39 -39
- package/src/bridge/core/journal.js +507 -507
- package/src/bridge/core/lock.js +96 -96
- package/src/bridge/core/normalizer.js +114 -114
- package/src/bridge/core/paths.js +12 -12
- package/src/bridge/core/project-registry.js +176 -176
- package/src/bridge/core/repo-context.js +191 -191
- package/src/bridge/core/runtime-home.js +189 -189
- package/src/bridge/core/semver.js +23 -23
- package/src/bridge/core/turn-identity.js +127 -127
- package/src/bridge/index.js +86 -86
- package/src/bridge/installers/claude-code/installer.js +181 -181
- package/src/bridge/installers/codex/installer.js +114 -114
- package/src/bridge/installers/opencode/installer.js +96 -96
- package/src/bridge/installers/zcode/installer.js +200 -200
- package/src/bridge/query/conversation-query.js +194 -194
- package/src/server.js +825 -825
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { normalizeClaudeCode } = require('../../core/normalizer');
|
|
4
|
-
const { resolveRepoContext } = require('../../core/repo-context');
|
|
5
|
-
const { journalFor } = require('../../core/journal-router');
|
|
6
|
-
const { isCaptureDisabled, captureDisabledResult } = require('../capture-guard');
|
|
7
|
-
const { notifyConsumers } = require('./notify');
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Durable-first Claude Code hook processing. The event is appended and fsynced
|
|
11
|
-
* to the Bridge journal before any consumer notification is attempted, and the
|
|
12
|
-
* whole entry fails open: a broken Bridge must never block the AI client.
|
|
13
|
-
* Internal host SDK sessions (AI_CODING_EVENT_BRIDGE_CAPTURE=0) are ignored
|
|
14
|
-
* before any Git/journal work happens.
|
|
15
|
-
*/
|
|
16
|
-
async function main({ home, payload, source }) {
|
|
17
|
-
if (isCaptureDisabled(process.env, payload)) {
|
|
18
|
-
return captureDisabledResult();
|
|
19
|
-
}
|
|
20
|
-
if (!payload || typeof payload !== 'object') {
|
|
21
|
-
return { status: 'ignored' };
|
|
22
|
-
}
|
|
23
|
-
const event = normalizeClaudeCode(payload, source);
|
|
24
|
-
if (!event) {
|
|
25
|
-
return { status: 'ignored' };
|
|
26
|
-
}
|
|
27
|
-
const cwd = typeof payload.cwd === 'string' && payload.cwd ? payload.cwd : process.cwd();
|
|
28
|
-
const repo = await resolveRepoContext(cwd);
|
|
29
|
-
const journal = journalFor(home, event.repoIdentity || repo.repoIdentity);
|
|
30
|
-
const appended = await journal.appendConversationEvent({
|
|
31
|
-
...event,
|
|
32
|
-
repoIdentity: event.repoIdentity || repo.repoIdentity,
|
|
33
|
-
projectPath: event.projectPath || repo.projectPath,
|
|
34
|
-
branch: event.branch || repo.branch,
|
|
35
|
-
headAtCapture: event.headAtCapture || repo.headAtCapture
|
|
36
|
-
});
|
|
37
|
-
await notifyConsumers(home, { sequence: appended.sequence, eventId: appended.eventId });
|
|
38
|
-
return { status: 'captured', sequence: appended.sequence, turnId: appended.turnId };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function mainFailOpen({ home, payload, source }) {
|
|
42
|
-
return main({ home, payload, source }).catch(() => ({ status: 'fail-open' }));
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
module.exports = { main, mainFailOpen };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { normalizeClaudeCode } = require('../../core/normalizer');
|
|
4
|
+
const { resolveRepoContext } = require('../../core/repo-context');
|
|
5
|
+
const { journalFor } = require('../../core/journal-router');
|
|
6
|
+
const { isCaptureDisabled, captureDisabledResult } = require('../capture-guard');
|
|
7
|
+
const { notifyConsumers } = require('./notify');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Durable-first Claude Code hook processing. The event is appended and fsynced
|
|
11
|
+
* to the Bridge journal before any consumer notification is attempted, and the
|
|
12
|
+
* whole entry fails open: a broken Bridge must never block the AI client.
|
|
13
|
+
* Internal host SDK sessions (AI_CODING_EVENT_BRIDGE_CAPTURE=0) are ignored
|
|
14
|
+
* before any Git/journal work happens.
|
|
15
|
+
*/
|
|
16
|
+
async function main({ home, payload, source }) {
|
|
17
|
+
if (isCaptureDisabled(process.env, payload)) {
|
|
18
|
+
return captureDisabledResult();
|
|
19
|
+
}
|
|
20
|
+
if (!payload || typeof payload !== 'object') {
|
|
21
|
+
return { status: 'ignored' };
|
|
22
|
+
}
|
|
23
|
+
const event = normalizeClaudeCode(payload, source);
|
|
24
|
+
if (!event) {
|
|
25
|
+
return { status: 'ignored' };
|
|
26
|
+
}
|
|
27
|
+
const cwd = typeof payload.cwd === 'string' && payload.cwd ? payload.cwd : process.cwd();
|
|
28
|
+
const repo = await resolveRepoContext(cwd);
|
|
29
|
+
const journal = journalFor(home, event.repoIdentity || repo.repoIdentity);
|
|
30
|
+
const appended = await journal.appendConversationEvent({
|
|
31
|
+
...event,
|
|
32
|
+
repoIdentity: event.repoIdentity || repo.repoIdentity,
|
|
33
|
+
projectPath: event.projectPath || repo.projectPath,
|
|
34
|
+
branch: event.branch || repo.branch,
|
|
35
|
+
headAtCapture: event.headAtCapture || repo.headAtCapture
|
|
36
|
+
});
|
|
37
|
+
await notifyConsumers(home, { sequence: appended.sequence, eventId: appended.eventId });
|
|
38
|
+
return { status: 'captured', sequence: appended.sequence, turnId: appended.turnId };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function mainFailOpen({ home, payload, source }) {
|
|
42
|
+
return main({ home, payload, source }).catch(() => ({ status: 'fail-open' }));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = { main, mainFailOpen };
|
|
@@ -1,57 +1,57 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const http = require('http');
|
|
4
|
-
const https = require('https');
|
|
5
|
-
const { ConsumerRegistry } = require('../../core/consumer-registry');
|
|
6
|
-
|
|
7
|
-
function postJson(url, body, timeoutMs = 1500) {
|
|
8
|
-
return new Promise((resolve) => {
|
|
9
|
-
try {
|
|
10
|
-
const target = new URL(url);
|
|
11
|
-
const mod = target.protocol === 'https:' ? https : http;
|
|
12
|
-
const data = JSON.stringify(body);
|
|
13
|
-
const req = mod.request(
|
|
14
|
-
{
|
|
15
|
-
hostname: target.hostname,
|
|
16
|
-
port: target.port || (target.protocol === 'https:' ? 443 : 80),
|
|
17
|
-
path: `${target.pathname}${target.search}`,
|
|
18
|
-
method: 'POST',
|
|
19
|
-
headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(data) },
|
|
20
|
-
timeout: timeoutMs
|
|
21
|
-
},
|
|
22
|
-
(res) => {
|
|
23
|
-
res.resume();
|
|
24
|
-
res.on('end', () => resolve({ ok: res.statusCode && res.statusCode < 500 }));
|
|
25
|
-
}
|
|
26
|
-
);
|
|
27
|
-
req.on('timeout', () => {
|
|
28
|
-
req.destroy();
|
|
29
|
-
resolve({ ok: false });
|
|
30
|
-
});
|
|
31
|
-
req.on('error', () => resolve({ ok: false }));
|
|
32
|
-
req.end(data);
|
|
33
|
-
} catch (_) {
|
|
34
|
-
resolve({ ok: false });
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Best-effort wake-up for consumers that registered a notifyUrl. Always runs
|
|
41
|
-
* after the durable append; failures never propagate to the AI client.
|
|
42
|
-
*/
|
|
43
|
-
async function notifyConsumers(home, info) {
|
|
44
|
-
try {
|
|
45
|
-
const registry = new ConsumerRegistry(home);
|
|
46
|
-
const consumers = await registry.getConsumers();
|
|
47
|
-
const urls = consumers
|
|
48
|
-
.map((consumer) => consumer.meta && consumer.meta.notifyUrl)
|
|
49
|
-
.filter((url) => typeof url === 'string' && url);
|
|
50
|
-
await Promise.all(urls.map((url) => postJson(url, info)));
|
|
51
|
-
return { notified: urls.length };
|
|
52
|
-
} catch (_) {
|
|
53
|
-
return { notified: 0 };
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
module.exports = { notifyConsumers, postJson };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const http = require('http');
|
|
4
|
+
const https = require('https');
|
|
5
|
+
const { ConsumerRegistry } = require('../../core/consumer-registry');
|
|
6
|
+
|
|
7
|
+
function postJson(url, body, timeoutMs = 1500) {
|
|
8
|
+
return new Promise((resolve) => {
|
|
9
|
+
try {
|
|
10
|
+
const target = new URL(url);
|
|
11
|
+
const mod = target.protocol === 'https:' ? https : http;
|
|
12
|
+
const data = JSON.stringify(body);
|
|
13
|
+
const req = mod.request(
|
|
14
|
+
{
|
|
15
|
+
hostname: target.hostname,
|
|
16
|
+
port: target.port || (target.protocol === 'https:' ? 443 : 80),
|
|
17
|
+
path: `${target.pathname}${target.search}`,
|
|
18
|
+
method: 'POST',
|
|
19
|
+
headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(data) },
|
|
20
|
+
timeout: timeoutMs
|
|
21
|
+
},
|
|
22
|
+
(res) => {
|
|
23
|
+
res.resume();
|
|
24
|
+
res.on('end', () => resolve({ ok: res.statusCode && res.statusCode < 500 }));
|
|
25
|
+
}
|
|
26
|
+
);
|
|
27
|
+
req.on('timeout', () => {
|
|
28
|
+
req.destroy();
|
|
29
|
+
resolve({ ok: false });
|
|
30
|
+
});
|
|
31
|
+
req.on('error', () => resolve({ ok: false }));
|
|
32
|
+
req.end(data);
|
|
33
|
+
} catch (_) {
|
|
34
|
+
resolve({ ok: false });
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Best-effort wake-up for consumers that registered a notifyUrl. Always runs
|
|
41
|
+
* after the durable append; failures never propagate to the AI client.
|
|
42
|
+
*/
|
|
43
|
+
async function notifyConsumers(home, info) {
|
|
44
|
+
try {
|
|
45
|
+
const registry = new ConsumerRegistry(home);
|
|
46
|
+
const consumers = await registry.getConsumers();
|
|
47
|
+
const urls = consumers
|
|
48
|
+
.map((consumer) => consumer.meta && consumer.meta.notifyUrl)
|
|
49
|
+
.filter((url) => typeof url === 'string' && url);
|
|
50
|
+
await Promise.all(urls.map((url) => postJson(url, info)));
|
|
51
|
+
return { notified: urls.length };
|
|
52
|
+
} catch (_) {
|
|
53
|
+
return { notified: 0 };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = { notifyConsumers, postJson };
|
|
@@ -1,83 +1,83 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const { writeJsonAtomicSync, readJsonIfExists, ensureDirSync } = require('../../core/fs-utils');
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Atomic per-session cursor state. One JSON file per Codex session under
|
|
9
|
-
* <home>/cursors/codex/, written via rename so a crash never corrupts it.
|
|
10
|
-
* Cursor v2 stores an independent offset and workspace context for every
|
|
11
|
-
* rollout file. A v1 cursor is upgraded in memory without resetting its
|
|
12
|
-
* consumed offset, so discovering a continuation file never replays history.
|
|
13
|
-
*/
|
|
14
|
-
const CURSOR_SCHEMA = 'codex-cursor/v2';
|
|
15
|
-
|
|
16
|
-
function fileState(state = {}) {
|
|
17
|
-
return {
|
|
18
|
-
filePath: state.filePath,
|
|
19
|
-
byteOffset: Number.isInteger(state.byteOffset) && state.byteOffset >= 0 ? state.byteOffset : 0,
|
|
20
|
-
lastRecordKey: state.lastRecordKey || null,
|
|
21
|
-
activeCwd: state.activeCwd || null,
|
|
22
|
-
repoIdentity: state.repoIdentity || null,
|
|
23
|
-
projectPath: state.projectPath || null,
|
|
24
|
-
branch: state.branch || null,
|
|
25
|
-
headAtCapture: state.headAtCapture || null
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function normalizeCursor(sessionId, state) {
|
|
30
|
-
const normalized = { schema: CURSOR_SCHEMA, sessionId: String(sessionId), files: {} };
|
|
31
|
-
if (!state || typeof state !== 'object') return normalized;
|
|
32
|
-
|
|
33
|
-
if (state.schema === CURSOR_SCHEMA && state.files && typeof state.files === 'object') {
|
|
34
|
-
for (const [key, value] of Object.entries(state.files)) {
|
|
35
|
-
if (!value || typeof value !== 'object' || typeof value.filePath !== 'string') continue;
|
|
36
|
-
normalized.files[key] = fileState(value);
|
|
37
|
-
}
|
|
38
|
-
return normalized;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// v1 compatibility: preserve the exact consumed offset and context. The
|
|
42
|
-
// caller supplies the canonical key after resolving the legacy file path.
|
|
43
|
-
if (typeof state.filePath === 'string' && state.filePath) {
|
|
44
|
-
normalized.legacyFile = fileState(state);
|
|
45
|
-
}
|
|
46
|
-
return normalized;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
class CodexCursorStore {
|
|
50
|
-
constructor(homeDir) {
|
|
51
|
-
this.dir = path.join(homeDir, 'cursors', 'codex');
|
|
52
|
-
ensureDirSync(this.dir);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
_fileFor(sessionId) {
|
|
56
|
-
const safe = String(sessionId).replace(/[^A-Za-z0-9._-]/g, '_');
|
|
57
|
-
return path.join(this.dir, `${safe}.json`);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
read(sessionId) {
|
|
61
|
-
return normalizeCursor(sessionId, readJsonIfExists(this._fileFor(sessionId)));
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
write(sessionId, state) {
|
|
65
|
-
const normalized = normalizeCursor(sessionId, state);
|
|
66
|
-
writeJsonAtomicSync(this._fileFor(sessionId), {
|
|
67
|
-
schema: CURSOR_SCHEMA,
|
|
68
|
-
sessionId: normalized.sessionId,
|
|
69
|
-
files: normalized.files,
|
|
70
|
-
updatedAt: new Date().toISOString()
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
clear(sessionId) {
|
|
75
|
-
try {
|
|
76
|
-
fs.rmSync(this._fileFor(sessionId), { force: true });
|
|
77
|
-
} catch (_) {
|
|
78
|
-
// Already gone.
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
module.exports = { CodexCursorStore, CURSOR_SCHEMA, normalizeCursor };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { writeJsonAtomicSync, readJsonIfExists, ensureDirSync } = require('../../core/fs-utils');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Atomic per-session cursor state. One JSON file per Codex session under
|
|
9
|
+
* <home>/cursors/codex/, written via rename so a crash never corrupts it.
|
|
10
|
+
* Cursor v2 stores an independent offset and workspace context for every
|
|
11
|
+
* rollout file. A v1 cursor is upgraded in memory without resetting its
|
|
12
|
+
* consumed offset, so discovering a continuation file never replays history.
|
|
13
|
+
*/
|
|
14
|
+
const CURSOR_SCHEMA = 'codex-cursor/v2';
|
|
15
|
+
|
|
16
|
+
function fileState(state = {}) {
|
|
17
|
+
return {
|
|
18
|
+
filePath: state.filePath,
|
|
19
|
+
byteOffset: Number.isInteger(state.byteOffset) && state.byteOffset >= 0 ? state.byteOffset : 0,
|
|
20
|
+
lastRecordKey: state.lastRecordKey || null,
|
|
21
|
+
activeCwd: state.activeCwd || null,
|
|
22
|
+
repoIdentity: state.repoIdentity || null,
|
|
23
|
+
projectPath: state.projectPath || null,
|
|
24
|
+
branch: state.branch || null,
|
|
25
|
+
headAtCapture: state.headAtCapture || null
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeCursor(sessionId, state) {
|
|
30
|
+
const normalized = { schema: CURSOR_SCHEMA, sessionId: String(sessionId), files: {} };
|
|
31
|
+
if (!state || typeof state !== 'object') return normalized;
|
|
32
|
+
|
|
33
|
+
if (state.schema === CURSOR_SCHEMA && state.files && typeof state.files === 'object') {
|
|
34
|
+
for (const [key, value] of Object.entries(state.files)) {
|
|
35
|
+
if (!value || typeof value !== 'object' || typeof value.filePath !== 'string') continue;
|
|
36
|
+
normalized.files[key] = fileState(value);
|
|
37
|
+
}
|
|
38
|
+
return normalized;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// v1 compatibility: preserve the exact consumed offset and context. The
|
|
42
|
+
// caller supplies the canonical key after resolving the legacy file path.
|
|
43
|
+
if (typeof state.filePath === 'string' && state.filePath) {
|
|
44
|
+
normalized.legacyFile = fileState(state);
|
|
45
|
+
}
|
|
46
|
+
return normalized;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
class CodexCursorStore {
|
|
50
|
+
constructor(homeDir) {
|
|
51
|
+
this.dir = path.join(homeDir, 'cursors', 'codex');
|
|
52
|
+
ensureDirSync(this.dir);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
_fileFor(sessionId) {
|
|
56
|
+
const safe = String(sessionId).replace(/[^A-Za-z0-9._-]/g, '_');
|
|
57
|
+
return path.join(this.dir, `${safe}.json`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
read(sessionId) {
|
|
61
|
+
return normalizeCursor(sessionId, readJsonIfExists(this._fileFor(sessionId)));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
write(sessionId, state) {
|
|
65
|
+
const normalized = normalizeCursor(sessionId, state);
|
|
66
|
+
writeJsonAtomicSync(this._fileFor(sessionId), {
|
|
67
|
+
schema: CURSOR_SCHEMA,
|
|
68
|
+
sessionId: normalized.sessionId,
|
|
69
|
+
files: normalized.files,
|
|
70
|
+
updatedAt: new Date().toISOString()
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
clear(sessionId) {
|
|
75
|
+
try {
|
|
76
|
+
fs.rmSync(this._fileFor(sessionId), { force: true });
|
|
77
|
+
} catch (_) {
|
|
78
|
+
// Already gone.
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = { CodexCursorStore, CURSOR_SCHEMA, normalizeCursor };
|