@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
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// `agentlink scan` 终端二维码渲染 —— 封装 `qrcode-terminal`(`small: true`)。
|
|
2
|
+
//
|
|
3
|
+
// spec S6(二维码展示兜底):终端不支持 ASCII 二维码渲染时(渲染抛错),
|
|
4
|
+
// SHALL 回退为打印 payload 文本,不因渲染失败中断配对流程。因此本函数
|
|
5
|
+
// **永不抛错**——渲染失败被内部吞掉并转成文本兜底。
|
|
6
|
+
|
|
7
|
+
import qrcodeTerminal from "qrcode-terminal";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 在终端渲染 payload 的 ASCII 二维码;渲染失败时回退打印 payload 文本。
|
|
11
|
+
*
|
|
12
|
+
* @param {string} payload 二维码内容(qr_payload deeplink)
|
|
13
|
+
* @param {{
|
|
14
|
+
* generateImpl?: (input: string, opts: {small: boolean}, cb: (output: string) => void) => void,
|
|
15
|
+
* print?: (s: string) => void,
|
|
16
|
+
* warn?: (s: string) => void,
|
|
17
|
+
* }} [deps]
|
|
18
|
+
* @returns {{ ok: boolean, mode: "qr" | "text", error?: string }}
|
|
19
|
+
*/
|
|
20
|
+
export function renderQrToTerminal(payload, { generateImpl, print, warn } = {}) {
|
|
21
|
+
// NB: must call through `qrcodeTerminal.generate(...)`, not a bare
|
|
22
|
+
// reference to the method — qrcode-terminal's generate() reads `this.error`
|
|
23
|
+
// internally, and a detached reference loses that binding (`this` would be
|
|
24
|
+
// undefined under ESM's implicit strict mode), throwing on every call.
|
|
25
|
+
const generate = generateImpl || ((input, opts, cb) => qrcodeTerminal.generate(input, opts, cb));
|
|
26
|
+
const doPrint = print || ((s) => console.log(s));
|
|
27
|
+
const doWarn = warn || ((s) => console.warn(s));
|
|
28
|
+
const text = payload == null ? "" : String(payload);
|
|
29
|
+
try {
|
|
30
|
+
generate(text, { small: true }, (output) => {
|
|
31
|
+
doPrint(output);
|
|
32
|
+
});
|
|
33
|
+
return { ok: true, mode: "qr" };
|
|
34
|
+
} catch (err) {
|
|
35
|
+
const message = String(err?.message || err);
|
|
36
|
+
doWarn(`⚠ 二维码渲染失败,回退为文本(仍可完成配对):${message}`);
|
|
37
|
+
doPrint(text);
|
|
38
|
+
return { ok: false, mode: "text", error: message };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import crypto from "node:crypto";
|
|
13
13
|
import { execFile } from "node:child_process";
|
|
14
|
+
import { promisify } from "node:util";
|
|
14
15
|
import fs from "node:fs";
|
|
15
16
|
import os from "node:os";
|
|
16
17
|
import path from "node:path";
|
|
@@ -22,6 +23,7 @@ const DEFAULT_LIMIT = 50;
|
|
|
22
23
|
const MAX_LIMIT = 500;
|
|
23
24
|
const HARD_TIMEOUT_MS = 5000;
|
|
24
25
|
const GIT_BRANCH_TIMEOUT_MS = 200;
|
|
26
|
+
const execFileAsync = promisify(execFile);
|
|
25
27
|
|
|
26
28
|
// Runtime metadata — agentKind values must match dart stub ScannedAgentGroup.kind exactly.
|
|
27
29
|
const RUNTIME_META = {
|
|
@@ -49,6 +51,11 @@ const RUNTIME_META = {
|
|
|
49
51
|
tagline: "Agentlink 内置对话智能体",
|
|
50
52
|
idPrefix: "hm",
|
|
51
53
|
},
|
|
54
|
+
Qwen: { agentKind: "Qwen", emoji: "🌊", tagline: "Qwen CLI", idPrefix: "qw" },
|
|
55
|
+
Kimi: { agentKind: "Kimi", emoji: "🌙", tagline: "Kimi CLI", idPrefix: "km" },
|
|
56
|
+
CodeBuddy: { agentKind: "CodeBuddy", emoji: "🤖", tagline: "CodeBuddy", idPrefix: "cb" },
|
|
57
|
+
Cursor: { agentKind: "Cursor", emoji: "⌨️", tagline: "Cursor", idPrefix: "cu" },
|
|
58
|
+
OpenCode: { agentKind: "OpenCode", emoji: "🧩", tagline: "OpenCode", idPrefix: "oo" },
|
|
52
59
|
};
|
|
53
60
|
|
|
54
61
|
// runtimeKind string → RUNTIME_META key
|
|
@@ -58,6 +65,7 @@ const RUNTIME_KIND_MAP = {
|
|
|
58
65
|
claude: "Claude Code",
|
|
59
66
|
codex: "Codex",
|
|
60
67
|
hermes: "Hermes",
|
|
68
|
+
qwen: "Qwen", kimi: "Kimi", codebuddy: "CodeBuddy", cursor: "Cursor", opencode: "OpenCode",
|
|
61
69
|
};
|
|
62
70
|
|
|
63
71
|
// ─── utility functions ───────────────────────────────────────────────────────
|
|
@@ -166,6 +174,7 @@ export async function readOpenClawWorkspaces({
|
|
|
166
174
|
limit,
|
|
167
175
|
includeBranch,
|
|
168
176
|
workspaceRoot,
|
|
177
|
+
openclawHome,
|
|
169
178
|
}) {
|
|
170
179
|
const warnings = [];
|
|
171
180
|
|
|
@@ -174,7 +183,7 @@ export async function readOpenClawWorkspaces({
|
|
|
174
183
|
dirs = [path.resolve(workspaceRoot)];
|
|
175
184
|
} else {
|
|
176
185
|
try {
|
|
177
|
-
dirs =
|
|
186
|
+
dirs = readJsonlCwds(openclawHome ?? path.join(resolveOpenClawHomeDir(), "agents"));
|
|
178
187
|
} catch (err) {
|
|
179
188
|
warnings.push(`OpenClaw: failed to resolve workspace dirs: ${err.message}`);
|
|
180
189
|
return { workspaces: [], warnings, truncated: false };
|
|
@@ -216,6 +225,29 @@ export async function readOpenClawWorkspaces({
|
|
|
216
225
|
return { workspaces, warnings, truncated };
|
|
217
226
|
}
|
|
218
227
|
|
|
228
|
+
function readJsonlCwds(root) {
|
|
229
|
+
const found = new Set();
|
|
230
|
+
const visit = (dir) => {
|
|
231
|
+
let entries; try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
232
|
+
for (const entry of entries) {
|
|
233
|
+
const item = path.join(dir, entry.name);
|
|
234
|
+
if (entry.isDirectory()) visit(item);
|
|
235
|
+
else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
236
|
+
try {
|
|
237
|
+
for (const line of fs.readFileSync(item, "utf8").split(/\r?\n/)) {
|
|
238
|
+
if (!line.trim()) continue;
|
|
239
|
+
const row = JSON.parse(line);
|
|
240
|
+
const cwd = row?.cwd ?? row?.message?.cwd ?? row?.context?.cwd;
|
|
241
|
+
if (typeof cwd === "string" && cwd) found.add(path.resolve(cwd));
|
|
242
|
+
}
|
|
243
|
+
} catch {}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
visit(root);
|
|
248
|
+
return [...found];
|
|
249
|
+
}
|
|
250
|
+
|
|
219
251
|
// ─── Claude Code reader ───────────────────────────────────────────────────────
|
|
220
252
|
|
|
221
253
|
/**
|
|
@@ -385,31 +417,19 @@ export async function readCodexWorkspaces({
|
|
|
385
417
|
const cwdByPath = new Map(); // absolutePath → true (for dedupe)
|
|
386
418
|
|
|
387
419
|
for (const dir of sessionDirs) {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
files = fs.readdirSync(dir);
|
|
391
|
-
} catch (err) {
|
|
392
|
-
if (err.code === "EACCES" || err.code === "EPERM") {
|
|
393
|
-
warnings.push(`Codex: permission denied reading ${dir}: ${err.message}`);
|
|
394
|
-
}
|
|
395
|
-
// ENOENT: dir doesn't exist — skip silently
|
|
396
|
-
continue;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
for (const file of files) {
|
|
400
|
-
if (!file.endsWith(".jsonl")) continue;
|
|
401
|
-
const filePath = path.join(dir, file);
|
|
420
|
+
if (!fs.existsSync(dir)) continue;
|
|
421
|
+
for (const filePath of findFiles(dir, (name) => name.endsWith(".jsonl"))) {
|
|
402
422
|
try {
|
|
403
423
|
const content = fs.readFileSync(filePath, "utf8");
|
|
404
424
|
const firstLine = content.split("\n")[0].trim();
|
|
405
425
|
if (!firstLine) continue;
|
|
406
426
|
const parsed = JSON.parse(firstLine);
|
|
407
|
-
const cwd = parsed?.cwd;
|
|
427
|
+
const cwd = parsed?.cwd ?? parsed?.payload?.cwd;
|
|
408
428
|
if (typeof cwd === "string" && cwd) {
|
|
409
429
|
cwdByPath.set(path.resolve(cwd.replace(/^~/, home)), true);
|
|
410
430
|
}
|
|
411
431
|
} catch (err) {
|
|
412
|
-
warnings.push(`Codex: failed to parse
|
|
432
|
+
warnings.push(`Codex: failed to parse session: ${err.message}`);
|
|
413
433
|
}
|
|
414
434
|
}
|
|
415
435
|
}
|
|
@@ -714,7 +734,10 @@ export async function readHermesWorkspaces({
|
|
|
714
734
|
*/
|
|
715
735
|
export async function scanWorkspacesByRuntime({ runtimeKind, gatewayId, params }) {
|
|
716
736
|
const kind = normalizeRuntimeKind(runtimeKind);
|
|
717
|
-
|
|
737
|
+
if (!kind) {
|
|
738
|
+
return { ok: true, agentKind: String(runtimeKind || ""), emoji: "", tagline: "", host: hostMeta(gatewayId), workspaces: [], truncated: false, scannedAt: new Date().toISOString(), warnings: [`unsupported runtime: ${runtimeKind}`] };
|
|
739
|
+
}
|
|
740
|
+
const meta = RUNTIME_META[kind];
|
|
718
741
|
const limit = clampLimit(params?.limit ?? DEFAULT_LIMIT);
|
|
719
742
|
const includeBranch = params?.includeBranch === true;
|
|
720
743
|
|
|
@@ -723,6 +746,9 @@ export async function scanWorkspacesByRuntime({ runtimeKind, gatewayId, params }
|
|
|
723
746
|
if (params?.hermesProfilesRoot != null) {
|
|
724
747
|
readerArgs.hermesProfilesRoot = params.hermesProfilesRoot;
|
|
725
748
|
}
|
|
749
|
+
for (const key of ["root", "openclawHome", "hermesDbPath", "opencodeDbPath", "codexHome"]) {
|
|
750
|
+
if (params?.[key] != null) readerArgs[key] = params[key];
|
|
751
|
+
}
|
|
726
752
|
|
|
727
753
|
// Wrap reader in 5s hard timeout
|
|
728
754
|
const readerPromise = runReader(kind, readerArgs);
|
|
@@ -768,19 +794,24 @@ function clampLimit(raw) {
|
|
|
768
794
|
}
|
|
769
795
|
|
|
770
796
|
function normalizeRuntimeKind(kind) {
|
|
771
|
-
if (!kind) return
|
|
797
|
+
if (!kind) return null;
|
|
772
798
|
const lower = String(kind).toLowerCase().trim();
|
|
773
|
-
return RUNTIME_KIND_MAP[lower] ??
|
|
799
|
+
return RUNTIME_KIND_MAP[lower] ?? null;
|
|
774
800
|
}
|
|
775
801
|
|
|
776
802
|
async function runReader(kind, args) {
|
|
777
803
|
try {
|
|
778
804
|
switch (kind) {
|
|
779
805
|
case "OpenClaw": return await readOpenClawWorkspaces(args);
|
|
780
|
-
case "Claude Code": return await
|
|
806
|
+
case "Claude Code": return await readDirectoryNameWorkspaces(args, "Claude Code", path.join(os.homedir(), ".claude", "projects"), true);
|
|
781
807
|
case "Codex": return await readCodexWorkspaces(args);
|
|
782
|
-
case "Hermes": return await readHermesWorkspaces(args);
|
|
783
|
-
|
|
808
|
+
case "Hermes": return args.hermesProfilesRoot ? await readHermesWorkspaces(args) : await readHermesDbWorkspaces(args);
|
|
809
|
+
case "Qwen": return await readEncodedProjectWorkspaces(args, "Qwen", path.join(os.homedir(), ".qwen", "projects"));
|
|
810
|
+
case "Kimi": return await readKimiWorkspaces(args);
|
|
811
|
+
case "CodeBuddy": return await readCodeBuddyWorkspaces(args);
|
|
812
|
+
case "Cursor": return await readCursorWorkspaces(args);
|
|
813
|
+
case "OpenCode": return await readOpenCodeWorkspaces(args);
|
|
814
|
+
default: return { workspaces: [], warnings: [`unsupported runtime: ${kind}`], truncated: false };
|
|
784
815
|
}
|
|
785
816
|
} catch (err) {
|
|
786
817
|
// Permission errors degrade to warnings
|
|
@@ -791,6 +822,115 @@ async function runReader(kind, args) {
|
|
|
791
822
|
}
|
|
792
823
|
}
|
|
793
824
|
|
|
825
|
+
async function workspaceEntries(paths, meta, limit, includeBranch) {
|
|
826
|
+
const selected = [...new Set(paths.map((p) => path.resolve(p)))].slice(0, clampLimit(limit));
|
|
827
|
+
const branches = await resolveBranches(selected, includeBranch);
|
|
828
|
+
return selected.map((absolutePath, i) => ({ id: pathToId(meta.idPrefix, absolutePath), name: path.basename(absolutePath), path: absolutePath, absolutePath, branch: branches[i], lastUsedAtMs: null }));
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
async function readProjectDirectoryWorkspaces({ limit, includeBranch, root }, kind, defaultRoot) {
|
|
832
|
+
const dir = root ?? defaultRoot; let entries = [];
|
|
833
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return { workspaces: [], warnings: [], truncated: false }; }
|
|
834
|
+
const paths = entries.filter((e) => e.isDirectory()).map((e) => path.join(dir, e.name));
|
|
835
|
+
return { workspaces: await workspaceEntries(paths, RUNTIME_META[kind], limit, includeBranch), warnings: [], truncated: paths.length > clampLimit(limit) };
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
async function readDirectoryNameWorkspaces(args, kind, defaultRoot, decode) {
|
|
839
|
+
let entries = []; const root = args.root ?? defaultRoot;
|
|
840
|
+
try { entries = fs.readdirSync(root, { withFileTypes: true }); } catch { return { workspaces: [], warnings: [], truncated: false }; }
|
|
841
|
+
const paths = entries.filter((e) => e.isDirectory()).map((e) => decode ? path.resolve("/" + e.name.replace(/^-/, "").replace(/-/g, path.sep)) : path.join(root, e.name));
|
|
842
|
+
return { workspaces: await workspaceEntries(paths, RUNTIME_META[kind], args.limit, args.includeBranch), warnings: [], truncated: paths.length > clampLimit(args.limit) };
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
async function readEncodedProjectWorkspaces(args, kind, root) {
|
|
846
|
+
let entries = []; try { entries = fs.readdirSync(args.root ?? root, { withFileTypes: true }); } catch { return { workspaces: [], warnings: [], truncated: false }; }
|
|
847
|
+
const paths = entries.filter((e) => e.isDirectory()).map((e) => path.resolve("/" + e.name.replace(/^-/, "").replace(/-/g, path.sep)));
|
|
848
|
+
return { workspaces: await workspaceEntries(paths, RUNTIME_META[kind], args.limit, args.includeBranch), warnings: [], truncated: paths.length > clampLimit(args.limit) };
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function findFiles(root, predicate) {
|
|
852
|
+
const files = [];
|
|
853
|
+
const visit = (dir) => { let entries; try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; } for (const entry of entries) { const item = path.join(dir, entry.name); if (entry.isDirectory()) visit(item); else if (entry.isFile() && predicate(entry.name)) files.push(item); } };
|
|
854
|
+
visit(root); return files;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
async function readKimiWorkspaces(args) {
|
|
858
|
+
const root = args.root ?? path.join(os.homedir(), ".kimi-code", "sessions");
|
|
859
|
+
const paths = [];
|
|
860
|
+
for (const file of findFiles(root, (name) => name === "state.json")) { try { const cwd = JSON.parse(fs.readFileSync(file, "utf8"))?.cwd; if (typeof cwd === "string" && cwd) paths.push(cwd); } catch {} }
|
|
861
|
+
return { workspaces: await workspaceEntries(paths, RUNTIME_META.Kimi, args.limit, args.includeBranch), warnings: [], truncated: paths.length > clampLimit(args.limit) };
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
async function readCodeBuddyWorkspaces(args) {
|
|
865
|
+
const root = args.root ?? path.join(os.homedir(), ".codebuddy", "projects");
|
|
866
|
+
const paths = [];
|
|
867
|
+
for (const file of findFiles(root, (name) => name.endsWith(".jsonl"))) { try { for (const line of fs.readFileSync(file, "utf8").split(/\r?\n/)) { const cwd = JSON.parse(line || "{}")?.cwd; if (typeof cwd === "string" && cwd) paths.push(cwd); } } catch {} }
|
|
868
|
+
return { workspaces: await workspaceEntries(paths, RUNTIME_META.CodeBuddy, args.limit, args.includeBranch), warnings: [], truncated: paths.length > clampLimit(args.limit) };
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
async function readCursorWorkspaces(args) {
|
|
872
|
+
const root = args.root ?? path.join(os.homedir(), ".cursor", "projects");
|
|
873
|
+
let entries = []; try { entries = fs.readdirSync(root, { withFileTypes: true }); } catch { return { workspaces: [], warnings: [], truncated: false }; }
|
|
874
|
+
const projects = entries.filter((entry) =>
|
|
875
|
+
entry.isDirectory() && fs.existsSync(path.join(root, entry.name, "agent-transcripts")),
|
|
876
|
+
);
|
|
877
|
+
const paths = [];
|
|
878
|
+
let unresolvedCount = 0;
|
|
879
|
+
for (const project of projects) {
|
|
880
|
+
const resolved = resolveCursorProjectSlug(project.name);
|
|
881
|
+
if (resolved) paths.push(resolved);
|
|
882
|
+
else unresolvedCount += 1;
|
|
883
|
+
}
|
|
884
|
+
const warnings = unresolvedCount > 0
|
|
885
|
+
? [`${unresolvedCount} Cursor project${unresolvedCount === 1 ? " was" : "s were"} skipped because ${unresolvedCount === 1 ? "its" : "their"} slug could not be resolved to an existing directory`]
|
|
886
|
+
: [];
|
|
887
|
+
return { workspaces: await workspaceEntries(paths, RUNTIME_META.Cursor, args.limit, args.includeBranch), warnings, truncated: paths.length > clampLimit(args.limit) };
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// Cursor replaces path separators and punctuation (`/`, `-`, `_`, `.`) with
|
|
891
|
+
// the same `-` in project directory names. That encoding is lossy: never turn
|
|
892
|
+
// every dash back into `/`. Instead, walk the real directory tree from `/` and
|
|
893
|
+
// accept a candidate only when every component exists on disk.
|
|
894
|
+
const CURSOR_SLUG_JOINERS = ["-", "_", ".", ""];
|
|
895
|
+
function resolveCursorProjectSlug(slug) {
|
|
896
|
+
const segments = String(slug ?? "").replace(/^-+/, "").split("-").filter(Boolean);
|
|
897
|
+
if (segments.length === 0) return null;
|
|
898
|
+
|
|
899
|
+
const walk = (base, index) => {
|
|
900
|
+
if (index >= segments.length) return isExistingDirectory(base) ? base : null;
|
|
901
|
+
for (let end = segments.length; end > index; end -= 1) {
|
|
902
|
+
const parts = segments.slice(index, end);
|
|
903
|
+
for (const joiner of CURSOR_SLUG_JOINERS) {
|
|
904
|
+
const candidate = path.join(base, parts.join(joiner));
|
|
905
|
+
if (!isExistingDirectory(candidate)) continue;
|
|
906
|
+
const resolved = walk(candidate, end);
|
|
907
|
+
if (resolved) return resolved;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
return null;
|
|
911
|
+
};
|
|
912
|
+
|
|
913
|
+
return walk(path.parse(process.cwd()).root, 0);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function isExistingDirectory(candidate) {
|
|
917
|
+
try { return fs.statSync(candidate).isDirectory(); } catch { return false; }
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
async function readSqlitePaths(dbPath, sql) {
|
|
921
|
+
try { const { stdout } = await execFileAsync("sqlite3", [dbPath, "-noheader", "-separator", "\t", sql], { timeout: HARD_TIMEOUT_MS }); return stdout.split(/\r?\n/).filter(Boolean).map((line) => line.split("\t")[0]).filter(Boolean); } catch { return []; }
|
|
922
|
+
}
|
|
923
|
+
async function readHermesDbWorkspaces(args) {
|
|
924
|
+
const db = args.hermesDbPath ?? path.join(os.homedir(), ".hermes", "state.db");
|
|
925
|
+
const paths = await readSqlitePaths(db, "SELECT cwd FROM sessions WHERE cwd IS NOT NULL AND cwd != '' GROUP BY cwd ORDER BY COUNT(*) DESC");
|
|
926
|
+
return { workspaces: await workspaceEntries(paths, RUNTIME_META.Hermes, args.limit, args.includeBranch), warnings: [], truncated: paths.length > clampLimit(args.limit) };
|
|
927
|
+
}
|
|
928
|
+
async function readOpenCodeWorkspaces(args) {
|
|
929
|
+
const db = args.opencodeDbPath ?? path.join(os.homedir(), ".local", "share", "opencode", "opencode.db");
|
|
930
|
+
const paths = await readSqlitePaths(db, "SELECT worktree FROM project WHERE worktree IS NOT NULL AND worktree != '' GROUP BY worktree ORDER BY MAX(time_updated) DESC");
|
|
931
|
+
return { workspaces: await workspaceEntries(paths, RUNTIME_META.OpenCode, args.limit, args.includeBranch), warnings: [], truncated: paths.length > clampLimit(args.limit) };
|
|
932
|
+
}
|
|
933
|
+
|
|
794
934
|
/**
|
|
795
935
|
* Resolve OpenClaw home dir. Mirrors bin/agentlink resolveOpenClawHome():
|
|
796
936
|
* OPENCLAW_HOME 环境变量优先,否则 <home>/.openclaw。
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Injection points for testing:
|
|
11
11
|
* producerFactory({ relayUrl, bridgeToken, threadId }) → { postEvents }
|
|
12
|
-
* openclawInvoke({ input, onChunk }) → async void (default:
|
|
12
|
+
* openclawInvoke({ input, onChunk }) → async void (default: openclaw wsHolder)
|
|
13
13
|
* flushPolicy({ size, timeMs }) → 控制 micro-batched flush 触发阈值(默认 {size:1, timeMs:0} —— 即来即发,保流式体验)
|
|
14
14
|
*/
|
|
15
15
|
|
|
@@ -29,7 +29,7 @@ import { signGetCosObject } from "./cosUploadClient.mjs";
|
|
|
29
29
|
// Streaming-first policy: size:1 means every envelope (especially
|
|
30
30
|
// content_block.delta) posts immediately, so the App SSE stream receives
|
|
31
31
|
// chunks as the runtime produces them rather than in 8-envelope batches.
|
|
32
|
-
// Tests
|
|
32
|
+
// Tests can override via flushPolicy if they want fewer POSTs.
|
|
33
33
|
const DEFAULT_FLUSH_POLICY = Object.freeze({ size: 1, timeMs: 0 });
|
|
34
34
|
const STREAM_DEBUG = process.env.BUBBO_DEBUG_STREAM === "1";
|
|
35
35
|
|
|
@@ -265,7 +265,7 @@ export function createUnifiedDispatchHandler({
|
|
|
265
265
|
return false; // not a dispatch request — let caller handle it
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
-
const { thread_id, run_id, input, blocks = [], workspace_path } = request.request ?? {};
|
|
268
|
+
const { thread_id, run_id, input, blocks = [], workspace_path, resume_session_id } = request.request ?? {};
|
|
269
269
|
|
|
270
270
|
// Idempotency check
|
|
271
271
|
if (dispatchedRunIds.has(run_id)) {
|
|
@@ -343,7 +343,7 @@ export function createUnifiedDispatchHandler({
|
|
|
343
343
|
let runPausedFromRuntime = false;
|
|
344
344
|
|
|
345
345
|
if (typeof openclawInvoke === "function") {
|
|
346
|
-
// Injected (test mode or
|
|
346
|
+
// Injected (test mode or ws integration)
|
|
347
347
|
// thread_id 透传给 runtime adapter,让需要"按 thread 维护原生 session
|
|
348
348
|
// 续接"的 runtime(如 claude --resume <sid>)可以做 mapping。
|
|
349
349
|
await openclawInvoke({
|
|
@@ -352,6 +352,10 @@ export function createUnifiedDispatchHandler({
|
|
|
352
352
|
input: effectiveInput,
|
|
353
353
|
blocks: processedBlocks,
|
|
354
354
|
workspace_path: workspace_path || null,
|
|
355
|
+
// relay only supplies this for the newest completed history import.
|
|
356
|
+
// Runtime adapters seed it only if their thread has no active local
|
|
357
|
+
// session, preventing later imported history from switching context.
|
|
358
|
+
resume_session_id: resume_session_id || null,
|
|
355
359
|
// relay 信封顶层 sessionKey(App 侧 abort RPC 用它定位要杀的 run)
|
|
356
360
|
session_key: request.sessionKey || null,
|
|
357
361
|
onChunk: async (chunk) => {
|
|
@@ -423,8 +427,11 @@ export function createUnifiedDispatchHandler({
|
|
|
423
427
|
});
|
|
424
428
|
}
|
|
425
429
|
try {
|
|
430
|
+
const failureCode = err?.code === "workspace_context_unavailable"
|
|
431
|
+
? "workspace_context_unavailable"
|
|
432
|
+
: "runtime.crash";
|
|
426
433
|
await producer.postEvents([
|
|
427
|
-
buildRunFailed(run_id,
|
|
434
|
+
buildRunFailed(run_id, failureCode, String(err?.message || err)),
|
|
428
435
|
]);
|
|
429
436
|
} catch (postErr) {
|
|
430
437
|
log.error("dispatch.post_failed.error", "failed to post run.failed envelope", {
|
|
@@ -40,8 +40,7 @@ export function createUsageReporter({ publishEvent, runtime, log, debounceMs = D
|
|
|
40
40
|
if (
|
|
41
41
|
runtime !== "claude" &&
|
|
42
42
|
runtime !== "codex" &&
|
|
43
|
-
runtime !== "hermes"
|
|
44
|
-
runtime !== "picoclaw"
|
|
43
|
+
runtime !== "hermes"
|
|
45
44
|
) {
|
|
46
45
|
throw new Error(`createUsageReporter: bad runtime ${runtime}`);
|
|
47
46
|
}
|
|
@@ -448,7 +448,7 @@ function translateOpenClawChunkToEnvelope(chunk, runId, blockState) {
|
|
|
448
448
|
}
|
|
449
449
|
envelopes.push(env);
|
|
450
450
|
} else if (event === "response.function_call_arguments.delta") {
|
|
451
|
-
//
|
|
451
|
+
// legacy event name → maps to tool_use.start + arguments.delta.
|
|
452
452
|
// chunk shape: { call_id, name, arguments } where arguments is a JSON
|
|
453
453
|
// fragment to append. We auto-start a tool_use block on first occurrence.
|
|
454
454
|
const callId = String(chunk.call_id || "");
|
|
@@ -540,7 +540,7 @@ function translateOpenClawChunkToEnvelope(chunk, runId, blockState) {
|
|
|
540
540
|
envelopes.push(started, completed);
|
|
541
541
|
} else if (event === "response.output_text.done") {
|
|
542
542
|
// No incremental envelope needed — full text already accumulated via deltas.
|
|
543
|
-
// If runtime carries authoritative full text on .done
|
|
543
|
+
// If the runtime carries authoritative full text on .done, prefer it.
|
|
544
544
|
if (typeof chunk.text === "string" && chunk.text.length > 0) {
|
|
545
545
|
text.content = chunk.text;
|
|
546
546
|
}
|
|
@@ -75,7 +75,7 @@ function normalizeStatus(raw) {
|
|
|
75
75
|
* rawItems 中每项支持字段:
|
|
76
76
|
* - id?: string(有则保留,无则生成 td_{idx})
|
|
77
77
|
* - content?: string(主内容字段,Claude/Hermes 用)
|
|
78
|
-
* - step?: string(Codex
|
|
78
|
+
* - step?: string(Codex 用,作为 content 的 fallback)
|
|
79
79
|
* - status?: string(经 normalizeStatus 归一)
|
|
80
80
|
*
|
|
81
81
|
* @param {Array<{id?: string, content?: string, step?: string, status?: string}>} rawItems
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// 桥接会话账本 —— 「重复导入」问题的方案 ①(打标侧)。
|
|
2
|
+
//
|
|
3
|
+
// 背景:经 AgentLink 桥接的实时对话会同时被 CLI 记进本机会话文件,且该文件与
|
|
4
|
+
// 用户手动 CLI 会话**无任何可区分标记**(实测 CodeBuddy 两类文件的
|
|
5
|
+
// `providerData.agent` 均为 "cli")。桌面端「重新导入历史」会把已经通过实时
|
|
6
|
+
// 链路存在于线程里的同一段话再追加一遍。
|
|
7
|
+
//
|
|
8
|
+
// 方案:bridge 每捕获到一个 CLI 原生 session id(onSession / resume_hint),
|
|
9
|
+
// 就把 (runtime, session_id) 追加进本账本;桌面端导入扫描时读它,命中的
|
|
10
|
+
// 会话文件整个跳过 —— 那段对话已经在线程里了。
|
|
11
|
+
//
|
|
12
|
+
// 三条口径:
|
|
13
|
+
// 1. 账本位置**固定** `~/.agentlink/bridged-cli-sessions.ndjson`,不跟 bridge
|
|
14
|
+
// 的 --debug state 走:CLI 本机历史本来就不分 debug,账本也不该分。
|
|
15
|
+
// 2. 打标是 best-effort:写失败(磁盘满 / 权限)静默吞掉,绝不影响对话链路;
|
|
16
|
+
// 漏标的代价只是「那一条会话可能被重复导入」,远小于打断实时对话。
|
|
17
|
+
// 3. 只追加不清理:账本行是 40 字节量级,一万条才 400KB,不值得引入压缩逻辑。
|
|
18
|
+
|
|
19
|
+
import fs from "node:fs";
|
|
20
|
+
import os from "node:os";
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
|
|
23
|
+
/** 测试用环境变量:覆盖账本路径(生产不设,走 ~/.agentlink 固定位置)。 */
|
|
24
|
+
const LEDGER_PATH_ENV = "AGENTLINK_BRIDGED_LEDGER";
|
|
25
|
+
|
|
26
|
+
/** 进程内已记录的 session id —— 同一会话每轮对话都会报同一个 id,只落一行。 */
|
|
27
|
+
const recorded = new Set();
|
|
28
|
+
|
|
29
|
+
export function bridgedSessionLedgerPath() {
|
|
30
|
+
const override = process.env[LEDGER_PATH_ENV];
|
|
31
|
+
if (override) return override;
|
|
32
|
+
return path.join(os.homedir(), ".agentlink", "bridged-cli-sessions.ndjson");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 记录一个经桥接产生的 CLI 原生 session id。幂等(进程内去重 + 跨进程靠
|
|
37
|
+
* 桌面端读取时按 session_id 建集合,重复行无害)。
|
|
38
|
+
*/
|
|
39
|
+
export function recordBridgedSession(runtime, sessionId) {
|
|
40
|
+
const id = String(sessionId || "").trim();
|
|
41
|
+
if (!id) return;
|
|
42
|
+
const key = `${runtime}${id}`;
|
|
43
|
+
if (recorded.has(key)) return;
|
|
44
|
+
recorded.add(key);
|
|
45
|
+
try {
|
|
46
|
+
const ledger = bridgedSessionLedgerPath();
|
|
47
|
+
fs.mkdirSync(path.dirname(ledger), { recursive: true });
|
|
48
|
+
fs.appendFileSync(
|
|
49
|
+
ledger,
|
|
50
|
+
`${JSON.stringify({ runtime: String(runtime || ""), session_id: id, recorded_at: new Date().toISOString() })}\n`,
|
|
51
|
+
);
|
|
52
|
+
} catch {
|
|
53
|
+
// best-effort:见文件头口径 2。
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 测试用:清空进程内去重缓存(账本文件由测试自己管理)。 */
|
|
58
|
+
export function resetBridgedSessionCacheForTest() {
|
|
59
|
+
recorded.clear();
|
|
60
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Qwen Code 与 CodeBuddy 的 stream-json 都采用 Claude 风格消息信封:
|
|
3
|
+
* system(init) → assistant/user 消息 → result。这里直接翻成 unified chunks,
|
|
4
|
+
* 避免把两套官方兼容协议各写一遍。
|
|
5
|
+
*/
|
|
6
|
+
export function translateClaudeSchemaEvent(event) {
|
|
7
|
+
if (!event || typeof event !== "object") return null;
|
|
8
|
+
if (event.type === "result") {
|
|
9
|
+
if (event.is_error || event.subtype === "error") {
|
|
10
|
+
const message = event.error?.message || event.result || "runtime failed";
|
|
11
|
+
return { event: "response.failed", error: { message: String(message) } };
|
|
12
|
+
}
|
|
13
|
+
return { event: "response.completed" };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const message = event.message;
|
|
17
|
+
const blocks = Array.isArray(message?.content) ? message.content : [];
|
|
18
|
+
const parent = event.parent_tool_use_id || null;
|
|
19
|
+
const withParent = (chunk) => parent ? { ...chunk, parent_tool_use_id: parent } : chunk;
|
|
20
|
+
const chunks = [];
|
|
21
|
+
|
|
22
|
+
if (event.type === "assistant") {
|
|
23
|
+
for (const block of blocks) {
|
|
24
|
+
if (block?.type === "text" && block.text) {
|
|
25
|
+
chunks.push(withParent({ event: "response.output_text.delta", delta: String(block.text) }));
|
|
26
|
+
} else if ((block?.type === "thinking" || block?.type === "reasoning") && block.text) {
|
|
27
|
+
chunks.push(withParent({ event: "response.reasoning_summary_text.delta", delta: String(block.text) }));
|
|
28
|
+
} else if (block?.type === "tool_use") {
|
|
29
|
+
const callId = String(block.id || "");
|
|
30
|
+
const name = String(block.name || "unknown");
|
|
31
|
+
const input = block.input && typeof block.input === "object" ? block.input : {};
|
|
32
|
+
chunks.push(withParent({ event: "response.tool_use.start", call_id: callId, name, input }));
|
|
33
|
+
chunks.push(withParent({ event: "response.tool_use.completed", call_id: callId, name, input }));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} else if (event.type === "user") {
|
|
37
|
+
for (const block of blocks) {
|
|
38
|
+
if (block?.type !== "tool_result") continue;
|
|
39
|
+
const text = typeof block.content === "string" ? block.content : "";
|
|
40
|
+
chunks.push(withParent({
|
|
41
|
+
event: "response.tool_result",
|
|
42
|
+
call_id: String(block.tool_use_id || ""),
|
|
43
|
+
output: text ? [{ type: "text", format: "plain", text }] : [],
|
|
44
|
+
is_error: !!block.is_error,
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return chunks;
|
|
49
|
+
}
|