@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.
- 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/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
package/package.json
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@sanqianx/ai-coding-event-bridge-console",
|
|
3
|
-
"version": "0.7.
|
|
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
|
-
"license": "MIT",
|
|
6
|
-
"private": false,
|
|
7
|
-
"repository": {
|
|
8
|
-
"type": "git",
|
|
9
|
-
"url": "https://github.com/SanQianX/ai-coding-event-bridge",
|
|
10
|
-
"directory": "packages/console"
|
|
11
|
-
},
|
|
12
|
-
"main": "src/server.js",
|
|
13
|
-
"bin": {
|
|
14
|
-
"ai-coding-event-bridge-console": "src/bin.js"
|
|
15
|
-
},
|
|
16
|
-
"engines": {
|
|
17
|
-
"node": ">=18"
|
|
18
|
-
},
|
|
19
|
-
"files": [
|
|
20
|
-
"src",
|
|
21
|
-
"console"
|
|
22
|
-
],
|
|
23
|
-
"scripts": {
|
|
24
|
-
"test": "node ../../test/run-all.js"
|
|
25
|
-
}
|
|
26
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@sanqianx/ai-coding-event-bridge-console",
|
|
3
|
+
"version": "0.7.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
|
+
"license": "MIT",
|
|
6
|
+
"private": false,
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/SanQianX/ai-coding-event-bridge",
|
|
10
|
+
"directory": "packages/console"
|
|
11
|
+
},
|
|
12
|
+
"main": "src/server.js",
|
|
13
|
+
"bin": {
|
|
14
|
+
"ai-coding-event-bridge-console": "src/bin.js"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"src",
|
|
21
|
+
"console"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"test": "node ../../test/run-all.js"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/agent-detect.js
CHANGED
|
@@ -1,128 +1,128 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const os = require('os');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
const { execFileSync } = require('child_process');
|
|
7
|
-
const core = require('./bridge');
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Detection of AI coding agent tools on this machine. "Installed" means the
|
|
11
|
-
* CLI is on PATH or its config exists; "hookInstalled" means the bridge shim
|
|
12
|
-
* is wired in — either as the installer's managed entry, or through a
|
|
13
|
-
* consumer fan-out chain (for example Codex notify lines that forward to the
|
|
14
|
-
* shim base64-encoded), which the managed-entry check alone would miss.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
function commandExists(command) {
|
|
18
|
-
const finder = process.platform === 'win32' ? 'where' : 'which';
|
|
19
|
-
try {
|
|
20
|
-
execFileSync(finder, [command], { stdio: 'ignore', windowsHide: true });
|
|
21
|
-
return true;
|
|
22
|
-
} catch (_) {
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function readTextIfAny(file) {
|
|
28
|
-
try {
|
|
29
|
-
return fs.readFileSync(file, 'utf8');
|
|
30
|
-
} catch (_) {
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function fileExists(file) {
|
|
36
|
-
try {
|
|
37
|
-
return fs.statSync(file).isFile();
|
|
38
|
-
} catch (_) {
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function mentionsShim(text) {
|
|
44
|
-
return typeof text === 'string' && (text.includes('bridge-hook.cjs') || text.includes('--bridge-base64'));
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function detectAgents({ homeDir, claudeSettingsFile, codexConfigFile, opencodePluginsDir, zcodeConfigFile } = {}) {
|
|
48
|
-
const home = os.homedir();
|
|
49
|
-
|
|
50
|
-
const claudeFiles = [
|
|
51
|
-
claudeSettingsFile || path.join(home, '.claude', 'settings.json'),
|
|
52
|
-
path.join(path.dirname(claudeSettingsFile || path.join(home, '.claude', 'settings.json')), 'settings.local.json')
|
|
53
|
-
];
|
|
54
|
-
const claudeStatus = await core.installers.claudeCode.statusClaudeCodeHook({
|
|
55
|
-
homeDir,
|
|
56
|
-
settingsFile: claudeFiles[0]
|
|
57
|
-
});
|
|
58
|
-
const claudeConfigFound = claudeFiles.some(fileExists);
|
|
59
|
-
const claudeTextHooked = claudeFiles.some((file) => mentionsShim(readTextIfAny(file)));
|
|
60
|
-
|
|
61
|
-
const codexFile = codexConfigFile || path.join(home, '.codex', 'config.toml');
|
|
62
|
-
const codexStatus = await core.installers.codex.statusCodexNotify({ homeDir, configFile: codexFile });
|
|
63
|
-
const codexConfigFound = fileExists(codexFile);
|
|
64
|
-
const codexTextHooked = mentionsShim(readTextIfAny(codexFile));
|
|
65
|
-
|
|
66
|
-
const opencodeDir = opencodePluginsDir || path.join(home, '.config', 'opencode', 'plugin');
|
|
67
|
-
const opencodeStatus = await core.installers.openCode.statusOpenCodePlugin({ homeDir, pluginsDir: opencodeDir });
|
|
68
|
-
let opencodePluginHooked = Boolean(opencodeStatus.installed);
|
|
69
|
-
if (!opencodePluginHooked) {
|
|
70
|
-
try {
|
|
71
|
-
for (const name of fs.readdirSync(opencodeDir)) {
|
|
72
|
-
if (mentionsShim(readTextIfAny(path.join(opencodeDir, name)))) {
|
|
73
|
-
opencodePluginHooked = true;
|
|
74
|
-
break;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
} catch (_) {
|
|
78
|
-
/* no plugin dir */
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const zcodeFile = zcodeConfigFile || path.join(home, '.zcode', 'cli', 'config.json');
|
|
83
|
-
const zcodeStatus = await core.installers.zcode.statusZcodeHook({ homeDir, configFile: zcodeFile });
|
|
84
|
-
|
|
85
|
-
return [
|
|
86
|
-
{
|
|
87
|
-
id: 'claude-code',
|
|
88
|
-
label: 'Claude Code',
|
|
89
|
-
cliFound: commandExists('claude'),
|
|
90
|
-
configFound: claudeConfigFound,
|
|
91
|
-
hookInstalled: Boolean(claudeStatus.installed) || claudeTextHooked,
|
|
92
|
-
managed: Boolean(claudeStatus.installed),
|
|
93
|
-
managedEvents: claudeStatus.managedEvents || [],
|
|
94
|
-
detail: claudeStatus.thirdPartyCount ? `另有 ${claudeStatus.thirdPartyCount} 条第三方 hook(保留不动)` : null
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
id: 'codex',
|
|
98
|
-
label: 'Codex',
|
|
99
|
-
cliFound: commandExists('codex'),
|
|
100
|
-
configFound: codexConfigFound,
|
|
101
|
-
hookInstalled: Boolean(codexStatus.installed) || codexTextHooked,
|
|
102
|
-
managed: Boolean(codexStatus.installed),
|
|
103
|
-
detail: codexStatus.thirdParty ? 'notify 已被第三方/消费方链占用(转发链仍会调用 bridge)' : null
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
id: 'opencode',
|
|
107
|
-
label: 'OpenCode',
|
|
108
|
-
cliFound: commandExists('opencode'),
|
|
109
|
-
configFound: fileExists(path.join(path.dirname(opencodeDir), 'opencode.jsonc')) || fileExists(path.join(path.dirname(opencodeDir), 'opencode.json')),
|
|
110
|
-
hookInstalled: opencodePluginHooked,
|
|
111
|
-
managed: Boolean(opencodeStatus.installed),
|
|
112
|
-
detail: opencodeStatus.thirdPartyFiles && opencodeStatus.thirdPartyFiles.length
|
|
113
|
-
? `插件目录另有 ${opencodeStatus.thirdPartyFiles.length} 个第三方插件(保留不动)`
|
|
114
|
-
: null
|
|
115
|
-
},
|
|
116
|
-
{
|
|
117
|
-
id: 'zcode',
|
|
118
|
-
label: 'ZCode',
|
|
119
|
-
cliFound: commandExists('zcode'),
|
|
120
|
-
configFound: fileExists(zcodeFile),
|
|
121
|
-
hookInstalled: Boolean(zcodeStatus.installed),
|
|
122
|
-
managed: Boolean(zcodeStatus.installed),
|
|
123
|
-
detail: zcodeStatus.thirdPartyCount ? `配置中另有 ${zcodeStatus.thirdPartyCount} 条第三方 hook(保留不动)` : null
|
|
124
|
-
}
|
|
125
|
-
];
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
module.exports = { detectAgents, commandExists };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { execFileSync } = require('child_process');
|
|
7
|
+
const core = require('./bridge');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Detection of AI coding agent tools on this machine. "Installed" means the
|
|
11
|
+
* CLI is on PATH or its config exists; "hookInstalled" means the bridge shim
|
|
12
|
+
* is wired in — either as the installer's managed entry, or through a
|
|
13
|
+
* consumer fan-out chain (for example Codex notify lines that forward to the
|
|
14
|
+
* shim base64-encoded), which the managed-entry check alone would miss.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
function commandExists(command) {
|
|
18
|
+
const finder = process.platform === 'win32' ? 'where' : 'which';
|
|
19
|
+
try {
|
|
20
|
+
execFileSync(finder, [command], { stdio: 'ignore', windowsHide: true });
|
|
21
|
+
return true;
|
|
22
|
+
} catch (_) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readTextIfAny(file) {
|
|
28
|
+
try {
|
|
29
|
+
return fs.readFileSync(file, 'utf8');
|
|
30
|
+
} catch (_) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function fileExists(file) {
|
|
36
|
+
try {
|
|
37
|
+
return fs.statSync(file).isFile();
|
|
38
|
+
} catch (_) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function mentionsShim(text) {
|
|
44
|
+
return typeof text === 'string' && (text.includes('bridge-hook.cjs') || text.includes('--bridge-base64'));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function detectAgents({ homeDir, claudeSettingsFile, codexConfigFile, opencodePluginsDir, zcodeConfigFile } = {}) {
|
|
48
|
+
const home = os.homedir();
|
|
49
|
+
|
|
50
|
+
const claudeFiles = [
|
|
51
|
+
claudeSettingsFile || path.join(home, '.claude', 'settings.json'),
|
|
52
|
+
path.join(path.dirname(claudeSettingsFile || path.join(home, '.claude', 'settings.json')), 'settings.local.json')
|
|
53
|
+
];
|
|
54
|
+
const claudeStatus = await core.installers.claudeCode.statusClaudeCodeHook({
|
|
55
|
+
homeDir,
|
|
56
|
+
settingsFile: claudeFiles[0]
|
|
57
|
+
});
|
|
58
|
+
const claudeConfigFound = claudeFiles.some(fileExists);
|
|
59
|
+
const claudeTextHooked = claudeFiles.some((file) => mentionsShim(readTextIfAny(file)));
|
|
60
|
+
|
|
61
|
+
const codexFile = codexConfigFile || path.join(home, '.codex', 'config.toml');
|
|
62
|
+
const codexStatus = await core.installers.codex.statusCodexNotify({ homeDir, configFile: codexFile });
|
|
63
|
+
const codexConfigFound = fileExists(codexFile);
|
|
64
|
+
const codexTextHooked = mentionsShim(readTextIfAny(codexFile));
|
|
65
|
+
|
|
66
|
+
const opencodeDir = opencodePluginsDir || path.join(home, '.config', 'opencode', 'plugin');
|
|
67
|
+
const opencodeStatus = await core.installers.openCode.statusOpenCodePlugin({ homeDir, pluginsDir: opencodeDir });
|
|
68
|
+
let opencodePluginHooked = Boolean(opencodeStatus.installed);
|
|
69
|
+
if (!opencodePluginHooked) {
|
|
70
|
+
try {
|
|
71
|
+
for (const name of fs.readdirSync(opencodeDir)) {
|
|
72
|
+
if (mentionsShim(readTextIfAny(path.join(opencodeDir, name)))) {
|
|
73
|
+
opencodePluginHooked = true;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} catch (_) {
|
|
78
|
+
/* no plugin dir */
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const zcodeFile = zcodeConfigFile || path.join(home, '.zcode', 'cli', 'config.json');
|
|
83
|
+
const zcodeStatus = await core.installers.zcode.statusZcodeHook({ homeDir, configFile: zcodeFile });
|
|
84
|
+
|
|
85
|
+
return [
|
|
86
|
+
{
|
|
87
|
+
id: 'claude-code',
|
|
88
|
+
label: 'Claude Code',
|
|
89
|
+
cliFound: commandExists('claude'),
|
|
90
|
+
configFound: claudeConfigFound,
|
|
91
|
+
hookInstalled: Boolean(claudeStatus.installed) || claudeTextHooked,
|
|
92
|
+
managed: Boolean(claudeStatus.installed),
|
|
93
|
+
managedEvents: claudeStatus.managedEvents || [],
|
|
94
|
+
detail: claudeStatus.thirdPartyCount ? `另有 ${claudeStatus.thirdPartyCount} 条第三方 hook(保留不动)` : null
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: 'codex',
|
|
98
|
+
label: 'Codex',
|
|
99
|
+
cliFound: commandExists('codex'),
|
|
100
|
+
configFound: codexConfigFound,
|
|
101
|
+
hookInstalled: Boolean(codexStatus.installed) || codexTextHooked,
|
|
102
|
+
managed: Boolean(codexStatus.installed),
|
|
103
|
+
detail: codexStatus.thirdParty ? 'notify 已被第三方/消费方链占用(转发链仍会调用 bridge)' : null
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: 'opencode',
|
|
107
|
+
label: 'OpenCode',
|
|
108
|
+
cliFound: commandExists('opencode'),
|
|
109
|
+
configFound: fileExists(path.join(path.dirname(opencodeDir), 'opencode.jsonc')) || fileExists(path.join(path.dirname(opencodeDir), 'opencode.json')),
|
|
110
|
+
hookInstalled: opencodePluginHooked,
|
|
111
|
+
managed: Boolean(opencodeStatus.installed),
|
|
112
|
+
detail: opencodeStatus.thirdPartyFiles && opencodeStatus.thirdPartyFiles.length
|
|
113
|
+
? `插件目录另有 ${opencodeStatus.thirdPartyFiles.length} 个第三方插件(保留不动)`
|
|
114
|
+
: null
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: 'zcode',
|
|
118
|
+
label: 'ZCode',
|
|
119
|
+
cliFound: commandExists('zcode'),
|
|
120
|
+
configFound: fileExists(zcodeFile),
|
|
121
|
+
hookInstalled: Boolean(zcodeStatus.installed),
|
|
122
|
+
managed: Boolean(zcodeStatus.installed),
|
|
123
|
+
detail: zcodeStatus.thirdPartyCount ? `配置中另有 ${zcodeStatus.thirdPartyCount} 条第三方 hook(保留不动)` : null
|
|
124
|
+
}
|
|
125
|
+
];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = { detectAgents, commandExists };
|
|
@@ -1,64 +1,64 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Bridge commit-boundary injection point. Any git hook (or a human, or a
|
|
6
|
-
* test) can signal "this repo just committed" by running:
|
|
7
|
-
*
|
|
8
|
-
* node commit-boundary.js --cwd <repo> [--home <bridge home>]
|
|
9
|
-
* [--sha <sha>] [--branch <name>] [--committed-at <iso>]
|
|
10
|
-
*
|
|
11
|
-
* The repo identity is resolved from the working tree (that is how the bridge
|
|
12
|
-
* knows which registered project — and therefore which store — the commit
|
|
13
|
-
* belongs to). Facts not provided are read from the repo's git. Output is a
|
|
14
|
-
* single JSON line on stdout; errors exit non-zero so callers can distinguish
|
|
15
|
-
* a failed injection from a skip.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
const { execFileSync } = require('child_process');
|
|
19
|
-
const { resolveRepoContext } = require('../core/repo-context');
|
|
20
|
-
const { createBridge } = require('../core/bridge');
|
|
21
|
-
|
|
22
|
-
function arg(name, fallback = '') {
|
|
23
|
-
const prefix = `--${name}`;
|
|
24
|
-
const argv = process.argv.slice(2);
|
|
25
|
-
const index = argv.indexOf(prefix);
|
|
26
|
-
if (index === -1) return fallback;
|
|
27
|
-
const value = argv[index + 1];
|
|
28
|
-
return value === undefined || value.startsWith('--') ? fallback : value;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function git(cwd, args) {
|
|
32
|
-
return String(execFileSync('git', args, { cwd, windowsHide: true, encoding: 'utf8' })).trim();
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async function main() {
|
|
36
|
-
const cwd = arg('cwd', process.cwd());
|
|
37
|
-
const repo = await resolveRepoContext(cwd);
|
|
38
|
-
if (!repo || !repo.repoIdentity) {
|
|
39
|
-
throw new Error(`not a Git work tree (no repo identity): ${cwd}`);
|
|
40
|
-
}
|
|
41
|
-
const commitSha = arg('sha') || git(cwd, ['rev-parse', 'HEAD']);
|
|
42
|
-
const branch = arg('branch') || git(cwd, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
43
|
-
const committedAt = arg('committed-at') || git(cwd, ['log', '-1', '--format=%cI']);
|
|
44
|
-
const subject = arg('subject') || git(cwd, ['log', '-1', '--format=%s']);
|
|
45
|
-
const homeDir = arg('home') || undefined;
|
|
46
|
-
const bridge = createBridge(homeDir ? { homeDir } : {});
|
|
47
|
-
const result = await bridge.appendCommitBoundary({
|
|
48
|
-
repoIdentity: repo.repoIdentity,
|
|
49
|
-
commitSha,
|
|
50
|
-
branch,
|
|
51
|
-
subject,
|
|
52
|
-
committedAt: committedAt || undefined
|
|
53
|
-
});
|
|
54
|
-
process.stdout.write(`${JSON.stringify({ ok: true, projectPath: repo.projectPath, ...result })}\n`);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
module.exports = { main };
|
|
58
|
-
|
|
59
|
-
if (require.main === module) {
|
|
60
|
-
main().catch((err) => {
|
|
61
|
-
process.stderr.write(`${JSON.stringify({ ok: false, error: err && err.message ? err.message : String(err) })}\n`);
|
|
62
|
-
process.exitCode = 1;
|
|
63
|
-
});
|
|
64
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Bridge commit-boundary injection point. Any git hook (or a human, or a
|
|
6
|
+
* test) can signal "this repo just committed" by running:
|
|
7
|
+
*
|
|
8
|
+
* node commit-boundary.js --cwd <repo> [--home <bridge home>]
|
|
9
|
+
* [--sha <sha>] [--branch <name>] [--committed-at <iso>]
|
|
10
|
+
*
|
|
11
|
+
* The repo identity is resolved from the working tree (that is how the bridge
|
|
12
|
+
* knows which registered project — and therefore which store — the commit
|
|
13
|
+
* belongs to). Facts not provided are read from the repo's git. Output is a
|
|
14
|
+
* single JSON line on stdout; errors exit non-zero so callers can distinguish
|
|
15
|
+
* a failed injection from a skip.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const { execFileSync } = require('child_process');
|
|
19
|
+
const { resolveRepoContext } = require('../core/repo-context');
|
|
20
|
+
const { createBridge } = require('../core/bridge');
|
|
21
|
+
|
|
22
|
+
function arg(name, fallback = '') {
|
|
23
|
+
const prefix = `--${name}`;
|
|
24
|
+
const argv = process.argv.slice(2);
|
|
25
|
+
const index = argv.indexOf(prefix);
|
|
26
|
+
if (index === -1) return fallback;
|
|
27
|
+
const value = argv[index + 1];
|
|
28
|
+
return value === undefined || value.startsWith('--') ? fallback : value;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function git(cwd, args) {
|
|
32
|
+
return String(execFileSync('git', args, { cwd, windowsHide: true, encoding: 'utf8' })).trim();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function main() {
|
|
36
|
+
const cwd = arg('cwd', process.cwd());
|
|
37
|
+
const repo = await resolveRepoContext(cwd);
|
|
38
|
+
if (!repo || !repo.repoIdentity) {
|
|
39
|
+
throw new Error(`not a Git work tree (no repo identity): ${cwd}`);
|
|
40
|
+
}
|
|
41
|
+
const commitSha = arg('sha') || git(cwd, ['rev-parse', 'HEAD']);
|
|
42
|
+
const branch = arg('branch') || git(cwd, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
43
|
+
const committedAt = arg('committed-at') || git(cwd, ['log', '-1', '--format=%cI']);
|
|
44
|
+
const subject = arg('subject') || git(cwd, ['log', '-1', '--format=%s']);
|
|
45
|
+
const homeDir = arg('home') || undefined;
|
|
46
|
+
const bridge = createBridge(homeDir ? { homeDir } : {});
|
|
47
|
+
const result = await bridge.appendCommitBoundary({
|
|
48
|
+
repoIdentity: repo.repoIdentity,
|
|
49
|
+
commitSha,
|
|
50
|
+
branch,
|
|
51
|
+
subject,
|
|
52
|
+
committedAt: committedAt || undefined
|
|
53
|
+
});
|
|
54
|
+
process.stdout.write(`${JSON.stringify({ ok: true, projectPath: repo.projectPath, ...result })}\n`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = { main };
|
|
58
|
+
|
|
59
|
+
if (require.main === module) {
|
|
60
|
+
main().catch((err) => {
|
|
61
|
+
process.stderr.write(`${JSON.stringify({ ok: false, error: err && err.message ? err.message : String(err) })}\n`);
|
|
62
|
+
process.exitCode = 1;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Capture-disable guard. Project-Knowledge (or any host) marks its internal
|
|
5
|
-
* AI SDK sessions with AI_CODING_EVENT_BRIDGE_CAPTURE=0 and
|
|
6
|
-
* AI_CODING_EVENT_ORIGIN. Every connector checks this FIRST — before
|
|
7
|
-
* normalization, repo Git resolution, or any journal write — so internal
|
|
8
|
-
* Workbench/Analyzer conversations never reach the durable journal.
|
|
9
|
-
*/
|
|
10
|
-
function isCaptureDisabled(env = process.env, payload = null) {
|
|
11
|
-
if (env && env.AI_CODING_EVENT_BRIDGE_CAPTURE === '0') {
|
|
12
|
-
return true;
|
|
13
|
-
}
|
|
14
|
-
if (payload && typeof payload === 'object' && payload.meta && typeof payload.meta === 'object') {
|
|
15
|
-
return String(payload.meta.bridgeCapture) === '0';
|
|
16
|
-
}
|
|
17
|
-
return false;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function captureDisabledResult() {
|
|
21
|
-
return { status: 'ignored', reason: 'capture-disabled' };
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
module.exports = { isCaptureDisabled, captureDisabledResult };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Capture-disable guard. Project-Knowledge (or any host) marks its internal
|
|
5
|
+
* AI SDK sessions with AI_CODING_EVENT_BRIDGE_CAPTURE=0 and
|
|
6
|
+
* AI_CODING_EVENT_ORIGIN. Every connector checks this FIRST — before
|
|
7
|
+
* normalization, repo Git resolution, or any journal write — so internal
|
|
8
|
+
* Workbench/Analyzer conversations never reach the durable journal.
|
|
9
|
+
*/
|
|
10
|
+
function isCaptureDisabled(env = process.env, payload = null) {
|
|
11
|
+
if (env && env.AI_CODING_EVENT_BRIDGE_CAPTURE === '0') {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
if (payload && typeof payload === 'object' && payload.meta && typeof payload.meta === 'object') {
|
|
15
|
+
return String(payload.meta.bridgeCapture) === '0';
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function captureDisabledResult() {
|
|
21
|
+
return { status: 'ignored', reason: 'capture-disabled' };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = { isCaptureDisabled, captureDisabledResult };
|
|
@@ -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 };
|