@sanqianx/ai-coding-event-bridge-console 0.4.0
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 -0
- package/README.md +63 -0
- package/console/index.html +921 -0
- package/package.json +29 -0
- package/src/agent-detect.js +128 -0
- package/src/bin.js +53 -0
- package/src/pick-folder.js +623 -0
- package/src/server.js +719 -0
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sanqianx/ai-coding-event-bridge-console",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Local console for the AI coding event bridge: project import with per-project journal storage, REST query API and a built-in conversation explorer page.",
|
|
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
|
+
"dependencies": {
|
|
27
|
+
"@sanqianx/ai-coding-event-bridge": "0.4.0"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +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('@sanqianx/ai-coding-event-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({ 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
|
+
settingsFile: claudeFiles[0]
|
|
56
|
+
});
|
|
57
|
+
const claudeConfigFound = claudeFiles.some(fileExists);
|
|
58
|
+
const claudeTextHooked = claudeFiles.some((file) => mentionsShim(readTextIfAny(file)));
|
|
59
|
+
|
|
60
|
+
const codexFile = codexConfigFile || path.join(home, '.codex', 'config.toml');
|
|
61
|
+
const codexStatus = await core.installers.codex.statusCodexNotify({ configFile: codexFile });
|
|
62
|
+
const codexConfigFound = fileExists(codexFile);
|
|
63
|
+
const codexTextHooked = mentionsShim(readTextIfAny(codexFile));
|
|
64
|
+
|
|
65
|
+
const opencodeDir = opencodePluginsDir || path.join(home, '.config', 'opencode', 'plugin');
|
|
66
|
+
const opencodeStatus = await core.installers.openCode.statusOpenCodePlugin({ pluginsDir: opencodeDir });
|
|
67
|
+
let opencodePluginHooked = Boolean(opencodeStatus.installed);
|
|
68
|
+
if (!opencodePluginHooked) {
|
|
69
|
+
try {
|
|
70
|
+
for (const name of fs.readdirSync(opencodeDir)) {
|
|
71
|
+
if (mentionsShim(readTextIfAny(path.join(opencodeDir, name)))) {
|
|
72
|
+
opencodePluginHooked = true;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} catch (_) {
|
|
77
|
+
/* no plugin dir */
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
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);
|
|
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) || mentionsShim(zcodeConfigText),
|
|
122
|
+
managed: Boolean(zcodeStatus.installed),
|
|
123
|
+
detail: zcodeStatus.thirdPartyCount ? `配置中另有 ${zcodeStatus.thirdPartyCount} 条第三方 hook(保留不动)` : null
|
|
124
|
+
}
|
|
125
|
+
];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = { detectAgents, commandExists };
|
package/src/bin.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const http = require('http');
|
|
5
|
+
const { bridgeHome } = require('@sanqianx/ai-coding-event-bridge');
|
|
6
|
+
const { createConsoleServer } = require('./server');
|
|
7
|
+
const { warmFolderPicker } = require('./pick-folder');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* ai-coding-event-bridge-console CLI
|
|
11
|
+
*
|
|
12
|
+
* serve [--port 8790] [--host 127.0.0.1] [--home DIR]
|
|
13
|
+
* Start the local console (explorer page + JSON API).
|
|
14
|
+
*
|
|
15
|
+
* The server binds to localhost by default: journals contain full
|
|
16
|
+
* conversation bodies, so they stay on this machine unless --host says
|
|
17
|
+
* otherwise.
|
|
18
|
+
*/
|
|
19
|
+
async function main() {
|
|
20
|
+
const argv = process.argv.slice(2);
|
|
21
|
+
const command = argv[0];
|
|
22
|
+
const get = (name) => {
|
|
23
|
+
const i = argv.indexOf('--' + name);
|
|
24
|
+
return i >= 0 ? argv[i + 1] : undefined;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
if (command !== 'serve') {
|
|
28
|
+
console.log('Usage: ai-coding-event-bridge-console serve [--port 8790] [--host 127.0.0.1] [--home DIR]');
|
|
29
|
+
process.exitCode = command ? 1 : 0;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const home = bridgeHome(get('home'));
|
|
34
|
+
const port = Number(get('port') || 8790);
|
|
35
|
+
const host = get('host') || '127.0.0.1';
|
|
36
|
+
const server = http.createServer(createConsoleServer({ home }));
|
|
37
|
+
await new Promise((resolve) => server.listen(port, host, resolve));
|
|
38
|
+
console.log(`ai-coding-event-bridge console serving on http://${host}:${port}`);
|
|
39
|
+
console.log(` home: ${home}`);
|
|
40
|
+
console.log(` import projects from the sidebar; conversations of imported`);
|
|
41
|
+
console.log(` projects are stored per-project under their chosen store.`);
|
|
42
|
+
// Compile the folder-picker helper up front so the first "browse…" click
|
|
43
|
+
// pops the dialog in milliseconds instead of paying the powershell +
|
|
44
|
+
// Add-Type startup. Never blocks or crashes the console.
|
|
45
|
+
if (process.env.BRIDGE_CONSOLE_PICK_FOLDER !== '0') {
|
|
46
|
+
warmFolderPicker({ title: '选择项目文件夹' }).catch(() => {});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
main().catch((err) => {
|
|
51
|
+
console.error(err && err.stack ? err.stack : err);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
});
|