@xyagent/cli 0.0.1 → 1.1.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/README.md +42 -0
- package/bin/agentlink +1002 -245
- package/bin/agentlink-agent +0 -0
- package/bin/agentlink-mcp-stdio +0 -0
- package/package.json +5 -3
- package/src/core.mjs +26 -0
- package/src/tunnel_service.mjs +17 -1
- package/src-ext/bin.mjs +12 -12
- package/src-ext/commands/agent.mjs +33 -10
- 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/agentlinkToolsBootstrap.mjs +1 -1
- package/src-ext/core/autoTunnelDetector.mjs +6 -1
- package/src-ext/core/autoTunnelWiring.mjs +1 -1
- package/src-ext/core/bridgeSelfUninstall.mjs +1 -1
- 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 +22 -2
- package/src-ext/core/runtimeRegistry.mjs +180 -0
- package/src-ext/core/scanDeeplink.mjs +67 -0
- package/src-ext/core/scanPairFlow.mjs +28 -0
- package/src-ext/core/scanQrRenderer.mjs +40 -0
- package/src-ext/core/scanWorkspaces.mjs +163 -23
- package/src-ext/core/unifiedDispatchHandler.mjs +12 -5
- package/src-ext/core/usageReporter.mjs +1 -2
- package/src-ext/openclaw-plugin/envelope-builder.cjs +2 -2
- package/src-ext/openclaw-plugin/todoTranslatorUtils.cjs +1 -1
- 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/relayObjectToBlock.mjs +9 -11
- package/src-ext/runtime/_shared/resolveWorkspaceCwd.mjs +47 -0
- package/src-ext/runtime/_shared/slashCommandRouter.mjs +10 -0
- package/src-ext/runtime/_shared/todoTranslatorUtils.mjs +1 -1
- package/src-ext/runtime/claude/handleRequest.mjs +15 -37
- package/src-ext/runtime/claude/launcher.mjs +0 -6
- 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/buildOpenclawDaemonInput.mjs +0 -6
- 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/src-shared/envelope_builder.mjs +2 -2
- package/src-ext/runtime/picoclaw/constants.mjs +0 -39
- package/src-ext/runtime/picoclaw/handleRequest.mjs +0 -289
- package/src-ext/runtime/picoclaw/index.mjs +0 -314
- package/src-ext/runtime/picoclaw/pairFlow.mjs +0 -224
- package/src-ext/runtime/picoclaw/state.mjs +0 -78
- package/src-ext/runtime/picoclaw/todoTranslator.mjs +0 -67
- package/src-ext/runtime/picoclaw/translator.mjs +0 -272
- package/src-ext/runtime/picoclaw/wsClient.mjs +0 -129
package/bin/agentlink-agent
CHANGED
|
File without changes
|
package/bin/agentlink-mcp-stdio
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xyagent/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Relay-first CLI for pairing and bridging OpenClaw gateway traffic",
|
|
6
6
|
"type": "module",
|
|
@@ -18,7 +18,7 @@
|
|
|
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",
|
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
"agentlink-mcp-stdio": "bin/agentlink-mcp-stdio"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"pdfjs-dist": "^4.10.38"
|
|
36
|
+
"pdfjs-dist": "^4.10.38",
|
|
37
|
+
"qrcode": "^1.5.4",
|
|
38
|
+
"qrcode-terminal": "^0.12.0"
|
|
37
39
|
}
|
|
38
40
|
}
|
package/src/core.mjs
CHANGED
|
@@ -80,6 +80,16 @@ export function normalizeProvidedCode(raw, onInvalid = throwError) {
|
|
|
80
80
|
if (/^\d{4}$/.test(compact)) return compact;
|
|
81
81
|
if (/^\d{6}$/.test(compact)) return compact;
|
|
82
82
|
if (/^[A-Z0-9]{8}$/.test(compact)) return formatCode(compact);
|
|
83
|
+
// High-entropy scan code (`agentlink scan`, unify-scan-pairing): generated
|
|
84
|
+
// locally by the CLI via generateScanPairCode(), never hand-typed by a user
|
|
85
|
+
// (QR/deeplink transport only) — so no dash-grouping is applied. Contract
|
|
86
|
+
// with go_relay (real-device P0 fix): EXACTLY 16 uppercase hex chars
|
|
87
|
+
// (64 bits) — go_relay's `pair_sessions.pair_code` column is `varchar(16)`
|
|
88
|
+
// (no DDL), and go_relay's own validation was tightened to the same
|
|
89
|
+
// `^[0-9A-F]{16}$`. Do not widen this range again without a matching DDL —
|
|
90
|
+
// a wider CLI-side accept here previously let an 18-char/72-bit code
|
|
91
|
+
// through that overflowed the column (HTTP 500 "Data too long").
|
|
92
|
+
if (/^[A-Z0-9]{16}$/.test(compact)) return compact;
|
|
83
93
|
return onInvalid(
|
|
84
94
|
"invalid --code format. expected 4 digits (e.g. 1234), 6 digits (e.g. 123456), " +
|
|
85
95
|
"or 8 alphanumeric characters (e.g. ABCD-1234; case-insensitive, dash optional)",
|
|
@@ -92,6 +102,22 @@ export function generatePairCode(random = Math.random, numericCodeLength = NUMER
|
|
|
92
102
|
return String(min + Math.floor(random() * (max - min)));
|
|
93
103
|
}
|
|
94
104
|
|
|
105
|
+
// High-entropy pair code for `agentlink scan` (unify-scan-pairing spec's "安全
|
|
106
|
+
// 信封" §1: >=64-bit entropy, cryptographic random source, unenumerable).
|
|
107
|
+
// EXACTLY 8 random bytes -> 16 uppercase hex chars = 64 bits — pinned to this
|
|
108
|
+
// exact length to fit go_relay's `pair_sessions.pair_code varchar(16)` column
|
|
109
|
+
// (no DDL, per user decision after a real-device P0: an earlier 9-byte/18-char
|
|
110
|
+
// version overflowed the column with HTTP 500 "Data too long"). 64 bits still
|
|
111
|
+
// clears the spec's >=64-bit floor for a short-TTL(<=5min) + rate-limited
|
|
112
|
+
// (30/min) one-time code. Never hand-typed by a user, so no dash-grouping
|
|
113
|
+
// (contrast generatePairCode()/formatCode() above, which target human entry).
|
|
114
|
+
// Do not change the byte count without re-confirming the column width with
|
|
115
|
+
// go_relay first.
|
|
116
|
+
export function generateScanPairCode(randomBytesFn = randomBytes) {
|
|
117
|
+
const buf = randomBytesFn(8);
|
|
118
|
+
return Buffer.from(buf).toString("hex").toUpperCase();
|
|
119
|
+
}
|
|
120
|
+
|
|
95
121
|
export function parseNonNegativeInt(value, fallback = 0) {
|
|
96
122
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
97
123
|
return value >= 0 ? Math.trunc(value) : fallback;
|
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,20 +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
|
-
case "picoclaw":
|
|
98
|
-
await runAgentCommand({ runtime: subcommand, options, positional, log });
|
|
99
|
-
return;
|
|
100
100
|
case "service":
|
|
101
101
|
await runServiceCommand({ options, positional, log });
|
|
102
102
|
return;
|
|
@@ -1,8 +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 {
|
|
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";
|
|
5
9
|
import { runPairInstall, runUninstall } from "./pair.mjs";
|
|
10
|
+
import { hasBridgeImplementation } from "../core/runtimeRegistry.mjs";
|
|
6
11
|
|
|
7
12
|
/**
|
|
8
13
|
* `agentlink-agent <runtime> [action] [args]` 分发。
|
|
@@ -17,13 +22,6 @@ export async function runAgentCommand({ runtime, options, positional, log }) {
|
|
|
17
22
|
const action = (positional[0] || "").toLowerCase();
|
|
18
23
|
|
|
19
24
|
if (action === "pair") {
|
|
20
|
-
// picoclaw 不走传统 `pair <code>`,daemon 启动后自动配对(QR 流)。
|
|
21
|
-
if (runtime === "picoclaw") {
|
|
22
|
-
process.stderr.write(
|
|
23
|
-
"picoclaw 不需要 pair 子命令,直接启动 daemon: agentlink-agent picoclaw\n",
|
|
24
|
-
);
|
|
25
|
-
process.exit(2);
|
|
26
|
-
}
|
|
27
25
|
const code = String(positional[1] || options.pair || "").trim();
|
|
28
26
|
if (!code) {
|
|
29
27
|
process.stderr.write(`usage: agentlink pair <code> -r ${runtime}\n`);
|
|
@@ -40,6 +38,19 @@ export async function runAgentCommand({ runtime, options, positional, log }) {
|
|
|
40
38
|
return;
|
|
41
39
|
}
|
|
42
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
|
+
|
|
43
54
|
rtLog.info("bridge.preflight.pass", "runtime dispatcher entered", {
|
|
44
55
|
runtime,
|
|
45
56
|
args_count: positional.length,
|
|
@@ -55,8 +66,20 @@ export async function runAgentCommand({ runtime, options, positional, log }) {
|
|
|
55
66
|
case "hermes":
|
|
56
67
|
await runHermesBridge({ options, positional, log: rtLog });
|
|
57
68
|
return;
|
|
58
|
-
case "
|
|
59
|
-
await
|
|
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 });
|
|
60
83
|
return;
|
|
61
84
|
default:
|
|
62
85
|
rtLog.error("bridge.unknown_runtime", "unsupported runtime", { runtime });
|
|
@@ -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
|
}
|
|
@@ -19,7 +19,7 @@ import { createPathAttachScanner } from "./bridgeAttachAutoScanner.mjs";
|
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* @param {object} opts
|
|
22
|
-
* @param {string} opts.runtime "claude"|"codex"|"hermes"|"openclaw"
|
|
22
|
+
* @param {string} opts.runtime "claude"|"codex"|"hermes"|"openclaw"
|
|
23
23
|
* @param {string} opts.gwId gateway id (= clientUserID 跨 pair_session 反查的键)
|
|
24
24
|
* @param {string} opts.relayUrl relay base URL
|
|
25
25
|
* @param {string} opts.bridgeToken gateway/client token, used by attach 工具调 relay sign/commit
|
|
@@ -22,7 +22,12 @@ const LAN_HOST =
|
|
|
22
22
|
|
|
23
23
|
// URL 尾部:可省略;若有,以 / ? # 起,止于空白或闭合标点。
|
|
24
24
|
// 不包含 ) ] > " ' ` 避免吞 markdown / HTML / 引号边界。
|
|
25
|
-
|
|
25
|
+
// 另排除全角标点/括号(-,含全角 `()`)与 CJK 字符(CJK 标点
|
|
26
|
+
// -ヿ、汉字 一-鿿、谚文 가-):模型常在 URL 后
|
|
27
|
+
// 紧跟中文说明(如 `index.html(返回 200 正常)`),不排除会把中文吞进 URL,
|
|
28
|
+
// 导致穿透 URL 与 markdown label 都带上乱码路径 → WebView 404。
|
|
29
|
+
const URL_TAIL =
|
|
30
|
+
"(?:[/?#][^\\s)\\]>\"'`\\u3000-\\u30FF\\u4E00-\\u9FFF\\uAC00-\\uD7AF\\uFF00-\\uFFEF]*)?";
|
|
26
31
|
|
|
27
32
|
function buildRegex(hostFragment) {
|
|
28
33
|
// 锚定:URL 前必须有非字母数字(或字符串开头)
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// 则禁用)选择是否注册到 bridgeHooks 全局 registry。
|
|
9
9
|
// - 多次调用幂等:用模块级 flag 防止重复注册。
|
|
10
10
|
//
|
|
11
|
-
// 调用方:每个 runtime 入口(src-ext/runtime/{claude,codex,hermes
|
|
11
|
+
// 调用方:每个 runtime 入口(src-ext/runtime/{claude,codex,hermes}/index.mjs)
|
|
12
12
|
// 在 bridge 主循环启动前调一次 installAutoTunnel({ relayUrl, log })。
|
|
13
13
|
|
|
14
14
|
import fs from "node:fs";
|
|
@@ -43,7 +43,7 @@ function findCliBinary() {
|
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
45
|
* @param {{ runtime: string, log: any }} opts
|
|
46
|
-
* runtime: hermes / claude / codex / openclaw
|
|
46
|
+
* runtime: hermes / claude / codex / openclaw
|
|
47
47
|
* log: bridge 自己的 logger(已经带 cid / gw context)
|
|
48
48
|
*
|
|
49
49
|
* 返回 Promise,不抛错 —— 任一步失败都 best-effort,最差也就是用户手动
|
|
@@ -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 });
|