@xyagent/cli 1.0.0 → 1.1.1
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/README.md +42 -0
- package/bin/agentlink +468 -86
- package/package.json +3 -3
- package/src/tunnel_service.mjs +17 -1
- package/src-ext/bin.mjs +12 -11
- package/src-ext/commands/agent.mjs +34 -0
- package/src-ext/commands/pair.mjs +122 -23
- package/src-ext/commands/service.mjs +1 -1
- package/src-ext/core/activeRuns.mjs +26 -9
- package/src-ext/core/defaultWorkspace.mjs +43 -13
- package/src-ext/core/defaultWorkspaceSync.mjs +20 -0
- package/src-ext/core/installationIdentity.mjs +94 -0
- package/src-ext/core/mcpRuntimeFanout.mjs +23 -1
- package/src-ext/core/pairCodeClient.mjs +48 -8
- package/src-ext/core/pairInventory.mjs +31 -6
- package/src-ext/core/relayWorker.mjs +21 -1
- package/src-ext/core/runtimeRegistry.mjs +180 -0
- package/src-ext/core/scanWorkspaces.mjs +163 -23
- package/src-ext/core/unifiedDispatchHandler.mjs +9 -2
- package/src-ext/postinstall.mjs +14 -0
- package/src-ext/runtime/_shared/bridgedSessionLedger.mjs +60 -0
- package/src-ext/runtime/_shared/claudeSchemaEvent.mjs +49 -0
- package/src-ext/runtime/_shared/headlessCliBridge.mjs +168 -0
- package/src-ext/runtime/_shared/jsonMcpConfigAdapter.mjs +70 -0
- package/src-ext/runtime/_shared/ndjsonProcess.mjs +141 -0
- package/src-ext/runtime/_shared/resolveWorkspaceCwd.mjs +47 -0
- package/src-ext/runtime/_shared/slashCommandRouter.mjs +10 -0
- package/src-ext/runtime/claude/handleRequest.mjs +15 -37
- package/src-ext/runtime/claude/stdoutParser.mjs +8 -3
- package/src-ext/runtime/codebuddy/index.mjs +41 -0
- package/src-ext/runtime/codex/handleRequest.mjs +13 -35
- package/src-ext/runtime/cursor/index.mjs +46 -0
- package/src-ext/runtime/cursor/mcpConfigAdapter.mjs +15 -0
- package/src-ext/runtime/deepagents/preflight.mjs +57 -0
- package/src-ext/runtime/hermes/envSetup.mjs +22 -8
- package/src-ext/runtime/hermes/gatewayManager.mjs +239 -3
- package/src-ext/runtime/hermes/handleRequest.mjs +13 -0
- package/src-ext/runtime/hermes/httpBackend.mjs +12 -1
- package/src-ext/runtime/hermes/index.mjs +1 -1
- package/src-ext/runtime/hermes/preflight.mjs +2 -1
- package/src-ext/runtime/kimi/index.mjs +100 -0
- package/src-ext/runtime/openclaw/workspaceContext.mjs +58 -0
- package/src-ext/runtime/opencode/index.mjs +48 -0
- package/src-ext/runtime/opencode/mcpConfigAdapter.mjs +15 -0
- package/src-ext/runtime/opencode/preflight.mjs +72 -0
- package/src-ext/runtime/qwen/index.mjs +42 -0
- package/src-ext/service/serviceManager.mjs +120 -42
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xyagent/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Relay-first CLI for pairing and bridging OpenClaw gateway traffic",
|
|
6
6
|
"type": "module",
|
|
@@ -18,12 +18,12 @@
|
|
|
18
18
|
"access": "public"
|
|
19
19
|
},
|
|
20
20
|
"engines": {
|
|
21
|
-
"node": ">=
|
|
21
|
+
"node": ">=18"
|
|
22
22
|
},
|
|
23
23
|
"scripts": {
|
|
24
24
|
"build:plugin": "node scripts/build-openclaw-plugin.mjs",
|
|
25
25
|
"prepublishOnly": "node scripts/build-openclaw-plugin.mjs",
|
|
26
|
-
"postinstall": "node
|
|
26
|
+
"postinstall": "node src-ext/postinstall.mjs",
|
|
27
27
|
"test": "node --test --test-concurrency=1 'test/**/*.test.mjs'",
|
|
28
28
|
"test:coverage": "node --test --experimental-test-coverage test/tunnel/core.test.mjs"
|
|
29
29
|
},
|
package/src/tunnel_service.mjs
CHANGED
|
@@ -4,6 +4,8 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import process from "node:process";
|
|
6
6
|
|
|
7
|
+
import { buildPathString, resolveSiblingBinDirs } from "./core.mjs";
|
|
8
|
+
|
|
7
9
|
export function startTunnelRuntime({
|
|
8
10
|
localPort,
|
|
9
11
|
relayUrl,
|
|
@@ -473,7 +475,7 @@ function getTunnelServiceStatus(localPort, runtime) {
|
|
|
473
475
|
};
|
|
474
476
|
}
|
|
475
477
|
|
|
476
|
-
function writeTunnelLaunchScript({ localPort, startArgs, nodePath, agentlinkHome }) {
|
|
478
|
+
export function writeTunnelLaunchScript({ localPort, startArgs, nodePath, agentlinkHome }) {
|
|
477
479
|
const scriptPath = resolveTunnelLaunchScriptPath(localPort);
|
|
478
480
|
fs.mkdirSync(path.dirname(scriptPath), { recursive: true });
|
|
479
481
|
const capturedHome = os.homedir();
|
|
@@ -482,12 +484,26 @@ function writeTunnelLaunchScript({ localPort, startArgs, nodePath, agentlinkHome
|
|
|
482
484
|
const appData =
|
|
483
485
|
(process.env.APPDATA ?? "").trim() ||
|
|
484
486
|
path.join(capturedHome, "AppData", "Roaming");
|
|
487
|
+
// ScheduledTask 的 PATH 是注册表值,看不到安装时进程里的增量(尤其是桌面端
|
|
488
|
+
// 私有 Node 的目录)—— 与 bridge 启动脚本同口径快照 PATH,`%` 转义防意外展开。
|
|
489
|
+
// 兄弟目录解析显式用 win32 语义(生产 win 上与默认 path 等价;
|
|
490
|
+
// 让 macOS 上跑的单测也能验证真实的反斜杠路径拆解)。
|
|
491
|
+
const defaultPath = buildPathString(
|
|
492
|
+
[
|
|
493
|
+
...resolveSiblingBinDirs(nodePath, path.win32),
|
|
494
|
+
path.join(capturedHome, ".volta", "bin"),
|
|
495
|
+
path.join(appData, "npm"),
|
|
496
|
+
"C:\\Program Files\\nodejs",
|
|
497
|
+
],
|
|
498
|
+
";",
|
|
499
|
+
);
|
|
485
500
|
const script = [
|
|
486
501
|
"@echo off",
|
|
487
502
|
`set "HOME=${capturedHome}"`,
|
|
488
503
|
`set "USERPROFILE=${capturedHome}"`,
|
|
489
504
|
`set "APPDATA=${appData}"`,
|
|
490
505
|
`set "AGENTLINK_HOME=${capturedAgentlinkHome}"`,
|
|
506
|
+
`set "PATH=${defaultPath.replaceAll("%", "%%")};%PATH%"`,
|
|
491
507
|
`"${nodePath}" ${startArgs.map((arg) => quoteWindowsCommandArg(arg)).join(" ")}`,
|
|
492
508
|
"",
|
|
493
509
|
].join("\r\n");
|
package/src-ext/bin.mjs
CHANGED
|
@@ -9,6 +9,14 @@ import { newCid } from "../src-shared/correlation.mjs";
|
|
|
9
9
|
import { runAgentCommand } from "./commands/agent.mjs";
|
|
10
10
|
import { runServiceCommand } from "./commands/service.mjs";
|
|
11
11
|
import { printHelp } from "./commands/help.mjs";
|
|
12
|
+
import { EXT_AGENT_RUNTIME_KINDS } from "./core/runtimeRegistry.mjs";
|
|
13
|
+
|
|
14
|
+
// `agentlink-agent <runtime>` 承载的 runtime 集合。hermes 也走这里(它的 bridge
|
|
15
|
+
// 就是 agentlink-agent hermes),所以不是直接用 EXT_AGENT_RUNTIME_KINDS,而是
|
|
16
|
+
// 在其之上加 hermes —— 用登记表派生,避免这里再多一份手抄的 runtime 名单。
|
|
17
|
+
const AGENT_SUBCOMMAND_RUNTIMES = Object.freeze([
|
|
18
|
+
...EXT_AGENT_RUNTIME_KINDS, "hermes",
|
|
19
|
+
]);
|
|
12
20
|
|
|
13
21
|
const PKG_NAME = "agentlink-agent";
|
|
14
22
|
|
|
@@ -83,19 +91,12 @@ async function main() {
|
|
|
83
91
|
node: process.version,
|
|
84
92
|
});
|
|
85
93
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
log.info("bridge.signal.recv", "SIGTERM", { signal: "SIGTERM" });
|
|
91
|
-
});
|
|
94
|
+
if (AGENT_SUBCOMMAND_RUNTIMES.includes(subcommand)) {
|
|
95
|
+
await runAgentCommand({ runtime: subcommand, options, positional, log });
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
92
98
|
|
|
93
99
|
switch (subcommand) {
|
|
94
|
-
case "claude":
|
|
95
|
-
case "codex":
|
|
96
|
-
case "hermes":
|
|
97
|
-
await runAgentCommand({ runtime: subcommand, options, positional, log });
|
|
98
|
-
return;
|
|
99
100
|
case "service":
|
|
100
101
|
await runServiceCommand({ options, positional, log });
|
|
101
102
|
return;
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { runClaudeBridge } from "../runtime/claude/index.mjs";
|
|
2
2
|
import { runCodexBridge } from "../runtime/codex/index.mjs";
|
|
3
3
|
import { runHermesBridge } from "../runtime/hermes/index.mjs";
|
|
4
|
+
import { runCursorBridge } from "../runtime/cursor/index.mjs";
|
|
5
|
+
import { runOpenCodeBridge } from "../runtime/opencode/index.mjs";
|
|
6
|
+
import { runQwenBridge } from "../runtime/qwen/index.mjs";
|
|
7
|
+
import { runKimiBridge } from "../runtime/kimi/index.mjs";
|
|
8
|
+
import { runCodeBuddyBridge } from "../runtime/codebuddy/index.mjs";
|
|
4
9
|
import { runPairInstall, runUninstall } from "./pair.mjs";
|
|
10
|
+
import { hasBridgeImplementation } from "../core/runtimeRegistry.mjs";
|
|
5
11
|
|
|
6
12
|
/**
|
|
7
13
|
* `agentlink-agent <runtime> [action] [args]` 分发。
|
|
@@ -32,6 +38,19 @@ export async function runAgentCommand({ runtime, options, positional, log }) {
|
|
|
32
38
|
return;
|
|
33
39
|
}
|
|
34
40
|
|
|
41
|
+
// 到这里说明用户要的是**前台 bridge**。配对能力与桥接能力是两层(见
|
|
42
|
+
// core/runtimeRegistry.mjs):opencode 本轮能配对、但没有 bridge 实现。
|
|
43
|
+
// 必须在这里显式说清楚,而不是掉进下面 switch 的 default 报一句含糊的
|
|
44
|
+
// "unsupported runtime"(用户刚配对成功,会以为是自己装错了)。
|
|
45
|
+
if (!hasBridgeImplementation(runtime)) {
|
|
46
|
+
rtLog.error("bridge.not_implemented", "runtime is pairable but has no bridge implementation", { runtime });
|
|
47
|
+
process.stderr.write(
|
|
48
|
+
`runtime \`${runtime}\` 本轮只支持配对(pair / uninstall),还没有 bridge 实现。\n` +
|
|
49
|
+
`配对凭证已可正常保存;等 bridge 落地后无需重新配对。\n`,
|
|
50
|
+
);
|
|
51
|
+
process.exit(2);
|
|
52
|
+
}
|
|
53
|
+
|
|
35
54
|
rtLog.info("bridge.preflight.pass", "runtime dispatcher entered", {
|
|
36
55
|
runtime,
|
|
37
56
|
args_count: positional.length,
|
|
@@ -47,6 +66,21 @@ export async function runAgentCommand({ runtime, options, positional, log }) {
|
|
|
47
66
|
case "hermes":
|
|
48
67
|
await runHermesBridge({ options, positional, log: rtLog });
|
|
49
68
|
return;
|
|
69
|
+
case "cursor":
|
|
70
|
+
await runCursorBridge({ options, positional, log: rtLog });
|
|
71
|
+
return;
|
|
72
|
+
case "opencode":
|
|
73
|
+
await runOpenCodeBridge({ options, positional, log: rtLog });
|
|
74
|
+
return;
|
|
75
|
+
case "qwen":
|
|
76
|
+
await runQwenBridge({ options, positional, log: rtLog });
|
|
77
|
+
return;
|
|
78
|
+
case "kimi":
|
|
79
|
+
await runKimiBridge({ options, positional, log: rtLog });
|
|
80
|
+
return;
|
|
81
|
+
case "codebuddy":
|
|
82
|
+
await runCodeBuddyBridge({ options, positional, log: rtLog });
|
|
83
|
+
return;
|
|
50
84
|
default:
|
|
51
85
|
rtLog.error("bridge.unknown_runtime", "unsupported runtime", { runtime });
|
|
52
86
|
process.stderr.write(`unsupported runtime: ${runtime}\n`);
|
|
@@ -26,13 +26,59 @@ import {
|
|
|
26
26
|
} from "../service/serviceManager.mjs";
|
|
27
27
|
import { ensureHermesApiServer } from "../runtime/hermes/envSetup.mjs";
|
|
28
28
|
import { manageHermesGateway } from "../runtime/hermes/gatewayManager.mjs";
|
|
29
|
-
import { ensureDefaultWorkspace } from "../core/defaultWorkspace.mjs";
|
|
29
|
+
import { ensureDefaultWorkspace, DEFAULT_WORKSPACE_RUNTIMES } from "../core/defaultWorkspace.mjs";
|
|
30
|
+
import { ensureRelayDefaultWorkspace } from "../core/defaultWorkspaceSync.mjs";
|
|
31
|
+
import { hasBridgeImplementation } from "../core/runtimeRegistry.mjs";
|
|
32
|
+
import { opencodePreflight } from "../runtime/opencode/preflight.mjs";
|
|
33
|
+
import { deepagentsPreflight } from "../runtime/deepagents/preflight.mjs";
|
|
34
|
+
import { qwenPreflight } from "../runtime/qwen/index.mjs";
|
|
35
|
+
import { kimiPreflight } from "../runtime/kimi/index.mjs";
|
|
36
|
+
import { codeBuddyPreflight } from "../runtime/codebuddy/index.mjs";
|
|
30
37
|
import os from "node:os";
|
|
31
38
|
import path from "node:path";
|
|
32
39
|
|
|
40
|
+
/**
|
|
41
|
+
* 配对**前**要跑的 runtime 本机自检。
|
|
42
|
+
*
|
|
43
|
+
* 只登记「除了配对之外没有别的地方会校验二进制」的 runtime:claude / codex 的
|
|
44
|
+
* 二进制会在 bridge 启动时被各自的 preflight 拦住,pair 阶段不重复探(保持既有行为);
|
|
45
|
+
* opencode 本轮没有 bridge,pair 是唯一的校验时机。
|
|
46
|
+
*
|
|
47
|
+
* 🔴 必须在 consumePairCode **之前**跑:配对码是一次性的,先消费再发现本机没装
|
|
48
|
+
* 会把用户的码白烧掉,只能回 App 重新出码。
|
|
49
|
+
*/
|
|
50
|
+
const PAIR_PREFLIGHTS = Object.freeze({
|
|
51
|
+
opencode: opencodePreflight,
|
|
52
|
+
deepagents: deepagentsPreflight,
|
|
53
|
+
qwen: qwenPreflight,
|
|
54
|
+
kimi: kimiPreflight,
|
|
55
|
+
codebuddy: codeBuddyPreflight,
|
|
56
|
+
});
|
|
57
|
+
|
|
33
58
|
export async function runPairInstall({ runtime, code, options, log }) {
|
|
34
59
|
const relay = resolveRelayUrl(options.relay);
|
|
35
60
|
const pLog = log.child({ comp: "pair" });
|
|
61
|
+
|
|
62
|
+
// 0. 本机自检(见 PAIR_PREFLIGHTS 注释:必须早于 consume)
|
|
63
|
+
const preflight = PAIR_PREFLIGHTS[runtime];
|
|
64
|
+
if (preflight) {
|
|
65
|
+
const pf = await preflight({ log: pLog });
|
|
66
|
+
if (!pf.ok) {
|
|
67
|
+
pLog.error("pair.preflight.failed", "runtime preflight failed before consume", {
|
|
68
|
+
runtime, err: pf.error || "",
|
|
69
|
+
});
|
|
70
|
+
process.stderr.write(
|
|
71
|
+
`配对失败:本机 ${runtime} 不可用。\n` +
|
|
72
|
+
` ${pf.hint || pf.error || "runtime 预检未通过"}\n` +
|
|
73
|
+
` 配对码未被消费,装好后可用同一个码重试。\n`,
|
|
74
|
+
);
|
|
75
|
+
process.exit(65);
|
|
76
|
+
}
|
|
77
|
+
pLog.info("pair.preflight.ok", "runtime preflight passed", {
|
|
78
|
+
runtime, version: pf.version || "",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
36
82
|
pLog.info("pair.consume.start", "consuming pair code", { code, relay });
|
|
37
83
|
|
|
38
84
|
// 1. consume
|
|
@@ -82,9 +128,16 @@ export async function runPairInstall({ runtime, code, options, log }) {
|
|
|
82
128
|
// 2.6 align-runtime-default-workspace: 对 claude/codex 配对时 eager 创建默认工作区。
|
|
83
129
|
// 这样新装用户配对完、打开 App 导入列表时,默认工作区已物理存在。
|
|
84
130
|
// openclaw/hermes 有原生默认工作区(openclaw.json / profileHome),跳过。
|
|
85
|
-
if (runtime
|
|
131
|
+
if (DEFAULT_WORKSPACE_RUNTIMES.includes(runtime)) {
|
|
86
132
|
try {
|
|
87
|
-
const
|
|
133
|
+
const profileName = runtime === "hermes" ? pairStateData.hermesProfileName : undefined;
|
|
134
|
+
const wsPath = await ensureDefaultWorkspace(runtime, { profileName });
|
|
135
|
+
await ensureRelayDefaultWorkspace({
|
|
136
|
+
runtime,
|
|
137
|
+
relayUrl: relay,
|
|
138
|
+
bridgeToken: pairStateData.bridge_token,
|
|
139
|
+
profileName,
|
|
140
|
+
});
|
|
88
141
|
pLog.info("pair.default_workspace.created", "default workspace ensured", {
|
|
89
142
|
runtime, path: wsPath,
|
|
90
143
|
});
|
|
@@ -96,6 +149,19 @@ export async function runPairInstall({ runtime, code, options, log }) {
|
|
|
96
149
|
}
|
|
97
150
|
}
|
|
98
151
|
|
|
152
|
+
// 2.7 无 bridge 实现的 runtime(见 core/runtimeRegistry.mjs 的 PAIR_ONLY):
|
|
153
|
+
// 配对到此为止。**不能**继续装 service —— service 里跑的是
|
|
154
|
+
// `agentlink-agent <runtime>`,没有 bridge 实现时它会立刻退出,只会给用户留下
|
|
155
|
+
// 一个反复重启的空转 launchd/systemd 单元 + 一句吓人的「进程 3 秒内未运行起来」。
|
|
156
|
+
// 也不做 MCP fanout(本轮没有对应适配器)。凭证已落地,bridge 落地后无需重配。
|
|
157
|
+
if (!hasBridgeImplementation(runtime)) {
|
|
158
|
+
pLog.info("pair.bridge.not_implemented", "paired; skipping service install (no bridge yet)", { runtime });
|
|
159
|
+
process.stdout.write(
|
|
160
|
+
`ℹ️ ${runtime} 本轮只支持配对:凭证已保存,后台服务与 MCP 配置留待 bridge 实现后接入。\n`,
|
|
161
|
+
);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
99
165
|
// 3. 装 service(可被 --no-service / --manual 跳过)
|
|
100
166
|
if (options["no-service"] || options.manual) {
|
|
101
167
|
process.stdout.write("⚠️ 已跳过后台服务安装(--no-service)\n");
|
|
@@ -230,7 +296,8 @@ export async function ensureHermesEnvAndProbe(log, opts = {}) {
|
|
|
230
296
|
// 一套默认值 127.0.0.1:8642 在 listen,append 默认值不改行为)。
|
|
231
297
|
restartMatters =
|
|
232
298
|
r.overridden.length > 0 ||
|
|
233
|
-
r.appended.
|
|
299
|
+
r.appended.some((key) =>
|
|
300
|
+
key === "API_SERVER_ENABLED" || key === "API_SERVER_KEY");
|
|
234
301
|
} else if (r.kept.length > 0) {
|
|
235
302
|
// status === "ok",但仍然把用户已设的 host/port 列出来给用户确认
|
|
236
303
|
const keepStr = r.kept.map(({ key, value }) => `${key}=${value}`).join(", ");
|
|
@@ -408,33 +475,65 @@ export async function runUninstall({ runtime, options, log }) {
|
|
|
408
475
|
// 1.5 撤 MCP fanout —— 把 ~/.claude.json / ~/.codex/config.toml /
|
|
409
476
|
// ~/.hermes/config.yaml 里 agentlink server entry 删掉(用户其它 MCP server
|
|
410
477
|
// 一字不动)。失败不致命,后面 service / state 清理继续。
|
|
478
|
+
//
|
|
479
|
+
// 两个清理各有各的闸,判据**不共用**:
|
|
480
|
+
// · MCP 撤销 → 有没有 MCP 适配器(listAdapters()/hasMcpAdapter,唯一真相在
|
|
481
|
+
// mcpRuntimeFanout 的 ADAPTERS 表)。没有适配器就没写过配置,deregister
|
|
482
|
+
// 只会回一条 unknown_runtime 的伪失败告警。
|
|
483
|
+
// · service 卸载 → 有没有 bridge 实现(PAIR_ONLY runtime 压根没装过 service,
|
|
484
|
+
// serviceUninstall 会撞「runtime must be one of ...」)。
|
|
485
|
+
// 曾经两者都挂在 hasBridgeImplementation 上:当前两集合恰好相同所以看不出问题,
|
|
486
|
+
// 但下一轮给 opencode 补上 MCP 适配器时(bridge 仍未实现),共用判据会让新写的
|
|
487
|
+
// 适配器被静默跳过。拆开后各自跟着自己的真相源走。
|
|
488
|
+
const bridgeCapable = hasBridgeImplementation(runtime);
|
|
489
|
+
// 动态 import 保持原样(adapters 会拉进 vendored TOML/YAML,不该为一次卸载常驻);
|
|
490
|
+
// 加载失败按「没有适配器」处理 —— 模块都进不来,deregister 更不可能成功。
|
|
491
|
+
let mcpCapable = false;
|
|
411
492
|
try {
|
|
412
|
-
const {
|
|
413
|
-
|
|
414
|
-
projectCwd: process.cwd(),
|
|
415
|
-
log: pLog,
|
|
416
|
-
});
|
|
417
|
-
if (r.ok && r.changed) {
|
|
418
|
-
process.stdout.write(`✅ 已从 ${runtime} 移除 agentlink MCP server\n`);
|
|
419
|
-
} else if (!r.ok) {
|
|
420
|
-
process.stderr.write(`⚠️ MCP 配置清理失败 (${r.reason || "unknown"});其它清理继续。\n`);
|
|
421
|
-
}
|
|
493
|
+
const { hasMcpAdapter } = await import("../core/mcpRuntimeFanout.mjs");
|
|
494
|
+
mcpCapable = hasMcpAdapter(runtime);
|
|
422
495
|
} catch (err) {
|
|
423
|
-
pLog.warn("uninstall.
|
|
496
|
+
pLog.warn("uninstall.mcp.probe_failed", "cannot load mcp fanout module; skipping mcp cleanup", {
|
|
424
497
|
err: err?.message || String(err),
|
|
425
498
|
});
|
|
426
499
|
}
|
|
500
|
+
if (!bridgeCapable) {
|
|
501
|
+
pLog.info("uninstall.bridge.not_implemented", "skipping service cleanup (no bridge for this runtime)", { runtime });
|
|
502
|
+
}
|
|
503
|
+
if (!mcpCapable) {
|
|
504
|
+
pLog.info("uninstall.mcp.no_adapter", "skipping mcp cleanup (no mcp adapter for this runtime)", { runtime });
|
|
505
|
+
}
|
|
506
|
+
if (mcpCapable) {
|
|
507
|
+
try {
|
|
508
|
+
const { deregister } = await import("../core/mcpRuntimeFanout.mjs");
|
|
509
|
+
const r = await deregister(runtime, {
|
|
510
|
+
projectCwd: process.cwd(),
|
|
511
|
+
log: pLog,
|
|
512
|
+
});
|
|
513
|
+
if (r.ok && r.changed) {
|
|
514
|
+
process.stdout.write(`✅ 已从 ${runtime} 移除 agentlink MCP server\n`);
|
|
515
|
+
} else if (!r.ok) {
|
|
516
|
+
process.stderr.write(`⚠️ MCP 配置清理失败 (${r.reason || "unknown"});其它清理继续。\n`);
|
|
517
|
+
}
|
|
518
|
+
} catch (err) {
|
|
519
|
+
pLog.warn("uninstall.fanout.failed", "mcp deregister failed", {
|
|
520
|
+
err: err?.message || String(err),
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
}
|
|
427
524
|
|
|
428
525
|
// 2. 卸 service (launchd plist / systemd-user / schtasks)
|
|
429
526
|
let serviceResult = null;
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
527
|
+
if (bridgeCapable) {
|
|
528
|
+
try {
|
|
529
|
+
serviceResult = serviceUninstall(runtime, pLog);
|
|
530
|
+
pLog.info("uninstall.service.done", "service uninstalled", serviceResult);
|
|
531
|
+
} catch (err) {
|
|
532
|
+
pLog.warn("uninstall.service.failed", "service uninstall failed", {
|
|
533
|
+
err: err?.message || String(err),
|
|
534
|
+
});
|
|
535
|
+
process.stderr.write(`⚠️ service uninstall: ${err?.message || err}\n`);
|
|
536
|
+
}
|
|
438
537
|
}
|
|
439
538
|
|
|
440
539
|
// 3. 清本地状态文件(除非用户显式保留)
|
|
@@ -14,7 +14,7 @@ export async function runServiceCommand({ options, positional, log }) {
|
|
|
14
14
|
const action = (positional[0] || "").toLowerCase();
|
|
15
15
|
const runtime = options.runtime || options.r || positional[1] || "";
|
|
16
16
|
if (!runtime) {
|
|
17
|
-
process.stderr.write("service 子命令需要 -r claude|codex\n");
|
|
17
|
+
process.stderr.write("service 子命令需要 -r claude|codex|cursor|opencode|qwen|kimi|codebuddy\n");
|
|
18
18
|
process.exit(2);
|
|
19
19
|
}
|
|
20
20
|
const svcLog = log.child({ comp: "service", rt: runtime });
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
/**
|
|
12
12
|
* @returns {{
|
|
13
13
|
* register: (entry: {sessionKey?: string|null, threadId?: string|null, runId?: string|null, kill: (reason?: string) => void}) => () => void,
|
|
14
|
-
* abort: (params?: {key?: string, runId?: string}) => {aborted: number, matched: string[]},
|
|
14
|
+
* abort: (params?: {key?: string, runId?: string}) => {aborted: number, matched: string[], status?: string},
|
|
15
|
+
* abortAll: (reason?: string) => {aborted: number, matched: string[]},
|
|
15
16
|
* size: () => number,
|
|
16
17
|
* }}
|
|
17
18
|
*/
|
|
@@ -36,8 +37,7 @@ export function createActiveRunRegistry({ log } = {}) {
|
|
|
36
37
|
* 杀掉匹配的活跃 run。匹配顺序:
|
|
37
38
|
* 1. runId 精确命中
|
|
38
39
|
* 2. key 命中 sessionKey 或 threadId
|
|
39
|
-
*
|
|
40
|
-
* 单会话场景下用户意图无歧义)
|
|
40
|
+
* 旧协议只有 key 时必须唯一命中;没有命中或命中多个都不误杀。
|
|
41
41
|
*/
|
|
42
42
|
function abort({ key, runId } = {}) {
|
|
43
43
|
const wantRun = String(runId || "").trim();
|
|
@@ -51,11 +51,12 @@ export function createActiveRunRegistry({ log } = {}) {
|
|
|
51
51
|
([, r]) => r.sessionKey === wantKey || r.threadId === wantKey,
|
|
52
52
|
);
|
|
53
53
|
}
|
|
54
|
-
if (
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
if (!wantRun && targets.length > 1) {
|
|
55
|
+
log?.warn?.("activeRuns.abort.ambiguous", "legacy abort key matched multiple runs", {
|
|
56
|
+
key: wantKey,
|
|
57
|
+
matched: targets.map(([, run]) => run.runId || run.threadId),
|
|
58
|
+
});
|
|
59
|
+
return { aborted: 0, matched: [], status: "ambiguous" };
|
|
59
60
|
}
|
|
60
61
|
const matched = [];
|
|
61
62
|
for (const [id, r] of targets) {
|
|
@@ -72,5 +73,21 @@ export function createActiveRunRegistry({ log } = {}) {
|
|
|
72
73
|
return { aborted: matched.length, matched };
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
|
|
76
|
+
function abortAll(reason = "shutdown") {
|
|
77
|
+
const matched = [];
|
|
78
|
+
for (const [id, run] of [...runs.entries()]) {
|
|
79
|
+
runs.delete(id);
|
|
80
|
+
matched.push(run.runId || run.threadId || String(id));
|
|
81
|
+
try {
|
|
82
|
+
run.kill(reason);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
log?.warn?.("activeRuns.abort_all.kill_failed", "kill threw", {
|
|
85
|
+
runId: run.runId, err: err?.message || String(err),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return { aborted: matched.length, matched };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { register, abort, abortAll, size: () => runs.size };
|
|
76
93
|
}
|
|
@@ -1,37 +1,62 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* defaultWorkspace.mjs —
|
|
2
|
+
* defaultWorkspace.mjs — runtime 默认工作区路径管理
|
|
3
3
|
*
|
|
4
4
|
* 提供三个函数:
|
|
5
|
-
* workspacesRoot() —
|
|
5
|
+
* workspacesRoot() — 工作区根目录(${AGENTLINK_HOME}/workspaces),供 default + 用户新建工作区共用
|
|
6
6
|
* defaultWorkspacePath(runtime) — 纯路径计算,不建目录(供 scan 合成 entry 用)
|
|
7
7
|
* ensureDefaultWorkspace(runtime) — mkdir -p 后返回路径(pair 时 eager 创建 + dispatch 懒建)
|
|
8
8
|
*
|
|
9
9
|
* 路径结构:<workspacesRoot()>/<runtime>/default
|
|
10
|
-
* 例如
|
|
11
|
-
*
|
|
10
|
+
* 例如 ~/.agentlink/workspaces/claude/default
|
|
11
|
+
* ~/.agentlink/workspaces/codex/default
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* 代码的项目根,后者是工具自身状态,二者不能混在一起(参 openclaw 官方同款建议:
|
|
16
|
-
* workspace 要与 ~/.openclaw 分离)。用户新建工作区走 <workspacesRoot()>/<runtime>/<name>。
|
|
13
|
+
* 工作区根统一位于状态根的 `workspaces/` 子目录,避免散落到用户主目录;凭证和
|
|
14
|
+
* session 仍保留在状态根的其它文件中。用户新建工作区走 <workspacesRoot()>/<runtime>/<name>。
|
|
17
15
|
*
|
|
18
16
|
* 改名 agentlink 时只需改 workspacesRoot() 的返回值(统一来源)。
|
|
19
17
|
*/
|
|
20
18
|
|
|
21
19
|
import fs from "node:fs/promises";
|
|
20
|
+
import fsSync from "node:fs";
|
|
22
21
|
import os from "node:os";
|
|
23
22
|
import path from "node:path";
|
|
24
23
|
|
|
25
24
|
import { ensureGitRepo } from "./ensureGitRepo.mjs";
|
|
26
25
|
|
|
26
|
+
export const DEFAULT_WORKSPACE_RUNTIMES = Object.freeze([
|
|
27
|
+
"openclaw", "hermes", "claude", "codex", "cursor", "opencode", "qwen", "kimi", "codebuddy",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
function agentlinkHome() {
|
|
31
|
+
return String(process.env.AGENTLINK_HOME || "").trim() || path.join(os.homedir(), ".agentlink");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function openclawDefaultWorkspace() {
|
|
35
|
+
const home = String(process.env.OPENCLAW_HOME || "").trim() || path.join(os.homedir(), ".openclaw");
|
|
36
|
+
const configPath = path.join(home, "openclaw.json");
|
|
37
|
+
try {
|
|
38
|
+
const parsed = JSON.parse(fsSync.readFileSync(configPath, "utf8"));
|
|
39
|
+
const configured = String(parsed?.agents?.defaults?.workspace || "").trim();
|
|
40
|
+
if (configured) return path.isAbsolute(configured) ? configured : path.resolve(home, configured);
|
|
41
|
+
} catch {}
|
|
42
|
+
return path.join(home, "workspace");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function hermesDefaultWorkspace(profileName) {
|
|
46
|
+
const configured = String(process.env.HERMES_HOME || "").trim();
|
|
47
|
+
if (configured) return path.resolve(configured.startsWith("~") ? path.join(os.homedir(), configured.slice(1)) : configured);
|
|
48
|
+
if (String(profileName || "").trim()) return path.join(os.homedir(), ".hermes", "profiles", String(profileName).trim());
|
|
49
|
+
return path.join(os.homedir(), ".hermes");
|
|
50
|
+
}
|
|
51
|
+
|
|
27
52
|
/**
|
|
28
|
-
*
|
|
53
|
+
* 返回工作区根目录的绝对路径(${AGENTLINK_HOME}/workspaces),不建目录。
|
|
29
54
|
* default 与用户新建工作区都挂在它下面:<root>/<runtime>/<name>。
|
|
30
55
|
*
|
|
31
56
|
* @returns {string} 绝对路径
|
|
32
57
|
*/
|
|
33
58
|
export function workspacesRoot() {
|
|
34
|
-
return path.join(
|
|
59
|
+
return path.join(agentlinkHome(), "workspaces");
|
|
35
60
|
}
|
|
36
61
|
|
|
37
62
|
/**
|
|
@@ -40,7 +65,12 @@ export function workspacesRoot() {
|
|
|
40
65
|
* @param {string} runtime "claude" | "codex"
|
|
41
66
|
* @returns {string} 绝对路径
|
|
42
67
|
*/
|
|
43
|
-
export function defaultWorkspacePath(runtime) {
|
|
68
|
+
export function defaultWorkspacePath(runtime, { profileName } = {}) {
|
|
69
|
+
if (!DEFAULT_WORKSPACE_RUNTIMES.includes(runtime)) {
|
|
70
|
+
throw new Error(`unsupported default workspace runtime: ${runtime}`);
|
|
71
|
+
}
|
|
72
|
+
if (runtime === "openclaw") return openclawDefaultWorkspace();
|
|
73
|
+
if (runtime === "hermes") return hermesDefaultWorkspace(profileName);
|
|
44
74
|
return path.join(workspacesRoot(), runtime, "default");
|
|
45
75
|
}
|
|
46
76
|
|
|
@@ -55,8 +85,8 @@ export function defaultWorkspacePath(runtime) {
|
|
|
55
85
|
* @param {{ _execFile?: Function }} [opts] 测试注入
|
|
56
86
|
* @returns {Promise<string>} 绝对路径
|
|
57
87
|
*/
|
|
58
|
-
export async function ensureDefaultWorkspace(runtime, { _execFile } = {}) {
|
|
59
|
-
const p = defaultWorkspacePath(runtime);
|
|
88
|
+
export async function ensureDefaultWorkspace(runtime, { _execFile, profileName } = {}) {
|
|
89
|
+
const p = defaultWorkspacePath(runtime, { profileName });
|
|
60
90
|
await fs.mkdir(p, { recursive: true, mode: 0o700 });
|
|
61
91
|
if (runtime === "codex") {
|
|
62
92
|
await ensureGitRepo(p, { _execFile });
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { defaultWorkspacePath } from "./defaultWorkspace.mjs";
|
|
2
|
+
|
|
3
|
+
function baseUrl(value) {
|
|
4
|
+
return String(value || "").replace(/\/+$/, "");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export async function ensureRelayDefaultWorkspace({ runtime, relayUrl, bridgeToken, profileName, fetchImpl = globalThis.fetch }) {
|
|
8
|
+
if (typeof fetchImpl !== "function") throw new Error("default workspace sync: fetch unavailable");
|
|
9
|
+
const workspacePath = defaultWorkspacePath(runtime, { profileName });
|
|
10
|
+
const response = await fetchImpl(`${baseUrl(relayUrl)}/v1/gateways/me/default_workspace`, {
|
|
11
|
+
method: "PUT",
|
|
12
|
+
headers: {
|
|
13
|
+
"Content-Type": "application/json",
|
|
14
|
+
"X-Agent-Bridge-Token": String(bridgeToken || ""),
|
|
15
|
+
},
|
|
16
|
+
body: JSON.stringify({ workspace_path: workspacePath, workspace_name: "default" }),
|
|
17
|
+
});
|
|
18
|
+
if (!response.ok) throw new Error(`default workspace sync failed: HTTP ${response.status}`);
|
|
19
|
+
return response.json();
|
|
20
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
|
|
6
|
+
import { CLI_STATE_DIR_NAME } from "./attachAuditLog.mjs";
|
|
7
|
+
|
|
8
|
+
const INSTALLATION_ID_FILE = "installation-identity.json";
|
|
9
|
+
const INSTALLATION_ID_VERSION = 1;
|
|
10
|
+
const PRIVATE_DIRECTORY_MODE = 0o700;
|
|
11
|
+
const PRIVATE_FILE_MODE = 0o600;
|
|
12
|
+
const UUID_V4_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
13
|
+
|
|
14
|
+
function installationStateDir() {
|
|
15
|
+
const configured = process.env.AGENTLINK_HOME?.trim();
|
|
16
|
+
return configured || path.join(os.homedir(), CLI_STATE_DIR_NAME);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function installationIdentityFile() {
|
|
20
|
+
return path.join(installationStateDir(), INSTALLATION_ID_FILE);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function parseInstallationIdentity(raw) {
|
|
24
|
+
let value;
|
|
25
|
+
try {
|
|
26
|
+
value = JSON.parse(raw);
|
|
27
|
+
} catch {
|
|
28
|
+
throw new Error("installation identity file is invalid JSON");
|
|
29
|
+
}
|
|
30
|
+
const installationId = String(value?.installation_id || "").trim();
|
|
31
|
+
if (value?.version !== INSTALLATION_ID_VERSION || !UUID_V4_RE.test(installationId)) {
|
|
32
|
+
throw new Error("installation identity file has an invalid schema");
|
|
33
|
+
}
|
|
34
|
+
return installationId;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readExistingIdentity(file) {
|
|
38
|
+
try {
|
|
39
|
+
return parseInstallationIdentity(fs.readFileSync(file, "utf8"));
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (error?.code === "ENOENT") return null;
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Return one random, non-hardware installation identity shared by every
|
|
48
|
+
* runtime in this CLI data directory. A hard-link publication makes creation
|
|
49
|
+
* no-clobber and atomic even when multiple runtime services boot together.
|
|
50
|
+
*/
|
|
51
|
+
export function getOrCreateInstallationId() {
|
|
52
|
+
const dir = installationStateDir();
|
|
53
|
+
const file = installationIdentityFile();
|
|
54
|
+
fs.mkdirSync(dir, { recursive: true, mode: PRIVATE_DIRECTORY_MODE });
|
|
55
|
+
const existing = readExistingIdentity(file);
|
|
56
|
+
if (existing) return existing;
|
|
57
|
+
|
|
58
|
+
const installationId = randomUUID();
|
|
59
|
+
const temporary = path.join(
|
|
60
|
+
dir,
|
|
61
|
+
`.${INSTALLATION_ID_FILE}.${process.pid}.${randomUUID()}.tmp`,
|
|
62
|
+
);
|
|
63
|
+
const payload = `${JSON.stringify({
|
|
64
|
+
version: INSTALLATION_ID_VERSION,
|
|
65
|
+
installation_id: installationId,
|
|
66
|
+
created_at: new Date().toISOString(),
|
|
67
|
+
}, null, 2)}\n`;
|
|
68
|
+
|
|
69
|
+
let descriptor;
|
|
70
|
+
try {
|
|
71
|
+
descriptor = fs.openSync(temporary, "wx", PRIVATE_FILE_MODE);
|
|
72
|
+
fs.writeFileSync(descriptor, payload, "utf8");
|
|
73
|
+
fs.fsyncSync(descriptor);
|
|
74
|
+
fs.closeSync(descriptor);
|
|
75
|
+
descriptor = undefined;
|
|
76
|
+
try {
|
|
77
|
+
fs.linkSync(temporary, file);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (error?.code !== "EEXIST") throw error;
|
|
80
|
+
}
|
|
81
|
+
} finally {
|
|
82
|
+
if (descriptor !== undefined) {
|
|
83
|
+
try { fs.closeSync(descriptor); } catch {}
|
|
84
|
+
}
|
|
85
|
+
try { fs.unlinkSync(temporary); } catch {}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return readExistingIdentity(file);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export const _test = Object.freeze({
|
|
92
|
+
installationIdentityFile,
|
|
93
|
+
parseInstallationIdentity,
|
|
94
|
+
});
|