@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
|
@@ -31,12 +31,16 @@ import * as claudeAdapter from "../runtime/claude/mcpConfigAdapter.mjs";
|
|
|
31
31
|
import * as codexAdapter from "../runtime/codex/mcpConfigAdapter.mjs";
|
|
32
32
|
import * as hermesAdapter from "../runtime/hermes/mcpConfigAdapter.mjs";
|
|
33
33
|
import * as openclawAdapter from "../runtime/openclaw/mcpConfigAdapter.mjs";
|
|
34
|
+
import * as cursorAdapter from "../runtime/cursor/mcpConfigAdapter.mjs";
|
|
35
|
+
import * as opencodeAdapter from "../runtime/opencode/mcpConfigAdapter.mjs";
|
|
34
36
|
|
|
35
37
|
const ADAPTERS = {
|
|
36
38
|
claude: claudeAdapter,
|
|
37
39
|
codex: codexAdapter,
|
|
38
40
|
hermes: hermesAdapter,
|
|
39
41
|
openclaw: openclawAdapter,
|
|
42
|
+
cursor: cursorAdapter,
|
|
43
|
+
opencode: opencodeAdapter,
|
|
40
44
|
};
|
|
41
45
|
|
|
42
46
|
const HISTORY_DIR = () => path.join(getAgentlinkDir(), "fanout-history");
|
|
@@ -195,12 +199,30 @@ export async function status(opts = {}) {
|
|
|
195
199
|
}
|
|
196
200
|
|
|
197
201
|
/**
|
|
198
|
-
*
|
|
202
|
+
* 调试用:列出所有已实现 MCP 配置适配的 runtime。
|
|
199
203
|
*/
|
|
200
204
|
export function listAdapters() {
|
|
201
205
|
return Object.keys(ADAPTERS);
|
|
202
206
|
}
|
|
203
207
|
|
|
208
|
+
/**
|
|
209
|
+
* 该 runtime 有没有 MCP 配置适配器 —— register/deregister 的**唯一**前置判据。
|
|
210
|
+
*
|
|
211
|
+
* 🔴 调用方不要拿「有没有 bridge 实现」当近似:两者当前恰好同集合,但语义无关。
|
|
212
|
+
* 用错概念会在下一个「先有 MCP 适配器、还没有 bridge」(或反过来)的 runtime 上
|
|
213
|
+
* 立刻错判 —— 要么静默跳过本该做的注册,要么打出 `unknown_runtime` 这种像是
|
|
214
|
+
* 出错的失败文案(其实只是这一层没实现)。判据只能来自 ADAPTERS 本身。
|
|
215
|
+
*
|
|
216
|
+
* @param {string} runtime
|
|
217
|
+
* @returns {boolean}
|
|
218
|
+
*/
|
|
219
|
+
export function hasMcpAdapter(runtime) {
|
|
220
|
+
return Object.prototype.hasOwnProperty.call(
|
|
221
|
+
ADAPTERS,
|
|
222
|
+
String(runtime ?? "").trim().toLowerCase(),
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
204
226
|
/**
|
|
205
227
|
* 推断 agentlink-mcp-stdio 二进制位置。如果不在 process.argv[1] 同目录,
|
|
206
228
|
* 退到 node_modules/.bin/agentlink-mcp-stdio。fanout 写入前用绝对路径,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// 升级后的协议(对齐服务端 internal/handler/pair_code/handler.go):
|
|
4
4
|
//
|
|
5
5
|
// POST /v1/pair-codes/<code>/consume
|
|
6
|
-
// Body: {"client_user_id":"<...>", "runtime":"
|
|
6
|
+
// Body: {"client_user_id":"<...>", "runtime":"<可配对 runtime,见 runtimeRegistry>"}
|
|
7
7
|
// Response: {"status":"ok", "gw_id":"...", "bridge_token":"...", "runtime":"..."}
|
|
8
8
|
//
|
|
9
9
|
// session_id 现在是 gw_id —— 服务端 BridgeStore.Issue 颁发的 gateway 身份。
|
|
@@ -22,6 +22,8 @@ import os from "node:os";
|
|
|
22
22
|
import process from "node:process";
|
|
23
23
|
import relayDefaults from "../../src-shared/relay-defaults.json" with { type: "json" };
|
|
24
24
|
import { CLI_STATE_DIR_NAME } from "./attachAuditLog.mjs";
|
|
25
|
+
import { getOrCreateInstallationId } from "./installationIdentity.mjs";
|
|
26
|
+
import { PAIRABLE_RUNTIME_KINDS, pairingCapabilitiesForRuntime } from "./runtimeRegistry.mjs";
|
|
25
27
|
|
|
26
28
|
/**
|
|
27
29
|
* Normalize a raw hostname for reporting to the server.
|
|
@@ -50,8 +52,17 @@ export function normalizeHostName(raw) {
|
|
|
50
52
|
/** 默认 relay URL。 */
|
|
51
53
|
export const DEFAULT_RELAY_URL = relayDefaults.defaultRelayUrl;
|
|
52
54
|
|
|
53
|
-
/**
|
|
54
|
-
|
|
55
|
+
/**
|
|
56
|
+
* 可消费配对码的 runtime —— 从 runtimeRegistry 的「可配对集」派生,不再手抄一份。
|
|
57
|
+
*
|
|
58
|
+
* 手抄那份是握手的隐形闸门:runtime 已经在 CLI 各层放行了,consumePairCode 这里
|
|
59
|
+
* 漏加就会在客户端自校验阶段直接抛 `runtime must be one of ...`,配对根本发不出去。
|
|
60
|
+
* 改成派生后,「加 runtime」只需改 runtimeRegistry 一处。
|
|
61
|
+
*
|
|
62
|
+
* ⚠️ 服务端 `util.IsValidRuntimeKind` 是**另一仓**的白名单,与本常量各自独立:
|
|
63
|
+
* 这里放行只保证请求发得出去,服务端未同步放行时会以 4xx 如实失败(不静默降级)。
|
|
64
|
+
*/
|
|
65
|
+
export const VALID_RUNTIMES = PAIRABLE_RUNTIME_KINDS;
|
|
55
66
|
|
|
56
67
|
/** 按优先级解析 relay url:显式 --relay > env > 默认。永远有值。 */
|
|
57
68
|
export function resolveRelayUrl(optionsRelay) {
|
|
@@ -88,6 +99,10 @@ function ensureDir() {
|
|
|
88
99
|
// ext 文件,导致 openclaw/hermes 新版 pair 后 bridge 启动报"尚未配对"。
|
|
89
100
|
// 这里加一层 fallback:ext 文件不存在 / 字段不全时,从 config.json 兜底。
|
|
90
101
|
function readPairStateFromConfig(runtime) {
|
|
102
|
+
// 🔴 白名单而非黑名单:config.json 里只有 openclaw / hermes 两个凭证槽位。
|
|
103
|
+
// 任何 ext-agent runtime(claude / codex / opencode)走到这里都必须返回 null ——
|
|
104
|
+
// 否则「自己的 state 缺失」会被兜底成「借用 openclaw 的网关身份」,
|
|
105
|
+
// 即历史 bug 的读方向版本。加 runtime 时**不要**顺手把它加进这个判断。
|
|
91
106
|
if (runtime !== "hermes" && runtime !== "openclaw") return null;
|
|
92
107
|
let parsed;
|
|
93
108
|
try {
|
|
@@ -97,7 +112,10 @@ function readPairStateFromConfig(runtime) {
|
|
|
97
112
|
const idKey = runtime === "hermes" ? "hermesRelayGatewayId" : "relayGatewayId";
|
|
98
113
|
const tokKey = runtime === "hermes" ? "hermesRelayGatewayToken" : "relayGatewayToken";
|
|
99
114
|
const gwId = String(parsed[idKey] || "").trim();
|
|
100
|
-
|
|
115
|
+
// 早期 config.json 只保存 gateway 凭据,没有 relayUrl。launchd 脚本会通过
|
|
116
|
+
// AGENTLINK_RELAY_URL / --relay 提供环境;这里沿用统一解析顺序,避免升级后把
|
|
117
|
+
// 已配对的 OpenClaw / Hermes 误判成“尚未配对”。
|
|
118
|
+
const relayUrl = resolveRelayUrl(parsed.relayUrl);
|
|
101
119
|
const bridgeToken = String(parsed[tokKey] || "").trim();
|
|
102
120
|
if (!gwId || !relayUrl) return null;
|
|
103
121
|
return {
|
|
@@ -200,10 +218,18 @@ export async function consumePairCode({ relayUrl, code, runtime, clientUserId, f
|
|
|
200
218
|
const base = relayUrl.replace(/\/+$/, "");
|
|
201
219
|
|
|
202
220
|
const hostName = normalizeHostName(os.hostname());
|
|
221
|
+
const installationId = getOrCreateInstallationId();
|
|
222
|
+
const capabilities = pairingCapabilitiesForRuntime(rt);
|
|
203
223
|
|
|
204
224
|
// 1. 优先打新端点
|
|
205
225
|
const v1 = `${base}/v1/pair-codes/${encodeURIComponent(code)}/consume`;
|
|
206
|
-
const v1Body = JSON.stringify({
|
|
226
|
+
const v1Body = JSON.stringify({
|
|
227
|
+
client_user_id: cuid,
|
|
228
|
+
runtime: rt,
|
|
229
|
+
hostName,
|
|
230
|
+
installation_id: installationId,
|
|
231
|
+
capabilities,
|
|
232
|
+
});
|
|
207
233
|
const v1Res = await f(v1, {
|
|
208
234
|
method: "POST",
|
|
209
235
|
headers: { "Content-Type": "application/json" },
|
|
@@ -223,7 +249,13 @@ export async function consumePairCode({ relayUrl, code, runtime, clientUserId, f
|
|
|
223
249
|
const legacyRes = await f(legacy, {
|
|
224
250
|
method: "POST",
|
|
225
251
|
headers: { "Content-Type": "application/json" },
|
|
226
|
-
body: JSON.stringify({
|
|
252
|
+
body: JSON.stringify({
|
|
253
|
+
runtime: rt,
|
|
254
|
+
client_user_id: cuid,
|
|
255
|
+
hostName,
|
|
256
|
+
installation_id: installationId,
|
|
257
|
+
capabilities,
|
|
258
|
+
}),
|
|
227
259
|
});
|
|
228
260
|
const legacyText = await legacyRes.text();
|
|
229
261
|
if (!legacyRes.ok) throw makeConsumeError(legacyRes.status, legacyText);
|
|
@@ -308,12 +340,20 @@ export async function resolveSession({ runtime, options, log }) {
|
|
|
308
340
|
}
|
|
309
341
|
const saved = readPairState(runtime);
|
|
310
342
|
if (saved) {
|
|
343
|
+
// 显式 --relay flag 必须压过 saved.relay_url:hermes 走 config.json 兜底时
|
|
344
|
+
// relay_url 可能落到 DEFAULT_RELAY_URL(生产),launchd 脚本明明传了
|
|
345
|
+
// --relay <test>,worker 却把心跳和 long-poll 全打到生产 relay ——
|
|
346
|
+
// 测试环境永远显示离线(2026-08-12 实测根因)。
|
|
347
|
+
const effectiveRelay = typeof options.relay === "string" && options.relay.trim()
|
|
348
|
+
? relay
|
|
349
|
+
: saved.relay_url;
|
|
311
350
|
log?.info("pair.state.reuse", "reusing saved pair state", {
|
|
312
|
-
gw_id: saved.gw_id, relay:
|
|
351
|
+
gw_id: saved.gw_id, relay: effectiveRelay, saved_relay: saved.relay_url,
|
|
352
|
+
has_bridge_token: !!saved.bridge_token,
|
|
313
353
|
});
|
|
314
354
|
const savedResult = {
|
|
315
355
|
gwId: saved.gw_id,
|
|
316
|
-
relayUrl:
|
|
356
|
+
relayUrl: effectiveRelay,
|
|
317
357
|
source: "saved",
|
|
318
358
|
bridgeToken: saved.bridge_token || "",
|
|
319
359
|
};
|
|
@@ -9,11 +9,23 @@
|
|
|
9
9
|
* summary ok=true && !skipped → "paired_new"
|
|
10
10
|
* summary skipped=true → "paired_existing"
|
|
11
11
|
* summary ok=false → "failed"
|
|
12
|
-
* 未出现在 summary
|
|
12
|
+
* 未出现在 summary + 未安装 → "not_installed"
|
|
13
|
+
* 未出现在 summary + 已安装 → "failed"(见下方分支注释)
|
|
13
14
|
*/
|
|
14
15
|
|
|
15
|
-
/**
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* 所有被扫描的 runtime 名 = bin/agentlink 的 HOST_DETECTED_RUNTIME_KINDS。
|
|
18
|
+
*
|
|
19
|
+
* OpenCode 与 DeepAgents 都在这里:OpenCode 通过 PATH 探测;DeepAgents 通过
|
|
20
|
+
* `agentlink-deepagents`、`langgraph.json` 或本地 status endpoint 探测。两者都使用
|
|
21
|
+
* ext runtime 凭证槽,不会回落或覆盖 OpenClaw 凭证。
|
|
22
|
+
*
|
|
23
|
+
* 导出供测试断言「探测集单一真相」,加 runtime 时两边不会漂。
|
|
24
|
+
*/
|
|
25
|
+
export const SCANNED_RUNTIMES = Object.freeze([
|
|
26
|
+
"openclaw", "hermes", "claude", "codex", "cursor", "opencode", "deepagents",
|
|
27
|
+
"qwen", "kimi", "codebuddy",
|
|
28
|
+
]);
|
|
17
29
|
|
|
18
30
|
/**
|
|
19
31
|
* buildPairSessionResults 从 summary + detected 构建 per-runtime outcome 列表。
|
|
@@ -21,9 +33,9 @@ const SCANNED_RUNTIMES = ["openclaw", "hermes", "claude", "codex"];
|
|
|
21
33
|
* @param {Array<{kind: string, ok: boolean, skipped?: boolean}>} summary
|
|
22
34
|
* runPairAll 内的 summary 数组,只包含已安装的 runtime。
|
|
23
35
|
* @param {Array<{kind: string, installed: boolean}>} detected
|
|
24
|
-
* detectInstalledRuntimes
|
|
36
|
+
* detectInstalledRuntimes 的返回值,含全部本机探测集 runtime。
|
|
25
37
|
* @returns {Array<{runtime: string, outcome: string}>}
|
|
26
|
-
*
|
|
38
|
+
* 长度 = SCANNED_RUNTIMES.length,覆盖全部 scanned runtimes。
|
|
27
39
|
*/
|
|
28
40
|
export function buildPairSessionResults(summary, detected) {
|
|
29
41
|
// 构建 summaryMap:kind → summary entry(方便 O(1) 查找)
|
|
@@ -50,7 +62,20 @@ export function buildPairSessionResults(summary, detected) {
|
|
|
50
62
|
} else if (!installedSet.has(rt)) {
|
|
51
63
|
outcome = "not_installed";
|
|
52
64
|
} else {
|
|
53
|
-
// 已安装但没有出现在 summary
|
|
65
|
+
// 已安装但没有出现在 summary。
|
|
66
|
+
//
|
|
67
|
+
// 历史:opencode 曾固定命中这里(当时它只入探测、没有配对实现,
|
|
68
|
+
// runPairAll 不会把它 spawn 进扇出)。自
|
|
69
|
+
// devagent-m-20260807-cli-runtime-opencode-pairable 起 opencode 已进入
|
|
70
|
+
// 扇出目标,会带着真实 outcome 出现在 summary 里 —— 本分支退回**纯防御性
|
|
71
|
+
// 兜底**:给下一个「只入探测、还没配对实现」的 runtime 用。
|
|
72
|
+
//
|
|
73
|
+
// 映射成 failed 而不是 not_installed:它确实装了,谎报未安装会让 App 把
|
|
74
|
+
// chip 显示成"未安装",与本机事实相反。failed 的语义"没配上"是真的。
|
|
75
|
+
//
|
|
76
|
+
// 也刻意 **不** 新增 `unsupported` 之类的 outcome 值:outcome 枚举由 relay
|
|
77
|
+
// 侧持有(另一仓),本轮 relay 只放行了 `opencode` 这个 runtime 名,
|
|
78
|
+
// 单方面发新 outcome 值有被静默丢弃/报错的风险,跨仓契约未核实。
|
|
54
79
|
outcome = "failed";
|
|
55
80
|
}
|
|
56
81
|
|
|
@@ -23,6 +23,8 @@ import {
|
|
|
23
23
|
bridgeAuthHeaders,
|
|
24
24
|
extractError,
|
|
25
25
|
} from "./relayWorkerHttp.mjs";
|
|
26
|
+
import { ensureDefaultWorkspace } from "./defaultWorkspace.mjs";
|
|
27
|
+
import { ensureRelayDefaultWorkspace } from "./defaultWorkspaceSync.mjs";
|
|
26
28
|
|
|
27
29
|
// long-poll 等待秒数(服务端上限 60s)。多数情况下队列里就有请求,空闲时占用一个连接。
|
|
28
30
|
const POLL_WAIT_SECONDS = 25;
|
|
@@ -44,7 +46,8 @@ const POLL_BACKOFF_MAX_MS = 15_000;
|
|
|
44
46
|
* relayUrl: string,
|
|
45
47
|
* gwId: string,
|
|
46
48
|
* bridgeToken: string,
|
|
47
|
-
* runtime:
|
|
49
|
+
* runtime: string,
|
|
50
|
+
* profileName?: string,
|
|
48
51
|
* log: any,
|
|
49
52
|
* }} opts
|
|
50
53
|
*/
|
|
@@ -180,6 +183,23 @@ export function createRelayWorker(opts) {
|
|
|
180
183
|
if (typeof handler !== "function") {
|
|
181
184
|
throw new Error("worker.run: handler must be a function");
|
|
182
185
|
}
|
|
186
|
+
// Pair 时的首次上报可能刚好撞上 relay 重启;worker 启动时补一次,失败
|
|
187
|
+
// 不阻断会话桥接,并在下一次 worker 重启时继续重试。
|
|
188
|
+
try {
|
|
189
|
+
await ensureDefaultWorkspace(opts.runtime, { profileName: opts.profileName });
|
|
190
|
+
await ensureRelayDefaultWorkspace({
|
|
191
|
+
runtime: opts.runtime,
|
|
192
|
+
relayUrl: base,
|
|
193
|
+
bridgeToken: opts.bridgeToken,
|
|
194
|
+
profileName: opts.profileName,
|
|
195
|
+
});
|
|
196
|
+
log.info("default_workspace.ready", "default workspace registered with relay", { runtime: opts.runtime });
|
|
197
|
+
} catch (err) {
|
|
198
|
+
log.warn("default_workspace.pending", "default workspace registration will retry on next worker start", {
|
|
199
|
+
runtime: opts.runtime,
|
|
200
|
+
err: err?.message || String(err),
|
|
201
|
+
});
|
|
202
|
+
}
|
|
183
203
|
// sessionKey → 该会话当前尾部 promise(chat 请求按会话串行、跨会话并行)
|
|
184
204
|
const sessionLanes = new Map();
|
|
185
205
|
startHeartbeat();
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// runtimeRegistry.mjs — runtime「能力分层」的单一真相表(cli-runtime-kind-registry)。
|
|
2
|
+
//
|
|
3
|
+
// 为什么要单独一个模块:本仓有两处代码各自持有一份 runtime 名单 ——
|
|
4
|
+
// `bin/agentlink`(native 路径 + 命令分派)与 `src-ext/**`(agentlink-agent 平行世界)。
|
|
5
|
+
// 两边都硬编码时,加一个 runtime 就要满仓 grep,漏一处就退化成「静默回落 openclaw」
|
|
6
|
+
// (历史 bug:非 openclaw 的配对结果被写进 openclaw 的 relayGatewayId/Token 槽位)。
|
|
7
|
+
// 现在两边都 import 这里,**加 runtime = 在对应层加一行**,「这一层要不要带上它」
|
|
8
|
+
// 被逼成一次显式选择。
|
|
9
|
+
//
|
|
10
|
+
// 🔴 分层不是同一批名字的近义词,语义严格不同,改前先读清楚:
|
|
11
|
+
//
|
|
12
|
+
// PAIRABLE 真有 pair / reset 实现、配对能落凭证的 runtime。
|
|
13
|
+
// BRIDGE_CAPABLE 真有 bridge / service / MCP 适配器实现、能起进程收发消息的 runtime。
|
|
14
|
+
// PAIR_ONLY = PAIRABLE ∖ BRIDGE_CAPABLE:配得上、但本轮还跑不起来。
|
|
15
|
+
// 这些 runtime 在 bridge/service/reload/scan/status 上必须**显式拦停**,
|
|
16
|
+
// 绝不能进 normalizeRuntimeKind(x, "openclaw") 那条回落路径。
|
|
17
|
+
// EXT_AGENT 由 `agentlink-agent` 子进程承载、凭证存
|
|
18
|
+
// ~/.agentlink/ext-<runtime>-last.json 的 runtime。
|
|
19
|
+
// 与之相对的 openclaw/hermes 凭证存 ~/.agentlink/config.json。
|
|
20
|
+
// ⚠️ 这一层决定「凭证写哪个槽位」,加错 = 静默错配。
|
|
21
|
+
//
|
|
22
|
+
// 本机探测集 / 已知枚举 / 扫码集仍留在 `bin/agentlink`(它们只有 native 侧用到,
|
|
23
|
+
// 且与 t() 文案渲染耦合);本模块只收敛 src-ext 与 bin **都**需要的能力分层。
|
|
24
|
+
|
|
25
|
+
export const RUNTIME_KIND_OPENCLAW = "openclaw";
|
|
26
|
+
export const RUNTIME_KIND_HERMES = "hermes";
|
|
27
|
+
export const RUNTIME_KIND_CLAUDE = "claude";
|
|
28
|
+
export const RUNTIME_KIND_CODEX = "codex";
|
|
29
|
+
export const RUNTIME_KIND_CURSOR = "cursor";
|
|
30
|
+
export const RUNTIME_KIND_OPENCODE = "opencode";
|
|
31
|
+
export const RUNTIME_KIND_DEEPAGENTS = "deepagents";
|
|
32
|
+
export const RUNTIME_KIND_QWEN = "qwen";
|
|
33
|
+
export const RUNTIME_KIND_KIMI = "kimi";
|
|
34
|
+
export const RUNTIME_KIND_CODEBUDDY = "codebuddy";
|
|
35
|
+
|
|
36
|
+
export const RUNTIME_CAPABILITY_SCHEMA_VERSION = 2;
|
|
37
|
+
export const MINIMUM_CAPABILITY_CLI_VERSION = "1.1.0";
|
|
38
|
+
export const CURRENT_PROTOCOL_VERSION = "v1.1";
|
|
39
|
+
|
|
40
|
+
const BRIDGE_MODE_AGENTLINK_CLI = "agentlink_cli";
|
|
41
|
+
const BRIDGE_MODE_SIDECAR = "sidecar";
|
|
42
|
+
|
|
43
|
+
const WORKSPACE_HISTORY_RUNTIMES = new Set([
|
|
44
|
+
RUNTIME_KIND_OPENCLAW,
|
|
45
|
+
RUNTIME_KIND_HERMES,
|
|
46
|
+
RUNTIME_KIND_CLAUDE,
|
|
47
|
+
RUNTIME_KIND_CODEX,
|
|
48
|
+
RUNTIME_KIND_CURSOR,
|
|
49
|
+
RUNTIME_KIND_OPENCODE,
|
|
50
|
+
RUNTIME_KIND_QWEN,
|
|
51
|
+
RUNTIME_KIND_KIMI,
|
|
52
|
+
RUNTIME_KIND_CODEBUDDY,
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
function capabilityOf(runtime, bridgeMode = BRIDGE_MODE_AGENTLINK_CLI) {
|
|
56
|
+
const workspaceV2 = WORKSPACE_HISTORY_RUNTIMES.has(runtime);
|
|
57
|
+
return Object.freeze({
|
|
58
|
+
runtime,
|
|
59
|
+
pairable: true,
|
|
60
|
+
bridgeMode,
|
|
61
|
+
identity: true,
|
|
62
|
+
cancel: true,
|
|
63
|
+
minimumCliVersion: MINIMUM_CAPABILITY_CLI_VERSION,
|
|
64
|
+
protocolVersions: Object.freeze([CURRENT_PROTOCOL_VERSION]),
|
|
65
|
+
installationIdentityV1: workspaceV2,
|
|
66
|
+
workspaceIdentityV2: workspaceV2,
|
|
67
|
+
historyImportV2: workspaceV2,
|
|
68
|
+
workspaceCwd: workspaceV2,
|
|
69
|
+
sessionResume: workspaceV2,
|
|
70
|
+
historySource: workspaceV2,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Boolean-only capability snapshot accepted by the relay pair endpoint. */
|
|
75
|
+
export function pairingCapabilitiesForRuntime(kind) {
|
|
76
|
+
const runtime = String(kind ?? "").trim().toLowerCase();
|
|
77
|
+
const capability = RUNTIME_CAPABILITIES.runtimes.find((item) => item.runtime === runtime);
|
|
78
|
+
if (!capability) return Object.freeze({});
|
|
79
|
+
return Object.freeze({
|
|
80
|
+
installation_identity_v1: capability.installationIdentityV1,
|
|
81
|
+
workspace_identity_v2: capability.workspaceIdentityV2,
|
|
82
|
+
history_import_v2: capability.historyImportV2,
|
|
83
|
+
workspace_cwd_v1: capability.workspaceCwd,
|
|
84
|
+
session_resume_v1: capability.sessionResume,
|
|
85
|
+
history_source_v1: capability.historySource,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 公开能力元数据,不读取配对凭据,也不探测或启动第三方 Agent。 */
|
|
90
|
+
export const RUNTIME_CAPABILITIES = Object.freeze({
|
|
91
|
+
schemaVersion: RUNTIME_CAPABILITY_SCHEMA_VERSION,
|
|
92
|
+
runtimes: Object.freeze([
|
|
93
|
+
capabilityOf(RUNTIME_KIND_OPENCLAW),
|
|
94
|
+
capabilityOf(RUNTIME_KIND_HERMES),
|
|
95
|
+
capabilityOf(RUNTIME_KIND_CLAUDE),
|
|
96
|
+
capabilityOf(RUNTIME_KIND_CODEX),
|
|
97
|
+
capabilityOf(RUNTIME_KIND_CURSOR),
|
|
98
|
+
capabilityOf(RUNTIME_KIND_OPENCODE),
|
|
99
|
+
capabilityOf(RUNTIME_KIND_DEEPAGENTS, BRIDGE_MODE_SIDECAR),
|
|
100
|
+
capabilityOf(RUNTIME_KIND_QWEN),
|
|
101
|
+
capabilityOf(RUNTIME_KIND_KIMI),
|
|
102
|
+
capabilityOf(RUNTIME_KIND_CODEBUDDY),
|
|
103
|
+
]),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
/** PATH 探测入口;Kimi 保留 N-1 发行版的 kimi-cli 兼容名。 */
|
|
107
|
+
export const RUNTIME_BINARY_CANDIDATES = Object.freeze({
|
|
108
|
+
[RUNTIME_KIND_HERMES]: Object.freeze(["hermes"]),
|
|
109
|
+
[RUNTIME_KIND_CLAUDE]: Object.freeze(["claude"]),
|
|
110
|
+
[RUNTIME_KIND_CODEX]: Object.freeze(["codex"]),
|
|
111
|
+
[RUNTIME_KIND_CURSOR]: Object.freeze(["cursor"]),
|
|
112
|
+
[RUNTIME_KIND_OPENCODE]: Object.freeze(["opencode"]),
|
|
113
|
+
[RUNTIME_KIND_DEEPAGENTS]: Object.freeze(["agentlink-deepagents"]),
|
|
114
|
+
[RUNTIME_KIND_QWEN]: Object.freeze(["qwen"]),
|
|
115
|
+
[RUNTIME_KIND_KIMI]: Object.freeze(["kimi", "kimi-cli"]),
|
|
116
|
+
[RUNTIME_KIND_CODEBUDDY]: Object.freeze(["codebuddy"]),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 有 bridge / service / MCP 适配器实现的 runtime。
|
|
121
|
+
* Cursor / OpenCode 使用 headless CLI bridge。
|
|
122
|
+
*/
|
|
123
|
+
export const BRIDGE_CAPABLE_RUNTIME_KINDS = Object.freeze([
|
|
124
|
+
RUNTIME_KIND_OPENCLAW,
|
|
125
|
+
RUNTIME_KIND_HERMES,
|
|
126
|
+
RUNTIME_KIND_CLAUDE,
|
|
127
|
+
RUNTIME_KIND_CODEX,
|
|
128
|
+
RUNTIME_KIND_CURSOR,
|
|
129
|
+
RUNTIME_KIND_OPENCODE,
|
|
130
|
+
RUNTIME_KIND_QWEN,
|
|
131
|
+
RUNTIME_KIND_KIMI,
|
|
132
|
+
RUNTIME_KIND_CODEBUDDY,
|
|
133
|
+
]);
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 有 pair / reset 实现、配对能真正落凭证的 runtime。
|
|
137
|
+
* opencode 自 devagent-m-20260807-cli-runtime-opencode-pairable 起进入本集合。
|
|
138
|
+
*/
|
|
139
|
+
export const PAIRABLE_RUNTIME_KINDS = Object.freeze([
|
|
140
|
+
...BRIDGE_CAPABLE_RUNTIME_KINDS,
|
|
141
|
+
RUNTIME_KIND_DEEPAGENTS,
|
|
142
|
+
]);
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 配得上、但本轮没有 bridge 实现的 runtime(= PAIRABLE ∖ BRIDGE_CAPABLE)。
|
|
146
|
+
* 派生而非硬编码:BRIDGE_CAPABLE 补齐时这里自动清空,不会留下过期的拦停。
|
|
147
|
+
*/
|
|
148
|
+
export const PAIR_ONLY_RUNTIME_KINDS = Object.freeze(
|
|
149
|
+
PAIRABLE_RUNTIME_KINDS.filter((k) => !BRIDGE_CAPABLE_RUNTIME_KINDS.includes(k)),
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 由 agentlink-agent 承载、凭证存 ext-<runtime>-last.json 的 runtime。
|
|
154
|
+
* 🔴 决定「配对凭证写哪个槽位」——openclaw/hermes 不在此列(它们写 config.json)。
|
|
155
|
+
*/
|
|
156
|
+
export const EXT_AGENT_RUNTIME_KINDS = Object.freeze([
|
|
157
|
+
RUNTIME_KIND_CLAUDE,
|
|
158
|
+
RUNTIME_KIND_CODEX,
|
|
159
|
+
RUNTIME_KIND_CURSOR,
|
|
160
|
+
RUNTIME_KIND_OPENCODE,
|
|
161
|
+
RUNTIME_KIND_DEEPAGENTS,
|
|
162
|
+
RUNTIME_KIND_QWEN,
|
|
163
|
+
RUNTIME_KIND_KIMI,
|
|
164
|
+
RUNTIME_KIND_CODEBUDDY,
|
|
165
|
+
]);
|
|
166
|
+
|
|
167
|
+
/** 该 runtime 有没有配对实现(pair / reset 能不能走)。 */
|
|
168
|
+
export function isPairableRuntime(kind) {
|
|
169
|
+
return PAIRABLE_RUNTIME_KINDS.includes(String(kind ?? "").trim().toLowerCase());
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** 该 runtime 有没有 bridge / service 实现(bridge / service / reload 能不能走)。 */
|
|
173
|
+
export function hasBridgeImplementation(kind) {
|
|
174
|
+
return BRIDGE_CAPABLE_RUNTIME_KINDS.includes(String(kind ?? "").trim().toLowerCase());
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** 该 runtime 的配对凭证是不是存 ext-<runtime>-last.json(而非 config.json)。 */
|
|
178
|
+
export function isExtAgentRuntime(kind) {
|
|
179
|
+
return EXT_AGENT_RUNTIME_KINDS.includes(String(kind ?? "").trim().toLowerCase());
|
|
180
|
+
}
|