alink-cli 0.6.0 → 0.6.2
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 +12 -1
- package/bin/agentlink.js +69 -6
- package/dist/daemon.js +300 -303
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -12,12 +12,20 @@ One command on your work machine links it to AgentLink so you can drive your loc
|
|
|
12
12
|
npx alink-cli --token al1.xxxxxxxx
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
首次成功连上后,凭证会存到 `~/.agentlink/credential`(权限 `0600`)。之后在同一台机器上**直接裸跑**即可,命令行 token
|
|
15
|
+
首次成功连上后,凭证会存到 `~/.agentlink/credential`(权限 `0600`)。之后在同一台机器上**直接裸跑**即可,命令行 token 只需出现这一次;裸跑会默认转入后台,日志在 `~/.agentlink/daemon.log`:
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
18
|
npx alink-cli
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
需要手动改凭证时:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx alink-cli credential set al1.xxxxxxxx
|
|
25
|
+
npx alink-cli credential show
|
|
26
|
+
npx alink-cli credential clear
|
|
27
|
+
```
|
|
28
|
+
|
|
21
29
|
## 参数 Options
|
|
22
30
|
|
|
23
31
|
| 参数 | 说明 | 默认值 |
|
|
@@ -26,6 +34,9 @@ npx alink-cli
|
|
|
26
34
|
| `--hub <wss://…>` | 要连接的 hub 地址。 | `wss://link.harmopath.com`(官方 hub) |
|
|
27
35
|
| `--dir <path>` | 工作目录根,可重复给多次;运行时可用其下任意子目录,网页里以目录树浏览。 | 当前目录 `process.cwd()` |
|
|
28
36
|
| `--tunnel` | 不连任何 hub:本地起一个 hub + 免费 Cloudflare 隧道,打印公网链接(需 `cloudflared`)。与 `--hub` 互斥。 | — |
|
|
37
|
+
| `credential [status|path|show|set|clear]` | 查看路径、明文显示、手动覆盖或删除本机保存的凭证。 | `~/.agentlink/credential` |
|
|
38
|
+
| `qr` | 在终端打印本机凭证二维码,同时输出整串凭证供手动粘贴。 | 读取已保存凭证 |
|
|
39
|
+
| `status` / `logs -f` / `stop` / `restart` | 查看、跟随日志、停止或重启后台 daemon。 | — |
|
|
29
40
|
| `--help`, `-h` | 显示用法。 | — |
|
|
30
41
|
|
|
31
42
|
## 自建 hub Self-hosting
|
package/bin/agentlink.js
CHANGED
|
@@ -22,7 +22,7 @@ import { homedir } from "node:os";
|
|
|
22
22
|
import { dirname, join } from "node:path";
|
|
23
23
|
import { fileURLToPath } from "node:url";
|
|
24
24
|
|
|
25
|
-
const OFFICIAL_HUB = "wss://link.harmopath.com";
|
|
25
|
+
const OFFICIAL_HUB = process.env.AGENTLINK_DEFAULT_HUB || "wss://link.harmopath.com";
|
|
26
26
|
|
|
27
27
|
// 凭证文件与 daemon 的状态目录同根(默认 ~/.agentlink,可用 AGENTLINK_STATE_DIR
|
|
28
28
|
// 覆盖,测试用),文件名固定为 credential。
|
|
@@ -68,6 +68,51 @@ function persistCredential(token) {
|
|
|
68
68
|
chmodSync(CREDENTIAL_FILE, 0o600);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
if (rawArgs[0] === "credential" || rawArgs[0] === "cred") {
|
|
72
|
+
ctlCredential(rawArgs.slice(1));
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function ctlCredential(args) {
|
|
77
|
+
const cmd = args[0] || "status";
|
|
78
|
+
if (cmd === "path") {
|
|
79
|
+
console.log(CREDENTIAL_FILE);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (cmd === "status") {
|
|
83
|
+
console.log(`[agentlink] 凭证文件: ${CREDENTIAL_FILE}`);
|
|
84
|
+
console.log(readCredentialFile() ? "[agentlink] 已保存凭证。用 credential show 查看,credential set 修改。" : "[agentlink] 未保存凭证。用 credential set <凭证> 写入。");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (cmd === "show") {
|
|
88
|
+
const token = readCredentialFile();
|
|
89
|
+
if (!token) {
|
|
90
|
+
console.error("[agentlink] 还没有保存凭证。");
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
console.log(token);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (cmd === "set") {
|
|
97
|
+
const raw = args[1] === "-" ? readFileSync(0, "utf-8") : args[1];
|
|
98
|
+
const token = raw?.trim();
|
|
99
|
+
if (!token || /\s/.test(token)) {
|
|
100
|
+
console.error("[agentlink] 用法: alink-cli credential set <al1.…> 或 alink-cli credential set - < token.txt");
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
persistCredential(token);
|
|
104
|
+
console.log(`[agentlink] 凭证已保存到 ${CREDENTIAL_FILE}(0600)。`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (cmd === "clear") {
|
|
108
|
+
rmSync(CREDENTIAL_FILE, { force: true });
|
|
109
|
+
console.log(`[agentlink] 已删除 ${CREDENTIAL_FILE}。`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
console.error("[agentlink] 用法: alink-cli credential [status|path|show|set|clear]");
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
|
|
71
116
|
// `qr` 子命令:在终端打印本机凭证的二维码(含 E2EE 密钥段)+ 指纹——给新手机
|
|
72
117
|
// 的「录入密钥」扫。凭证从 --token / AGENTLINK_TOKEN / 落盘文件取,不联网。
|
|
73
118
|
if (rawArgs[0] === "qr") {
|
|
@@ -87,6 +132,9 @@ if (rawArgs[0] === "qr") {
|
|
|
87
132
|
const fp = createHash("sha256").update(Buffer.from(enckey, "base64url")).digest("hex").slice(0, 8);
|
|
88
133
|
console.log("用手机 AgentLink 的「录入密钥」扫下面的二维码:\n");
|
|
89
134
|
renderQr(token);
|
|
135
|
+
console.log("\n凭证信息(无法扫码时可手动粘贴):\n");
|
|
136
|
+
console.log(token);
|
|
137
|
+
console.log("");
|
|
90
138
|
console.log(`密钥指纹 ${fp} —— 与手机上显示的一致即可放心保存。`);
|
|
91
139
|
process.exit(0);
|
|
92
140
|
}
|
|
@@ -95,7 +143,8 @@ if (rawArgs[0] === "qr") {
|
|
|
95
143
|
// 后台管理子命令:start / stop / status / logs / restart。
|
|
96
144
|
// `start` 把 daemon 挂到后台(detached,日志写 ~/.agentlink/daemon.log,进程
|
|
97
145
|
// 元数据写 daemon.json),其余子命令围着这两个文件 + daemon 自己维护的
|
|
98
|
-
// status.json
|
|
146
|
+
// status.json 转。首次无凭证裸跑仍是前台——扫码配对必须能看见终端里的二维码;
|
|
147
|
+
// 已有凭证的裸跑默认转后台。
|
|
99
148
|
|
|
100
149
|
const DAEMON_JSON = join(STATE_DIR, "daemon.json");
|
|
101
150
|
const DAEMON_LOG = join(STATE_DIR, "daemon.log");
|
|
@@ -145,7 +194,7 @@ function sleep(ms) {
|
|
|
145
194
|
return new Promise((r) => setTimeout(r, ms));
|
|
146
195
|
}
|
|
147
196
|
|
|
148
|
-
async function ctlStart(args) {
|
|
197
|
+
async function ctlStart(args, { alreadyOk = false } = {}) {
|
|
149
198
|
if (hasFlag("tunnel", args)) {
|
|
150
199
|
console.error("[agentlink] --tunnel 模式带着 cloudflared 子进程,请前台运行(不支持 start)。");
|
|
151
200
|
process.exit(1);
|
|
@@ -156,7 +205,13 @@ async function ctlStart(args) {
|
|
|
156
205
|
}
|
|
157
206
|
const already = runningDaemon();
|
|
158
207
|
if (already) {
|
|
159
|
-
|
|
208
|
+
const msg = `[agentlink] 已在后台运行 (pid ${already.pid})。要重启用 alink-cli restart。`;
|
|
209
|
+
if (alreadyOk) {
|
|
210
|
+
console.log(msg);
|
|
211
|
+
console.log(`[agentlink] 日志: ${DAEMON_LOG}`);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
console.error(msg);
|
|
160
215
|
process.exit(1);
|
|
161
216
|
}
|
|
162
217
|
// 凭证前置检查:后台进程没法扫码配对(二维码只会写进日志文件),所以
|
|
@@ -175,7 +230,7 @@ async function ctlStart(args) {
|
|
|
175
230
|
const child = spawn(process.execPath, [fileURLToPath(import.meta.url), ...args], {
|
|
176
231
|
detached: true,
|
|
177
232
|
stdio: ["ignore", logFd, logFd],
|
|
178
|
-
env: process.env,
|
|
233
|
+
env: { ...process.env, AGENTLINK_FOREGROUND: "1" },
|
|
179
234
|
});
|
|
180
235
|
child.unref();
|
|
181
236
|
writeFileSync(DAEMON_JSON, JSON.stringify({ pid: child.pid, args, startedAt: Date.now() }));
|
|
@@ -308,6 +363,11 @@ async function ctlRestart(args) {
|
|
|
308
363
|
await ctlStart(startArgs);
|
|
309
364
|
}
|
|
310
365
|
|
|
366
|
+
if (rawArgs.length === 0 && !process.env.AGENTLINK_FOREGROUND && (process.env.AGENTLINK_TOKEN || readCredentialFile())) {
|
|
367
|
+
await ctlStart([], { alreadyOk: true });
|
|
368
|
+
process.exit(0);
|
|
369
|
+
}
|
|
370
|
+
|
|
311
371
|
// `local` 子命令 = 同机驾驶舱:本地起 single 模式 hub + daemon,自动开浏览器,
|
|
312
372
|
// 不遥控。归一成 daemon 认的 --local flag(daemon 参数不收 positional)。既接受
|
|
313
373
|
// `npx alink-cli local`,也接受 `npx alink-cli --local`。
|
|
@@ -390,13 +450,14 @@ function printHelp() {
|
|
|
390
450
|
`用法: npx ${self} [--hub <wss://…>] [--dir <目录>]…`,
|
|
391
451
|
` npx ${self} local [--dir …] # 同机驾驶舱:本地起 hub 并自动开浏览器,不遥控`,
|
|
392
452
|
` npx ${self} qr # 终端打印本机凭证二维码(给新手机录密钥)`,
|
|
453
|
+
` npx ${self} credential set <凭证> # 手动写入 / 修改本机保存的凭证`,
|
|
393
454
|
` npx ${self} start [--dir …] # 后台运行(先前台配对过一次)`,
|
|
394
455
|
` npx ${self} status|logs [-f]|stop|restart`,
|
|
395
456
|
``,
|
|
396
457
|
`把当前这台工作机接入 AgentLink,随时随地遥控本机的编码 agent。`,
|
|
397
458
|
`首次裸跑会进入扫码配对:加密密钥在这台电脑上生成,终端打出二维码,`,
|
|
398
459
|
`用手机 AgentLink 的「添加机器」扫一下即绑定——密钥只走二维码,不经过服务器。`,
|
|
399
|
-
|
|
460
|
+
`绑定后裸跑会默认挂到后台(日志 ~/.agentlink/daemon.log),status 看连接`,
|
|
400
461
|
`状态,logs -f 跟日志,stop/restart 停止或重启。`,
|
|
401
462
|
``,
|
|
402
463
|
`参数:`,
|
|
@@ -404,6 +465,8 @@ function printHelp() {
|
|
|
404
465
|
` --token <token> 直接给机器凭证(al1.… 整串,如从别处迁移)。首次成功连上后`,
|
|
405
466
|
` 存到 ~/.agentlink/credential(0600),之后裸跑自动复用。`,
|
|
406
467
|
` 也可用环境变量 AGENTLINK_TOKEN 提供(--token 优先)。`,
|
|
468
|
+
` credential 管理本机保存的凭证:status/path/show/set/clear。show 会明文打印`,
|
|
469
|
+
` 凭证;set 直接覆盖 ~/.agentlink/credential。`,
|
|
407
470
|
` --hub <url> hub 地址,缺省官方 hub ${OFFICIAL_HUB}。`,
|
|
408
471
|
` --dir <path> 工作目录根,可重复;缺省当前目录。运行时可用其下任意子目录。`,
|
|
409
472
|
` --tunnel 不连任何 hub:本地起 hub + 免费 Cloudflare 隧道并打印公网链接`,
|
package/dist/daemon.js
CHANGED
|
@@ -4899,6 +4899,7 @@ var claude = {
|
|
|
4899
4899
|
// configured with (settings.json / ~/.claude.json), merged ahead of these.
|
|
4900
4900
|
models: ["opus", "sonnet", "haiku"],
|
|
4901
4901
|
probeModels: async () => probeClaudeModels(),
|
|
4902
|
+
command: "claude",
|
|
4902
4903
|
acp: { bin: "claude-code-acp", args: [] }
|
|
4903
4904
|
};
|
|
4904
4905
|
|
|
@@ -4928,6 +4929,7 @@ var codex = {
|
|
|
4928
4929
|
// (incl. custom/proxy models), merged ahead of these aliases.
|
|
4929
4930
|
models: ["gpt-5-codex", "gpt-5", "o3"],
|
|
4930
4931
|
probeModels: async () => probeCodexModels(),
|
|
4932
|
+
command: "codex",
|
|
4931
4933
|
acp: { bin: "codex-acp", args: [] }
|
|
4932
4934
|
};
|
|
4933
4935
|
|
|
@@ -4962,7 +4964,8 @@ var REGISTRY = [claude, codex, gemini, qwen, kimi];
|
|
|
4962
4964
|
function detectAgents() {
|
|
4963
4965
|
return REGISTRY.flatMap((def) => {
|
|
4964
4966
|
const bin = resolveBin(def.acp.bin);
|
|
4965
|
-
|
|
4967
|
+
const command = def.command ? resolveBin(def.command) : bin;
|
|
4968
|
+
return bin && command ? [{ ...def, bin }] : [];
|
|
4966
4969
|
});
|
|
4967
4970
|
}
|
|
4968
4971
|
function getAgent(id) {
|
|
@@ -23197,7 +23200,12 @@ function toolContentText(content) {
|
|
|
23197
23200
|
}
|
|
23198
23201
|
return parts.filter(Boolean).join("\n");
|
|
23199
23202
|
}
|
|
23200
|
-
function
|
|
23203
|
+
function cleanTitle(v) {
|
|
23204
|
+
if (typeof v !== "string") return void 0;
|
|
23205
|
+
const title = v.split("\n").find((line) => line.trim())?.trim();
|
|
23206
|
+
return title ? title.slice(0, 200) : void 0;
|
|
23207
|
+
}
|
|
23208
|
+
function routeUpdate(update, onEvent, onTitle) {
|
|
23201
23209
|
switch (update.sessionUpdate) {
|
|
23202
23210
|
case "agent_message_chunk": {
|
|
23203
23211
|
const t = textOf(update.content);
|
|
@@ -23222,6 +23230,11 @@ function routeUpdate(update, onEvent) {
|
|
|
23222
23230
|
}
|
|
23223
23231
|
return;
|
|
23224
23232
|
}
|
|
23233
|
+
case "session_info_update": {
|
|
23234
|
+
const title = cleanTitle(update.title);
|
|
23235
|
+
if (title) onTitle?.(title);
|
|
23236
|
+
return;
|
|
23237
|
+
}
|
|
23225
23238
|
default:
|
|
23226
23239
|
return;
|
|
23227
23240
|
}
|
|
@@ -23254,7 +23267,7 @@ async function runAcp(o) {
|
|
|
23254
23267
|
Readable.toWeb(child.stdout)
|
|
23255
23268
|
);
|
|
23256
23269
|
const app = client({ name: "agentlink" }).onNotification("session/update", async (ctx) => {
|
|
23257
|
-
routeUpdate(ctx.params.update, o.onEvent);
|
|
23270
|
+
routeUpdate(ctx.params.update, o.onEvent, o.onTitle);
|
|
23258
23271
|
}).onRequest("session/request_permission", async (ctx) => handlePermission(ctx.params, o.requestPermission));
|
|
23259
23272
|
const killChild = () => {
|
|
23260
23273
|
try {
|
|
@@ -23479,76 +23492,9 @@ var RUNS_DIR = join4(STATE_DIR, "runs");
|
|
|
23479
23492
|
var CONVS_FILE = join4(STATE_DIR, "conversations.json");
|
|
23480
23493
|
var ENCKEYS_FILE = join4(STATE_DIR, "enckeys.json");
|
|
23481
23494
|
var MAX_RUN_BYTES = 512 * 1024;
|
|
23482
|
-
var MAX_RUNS_PER_CONVERSATION = 100;
|
|
23483
|
-
function isValidConversationId(id) {
|
|
23484
|
-
return typeof id === "string" && /^[A-Za-z0-9_-]{1,64}$/.test(id);
|
|
23485
|
-
}
|
|
23486
23495
|
function ensureDirs() {
|
|
23487
23496
|
mkdirSync(RUNS_DIR, { recursive: true });
|
|
23488
23497
|
}
|
|
23489
|
-
function loadConversations() {
|
|
23490
|
-
try {
|
|
23491
|
-
const parsed = JSON.parse(readFileSync3(CONVS_FILE, "utf-8"));
|
|
23492
|
-
return Array.isArray(parsed) ? parsed.filter((c) => isValidConversationId(c?.id)) : [];
|
|
23493
|
-
} catch {
|
|
23494
|
-
return [];
|
|
23495
|
-
}
|
|
23496
|
-
}
|
|
23497
|
-
function saveConversations(convs) {
|
|
23498
|
-
ensureDirs();
|
|
23499
|
-
const tmp = CONVS_FILE + ".tmp";
|
|
23500
|
-
writeFileSync(tmp, JSON.stringify(convs, null, 1));
|
|
23501
|
-
renameSync(tmp, CONVS_FILE);
|
|
23502
|
-
}
|
|
23503
|
-
function listConversations() {
|
|
23504
|
-
return loadConversations().sort((a, b) => b.lastActiveAt - a.lastActiveAt);
|
|
23505
|
-
}
|
|
23506
|
-
function putConversation(patch) {
|
|
23507
|
-
if (!isValidConversationId(patch?.id)) return { error: "conversation.id is missing or malformed" };
|
|
23508
|
-
const strOrUndef = (v) => typeof v === "string" ? v : void 0;
|
|
23509
|
-
const numOrUndef = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
23510
|
-
const convs = loadConversations();
|
|
23511
|
-
const existing = convs.find((c) => c.id === patch.id);
|
|
23512
|
-
const merged = {
|
|
23513
|
-
id: patch.id,
|
|
23514
|
-
agent: strOrUndef(patch.agent) ?? existing?.agent ?? "",
|
|
23515
|
-
dir: strOrUndef(patch.dir) ?? existing?.dir ?? "",
|
|
23516
|
-
title: strOrUndef(patch.title) ?? existing?.title ?? "",
|
|
23517
|
-
createdAt: numOrUndef(patch.createdAt) ?? existing?.createdAt ?? 0,
|
|
23518
|
-
lastActiveAt: numOrUndef(patch.lastActiveAt) ?? existing?.lastActiveAt ?? 0
|
|
23519
|
-
};
|
|
23520
|
-
const model = patch.model === "" ? void 0 : strOrUndef(patch.model) ?? existing?.model;
|
|
23521
|
-
if (model !== void 0) merged.model = model;
|
|
23522
|
-
const sessionId = patch.sessionId === "" ? void 0 : strOrUndef(patch.sessionId) ?? existing?.sessionId;
|
|
23523
|
-
if (sessionId !== void 0) merged.sessionId = sessionId;
|
|
23524
|
-
const archived = typeof patch.archived === "boolean" ? patch.archived : existing?.archived;
|
|
23525
|
-
if (archived === true) merged.archived = true;
|
|
23526
|
-
const takeover = typeof patch.takeover === "boolean" ? patch.takeover : existing?.takeover;
|
|
23527
|
-
if (takeover === true) merged.takeover = true;
|
|
23528
|
-
if (!merged.agent || !merged.dir || !merged.title || !merged.createdAt || !merged.lastActiveAt) {
|
|
23529
|
-
return { error: "creating a conversation requires agent, dir, title, createdAt and lastActiveAt" };
|
|
23530
|
-
}
|
|
23531
|
-
const next = existing ? convs.map((c) => c.id === merged.id ? merged : c) : [...convs, merged];
|
|
23532
|
-
saveConversations(next);
|
|
23533
|
-
return { conversation: merged };
|
|
23534
|
-
}
|
|
23535
|
-
function deleteConversation(id) {
|
|
23536
|
-
if (!isValidConversationId(id)) return;
|
|
23537
|
-
saveConversations(loadConversations().filter((c) => c.id !== id));
|
|
23538
|
-
rmSync(join4(RUNS_DIR, id), { recursive: true, force: true });
|
|
23539
|
-
}
|
|
23540
|
-
function clearConversations() {
|
|
23541
|
-
rmSync(CONVS_FILE, { force: true });
|
|
23542
|
-
rmSync(RUNS_DIR, { recursive: true, force: true });
|
|
23543
|
-
}
|
|
23544
|
-
function touchConversation(id, patch) {
|
|
23545
|
-
const convs = loadConversations();
|
|
23546
|
-
const conv = convs.find((c) => c.id === id);
|
|
23547
|
-
if (!conv) return;
|
|
23548
|
-
if (patch.lastActiveAt) conv.lastActiveAt = patch.lastActiveAt;
|
|
23549
|
-
if (patch.sessionId) conv.sessionId = patch.sessionId;
|
|
23550
|
-
saveConversations(convs);
|
|
23551
|
-
}
|
|
23552
23498
|
function saveEnckey(machineId, enckey) {
|
|
23553
23499
|
try {
|
|
23554
23500
|
ensureDirs();
|
|
@@ -23568,112 +23514,18 @@ function saveEnckey(machineId, enckey) {
|
|
|
23568
23514
|
return false;
|
|
23569
23515
|
}
|
|
23570
23516
|
}
|
|
23571
|
-
var openRuns = /* @__PURE__ */ new Set();
|
|
23572
|
-
function recordRun(conversationId, meta3) {
|
|
23573
|
-
if (!isValidConversationId(conversationId)) return null;
|
|
23574
|
-
const dir = join4(RUNS_DIR, conversationId);
|
|
23575
|
-
const startedAt = Date.now();
|
|
23576
|
-
const file2 = join4(dir, `${startedAt}-${meta3.runId}.jsonl`);
|
|
23577
|
-
try {
|
|
23578
|
-
mkdirSync(dir, { recursive: true });
|
|
23579
|
-
const files = readdirSync2(dir).filter((f) => f.endsWith(".jsonl")).sort();
|
|
23580
|
-
for (const old of files.slice(0, Math.max(0, files.length - (MAX_RUNS_PER_CONVERSATION - 1)))) {
|
|
23581
|
-
rmSync(join4(dir, old), { force: true });
|
|
23582
|
-
}
|
|
23583
|
-
appendFileSync(file2, JSON.stringify({ kind: "meta", startedAt, ...meta3 }) + "\n");
|
|
23584
|
-
} catch {
|
|
23585
|
-
return null;
|
|
23586
|
-
}
|
|
23587
|
-
openRuns.add(meta3.runId);
|
|
23588
|
-
touchConversation(conversationId, { lastActiveAt: startedAt });
|
|
23589
|
-
let bytes = 0;
|
|
23590
|
-
let truncated = false;
|
|
23591
|
-
const append = (line) => {
|
|
23592
|
-
try {
|
|
23593
|
-
appendFileSync(file2, JSON.stringify(line) + "\n");
|
|
23594
|
-
} catch {
|
|
23595
|
-
}
|
|
23596
|
-
};
|
|
23597
|
-
return {
|
|
23598
|
-
event(e, stored) {
|
|
23599
|
-
if (e.type === "session") touchConversation(conversationId, { sessionId: e.id });
|
|
23600
|
-
if (truncated) return;
|
|
23601
|
-
const line = JSON.stringify({ kind: "event", event: stored ?? e }) + "\n";
|
|
23602
|
-
bytes += line.length;
|
|
23603
|
-
if (bytes > MAX_RUN_BYTES) {
|
|
23604
|
-
truncated = true;
|
|
23605
|
-
append({ kind: "truncated" });
|
|
23606
|
-
return;
|
|
23607
|
-
}
|
|
23608
|
-
try {
|
|
23609
|
-
appendFileSync(file2, line);
|
|
23610
|
-
} catch {
|
|
23611
|
-
}
|
|
23612
|
-
},
|
|
23613
|
-
done(code, error51) {
|
|
23614
|
-
openRuns.delete(meta3.runId);
|
|
23615
|
-
append({ kind: "done", code, ...error51 !== void 0 ? { error: error51 } : {} });
|
|
23616
|
-
touchConversation(conversationId, { lastActiveAt: Date.now() });
|
|
23617
|
-
}
|
|
23618
|
-
};
|
|
23619
|
-
}
|
|
23620
|
-
function history(conversationId) {
|
|
23621
|
-
if (!isValidConversationId(conversationId)) return [];
|
|
23622
|
-
const dir = join4(RUNS_DIR, conversationId);
|
|
23623
|
-
if (!existsSync2(dir)) return [];
|
|
23624
|
-
const runs = [];
|
|
23625
|
-
for (const name of readdirSync2(dir).filter((f) => f.endsWith(".jsonl")).sort()) {
|
|
23626
|
-
let run = null;
|
|
23627
|
-
let text;
|
|
23628
|
-
try {
|
|
23629
|
-
text = readFileSync3(join4(dir, name), "utf-8");
|
|
23630
|
-
} catch {
|
|
23631
|
-
continue;
|
|
23632
|
-
}
|
|
23633
|
-
for (const raw of text.split("\n")) {
|
|
23634
|
-
if (!raw.trim()) continue;
|
|
23635
|
-
let line;
|
|
23636
|
-
try {
|
|
23637
|
-
line = JSON.parse(raw);
|
|
23638
|
-
} catch {
|
|
23639
|
-
continue;
|
|
23640
|
-
}
|
|
23641
|
-
if (line.kind === "meta") {
|
|
23642
|
-
run = {
|
|
23643
|
-
runId: line.runId,
|
|
23644
|
-
agent: line.agent,
|
|
23645
|
-
prompt: line.prompt,
|
|
23646
|
-
...line.cwd !== void 0 ? { cwd: line.cwd } : {},
|
|
23647
|
-
...line.model !== void 0 ? { model: line.model } : {},
|
|
23648
|
-
startedAt: line.startedAt,
|
|
23649
|
-
status: "disconnected",
|
|
23650
|
-
// upgraded below by done / openRuns
|
|
23651
|
-
truncated: false,
|
|
23652
|
-
events: []
|
|
23653
|
-
};
|
|
23654
|
-
} else if (run && line.kind === "event") {
|
|
23655
|
-
if ("type" in line.event && line.event.type === "session") run.sessionId = line.event.id;
|
|
23656
|
-
run.events.push(line.event);
|
|
23657
|
-
} else if (run && line.kind === "truncated") {
|
|
23658
|
-
run.truncated = true;
|
|
23659
|
-
} else if (run && line.kind === "done") {
|
|
23660
|
-
run.status = "done";
|
|
23661
|
-
run.code = line.code;
|
|
23662
|
-
if (line.error !== void 0) run.error = line.error;
|
|
23663
|
-
}
|
|
23664
|
-
}
|
|
23665
|
-
if (!run) continue;
|
|
23666
|
-
if (run.status !== "done" && openRuns.has(run.runId)) run.status = "running";
|
|
23667
|
-
if (run.status === "disconnected") run.error = "daemon disconnected";
|
|
23668
|
-
runs.push(run);
|
|
23669
|
-
}
|
|
23670
|
-
return runs;
|
|
23671
|
-
}
|
|
23672
23517
|
|
|
23673
23518
|
// src/sessions.ts
|
|
23674
|
-
import {
|
|
23519
|
+
import {
|
|
23520
|
+
closeSync,
|
|
23521
|
+
openSync,
|
|
23522
|
+
readFileSync as readFileSync4,
|
|
23523
|
+
readSync,
|
|
23524
|
+
readdirSync as readdirSync3,
|
|
23525
|
+
statSync
|
|
23526
|
+
} from "node:fs";
|
|
23675
23527
|
import { homedir as homedir5 } from "node:os";
|
|
23676
|
-
import { join as join5 } from "node:path";
|
|
23528
|
+
import { basename, join as join5 } from "node:path";
|
|
23677
23529
|
|
|
23678
23530
|
// ../core/src/session-archive.ts
|
|
23679
23531
|
function resultText(content) {
|
|
@@ -23802,7 +23654,8 @@ function parseClaudeArchive(text, opts) {
|
|
|
23802
23654
|
function claudeArchiveMeta(headText, tailText) {
|
|
23803
23655
|
let sessionId;
|
|
23804
23656
|
let cwd;
|
|
23805
|
-
let
|
|
23657
|
+
let promptTitle = "";
|
|
23658
|
+
let aiTitle = "";
|
|
23806
23659
|
for (const raw of headText.split("\n")) {
|
|
23807
23660
|
if (!raw.trim()) continue;
|
|
23808
23661
|
let obj;
|
|
@@ -23813,28 +23666,35 @@ function claudeArchiveMeta(headText, tailText) {
|
|
|
23813
23666
|
}
|
|
23814
23667
|
if (typeof obj.sessionId === "string" && !sessionId) sessionId = obj.sessionId;
|
|
23815
23668
|
if (typeof obj.cwd === "string" && !cwd) cwd = obj.cwd;
|
|
23816
|
-
if (
|
|
23669
|
+
if (obj.type === "ai-title" && typeof obj.title === "string" && obj.title.trim()) {
|
|
23670
|
+
aiTitle = firstLine(obj.title);
|
|
23671
|
+
}
|
|
23672
|
+
if (!promptTitle && obj.type === "user" && obj.message && typeof obj.message === "object") {
|
|
23817
23673
|
const msg = obj.message;
|
|
23818
|
-
if (isHumanPrompt(msg))
|
|
23674
|
+
if (isHumanPrompt(msg)) promptTitle = firstLine(userText(msg));
|
|
23819
23675
|
}
|
|
23820
|
-
if (sessionId && cwd &&
|
|
23676
|
+
if (sessionId && cwd && aiTitle) break;
|
|
23821
23677
|
}
|
|
23822
23678
|
let lastLine = "";
|
|
23679
|
+
let tailAiTitle = "";
|
|
23823
23680
|
const tailLines = tailText.split("\n").filter((l) => l.trim());
|
|
23824
|
-
for (let i = tailLines.length - 1; i >= 0 && !lastLine; i--) {
|
|
23681
|
+
for (let i = tailLines.length - 1; i >= 0 && (!lastLine || !tailAiTitle); i--) {
|
|
23825
23682
|
let obj;
|
|
23826
23683
|
try {
|
|
23827
23684
|
obj = JSON.parse(tailLines[i]);
|
|
23828
23685
|
} catch {
|
|
23829
23686
|
continue;
|
|
23830
23687
|
}
|
|
23688
|
+
if (!tailAiTitle && obj.type === "ai-title" && typeof obj.title === "string" && obj.title.trim()) {
|
|
23689
|
+
tailAiTitle = firstLine(obj.title);
|
|
23690
|
+
}
|
|
23831
23691
|
if ((obj.type === "assistant" || obj.type === "user") && obj.message && typeof obj.message === "object") {
|
|
23832
23692
|
const msg = obj.message;
|
|
23833
23693
|
const text = obj.type === "assistant" ? assistantText(msg) : isHumanPrompt(msg) ? userText(msg) : "";
|
|
23834
|
-
if (text.trim()) lastLine = firstLine(text);
|
|
23694
|
+
if (!lastLine && text.trim()) lastLine = firstLine(text);
|
|
23835
23695
|
}
|
|
23836
23696
|
}
|
|
23837
|
-
return { ...sessionId ? { sessionId } : {}, ...cwd ? { cwd } : {}, title, lastLine };
|
|
23697
|
+
return { ...sessionId ? { sessionId } : {}, ...cwd ? { cwd } : {}, title: tailAiTitle || aiTitle || promptTitle, lastLine };
|
|
23838
23698
|
}
|
|
23839
23699
|
function assistantText(message) {
|
|
23840
23700
|
const content = message.content;
|
|
@@ -23852,122 +23712,257 @@ function firstLine(s) {
|
|
|
23852
23712
|
const line = s.split("\n").find((l) => l.trim()) ?? "";
|
|
23853
23713
|
return line.trim();
|
|
23854
23714
|
}
|
|
23855
|
-
|
|
23856
|
-
|
|
23857
|
-
function claudeProjectsDir() {
|
|
23858
|
-
const base = process.env.AGENTLINK_CLAUDE_HOME || join5(homedir5(), ".claude");
|
|
23859
|
-
return join5(base, "projects");
|
|
23860
|
-
}
|
|
23861
|
-
var META_SLICE_BYTES = 64 * 1024;
|
|
23862
|
-
var MAX_SESSIONS = 40;
|
|
23863
|
-
var MAX_HISTORY_TURNS = 40;
|
|
23864
|
-
var SESSION_ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
|
|
23865
|
-
function readHeadTail(path, size) {
|
|
23866
|
-
if (size <= META_SLICE_BYTES * 2) {
|
|
23867
|
-
const whole = readFileSync4(path, "utf-8");
|
|
23868
|
-
return { head: whole, tail: whole };
|
|
23869
|
-
}
|
|
23870
|
-
const fd = openSync(path, "r");
|
|
23871
|
-
try {
|
|
23872
|
-
const headBuf = Buffer.alloc(META_SLICE_BYTES);
|
|
23873
|
-
readSync(fd, headBuf, 0, META_SLICE_BYTES, 0);
|
|
23874
|
-
const tailBuf = Buffer.alloc(META_SLICE_BYTES);
|
|
23875
|
-
readSync(fd, tailBuf, 0, META_SLICE_BYTES, size - META_SLICE_BYTES);
|
|
23876
|
-
const tail = tailBuf.toString("utf-8");
|
|
23877
|
-
return { head: headBuf.toString("utf-8"), tail: tail.slice(tail.indexOf("\n") + 1) };
|
|
23878
|
-
} finally {
|
|
23879
|
-
closeSync(fd);
|
|
23880
|
-
}
|
|
23715
|
+
function codexPayload(obj) {
|
|
23716
|
+
return obj.payload && typeof obj.payload === "object" ? obj.payload : null;
|
|
23881
23717
|
}
|
|
23882
|
-
function
|
|
23883
|
-
|
|
23884
|
-
|
|
23885
|
-
|
|
23886
|
-
|
|
23887
|
-
|
|
23888
|
-
|
|
23889
|
-
|
|
23890
|
-
|
|
23891
|
-
|
|
23892
|
-
|
|
23718
|
+
function codexMessage(payload) {
|
|
23719
|
+
return typeof payload.message === "string" ? payload.message : "";
|
|
23720
|
+
}
|
|
23721
|
+
function parseCodexArchive(text, opts) {
|
|
23722
|
+
const maxTurns = opts?.maxTurns ?? Infinity;
|
|
23723
|
+
const turns = [];
|
|
23724
|
+
let current = null;
|
|
23725
|
+
let sessionId;
|
|
23726
|
+
let cwd;
|
|
23727
|
+
let omitted = 0;
|
|
23728
|
+
const openTurn = (prompt, startedAt) => {
|
|
23729
|
+
current = { prompt, events: [], ...startedAt !== void 0 ? { startedAt } : {} };
|
|
23730
|
+
turns.push(current);
|
|
23731
|
+
};
|
|
23732
|
+
for (const raw of text.split("\n")) {
|
|
23733
|
+
if (!raw.trim()) continue;
|
|
23734
|
+
let obj;
|
|
23893
23735
|
try {
|
|
23894
|
-
|
|
23736
|
+
obj = JSON.parse(raw);
|
|
23895
23737
|
} catch {
|
|
23738
|
+
omitted++;
|
|
23896
23739
|
continue;
|
|
23897
23740
|
}
|
|
23898
|
-
|
|
23899
|
-
|
|
23900
|
-
|
|
23901
|
-
|
|
23902
|
-
|
|
23903
|
-
|
|
23904
|
-
|
|
23741
|
+
const payload = codexPayload(obj);
|
|
23742
|
+
if (!payload) continue;
|
|
23743
|
+
if (obj.type === "session_meta") {
|
|
23744
|
+
if (!sessionId && typeof payload.id === "string") sessionId = payload.id;
|
|
23745
|
+
if (!cwd && typeof payload.cwd === "string") cwd = payload.cwd;
|
|
23746
|
+
continue;
|
|
23747
|
+
}
|
|
23748
|
+
if (obj.type === "event_msg" && payload.type === "user_message") {
|
|
23749
|
+
const prompt = codexMessage(payload);
|
|
23750
|
+
if (prompt.trim()) openTurn(prompt, tsOf(obj));
|
|
23751
|
+
continue;
|
|
23752
|
+
}
|
|
23753
|
+
if (obj.type === "event_msg" && payload.type === "agent_message") {
|
|
23754
|
+
const message = codexMessage(payload);
|
|
23755
|
+
if (message) {
|
|
23756
|
+
if (!current) openTurn("", tsOf(obj));
|
|
23757
|
+
current.events.push({ type: "text", text: message });
|
|
23758
|
+
}
|
|
23759
|
+
continue;
|
|
23760
|
+
}
|
|
23761
|
+
if (obj.type !== "response_item") continue;
|
|
23762
|
+
const itemType = payload.type;
|
|
23763
|
+
if (itemType === "custom_tool_call" || itemType === "function_call") {
|
|
23764
|
+
if (!current) openTurn("", tsOf(obj));
|
|
23765
|
+
let input = payload.input ?? null;
|
|
23766
|
+
if (typeof input === "string") {
|
|
23767
|
+
try {
|
|
23768
|
+
input = JSON.parse(input);
|
|
23769
|
+
} catch {
|
|
23770
|
+
}
|
|
23905
23771
|
}
|
|
23772
|
+
current.events.push({
|
|
23773
|
+
type: "tool_use",
|
|
23774
|
+
id: String(payload.call_id ?? payload.id ?? ""),
|
|
23775
|
+
name: String(payload.name ?? "tool"),
|
|
23776
|
+
input
|
|
23777
|
+
});
|
|
23778
|
+
} else if (itemType === "custom_tool_call_output" || itemType === "function_call_output") {
|
|
23779
|
+
if (!current) openTurn("", tsOf(obj));
|
|
23780
|
+
current.events.push({
|
|
23781
|
+
type: "tool_result",
|
|
23782
|
+
toolUseId: String(payload.call_id ?? ""),
|
|
23783
|
+
content: resultText(payload.output),
|
|
23784
|
+
isError: false
|
|
23785
|
+
});
|
|
23906
23786
|
}
|
|
23907
23787
|
}
|
|
23908
|
-
|
|
23909
|
-
|
|
23910
|
-
|
|
23911
|
-
|
|
23788
|
+
const truncatedEarlier = turns.length > maxTurns;
|
|
23789
|
+
return {
|
|
23790
|
+
sessionId,
|
|
23791
|
+
cwd,
|
|
23792
|
+
turns: truncatedEarlier ? turns.slice(turns.length - maxTurns) : turns,
|
|
23793
|
+
omitted,
|
|
23794
|
+
truncatedEarlier
|
|
23795
|
+
};
|
|
23796
|
+
}
|
|
23797
|
+
function codexArchiveMeta(headText, tailText) {
|
|
23798
|
+
let sessionId;
|
|
23799
|
+
let cwd;
|
|
23800
|
+
let title = "";
|
|
23801
|
+
for (const raw of headText.split("\n")) {
|
|
23802
|
+
if (!raw.trim()) continue;
|
|
23912
23803
|
try {
|
|
23913
|
-
const
|
|
23914
|
-
|
|
23804
|
+
const obj = JSON.parse(raw);
|
|
23805
|
+
const payload = codexPayload(obj);
|
|
23806
|
+
if (!payload) continue;
|
|
23807
|
+
if (obj.type === "session_meta") {
|
|
23808
|
+
if (!sessionId && typeof payload.id === "string") sessionId = payload.id;
|
|
23809
|
+
if (!cwd && typeof payload.cwd === "string") cwd = payload.cwd;
|
|
23810
|
+
} else if (!title && obj.type === "event_msg" && payload.type === "user_message") {
|
|
23811
|
+
title = firstLine(codexMessage(payload));
|
|
23812
|
+
}
|
|
23813
|
+
} catch {
|
|
23814
|
+
}
|
|
23815
|
+
}
|
|
23816
|
+
let lastLine = "";
|
|
23817
|
+
const lines = tailText.split("\n").filter((line) => line.trim());
|
|
23818
|
+
for (let i = lines.length - 1; i >= 0 && !lastLine; i--) {
|
|
23819
|
+
try {
|
|
23820
|
+
const obj = JSON.parse(lines[i]);
|
|
23821
|
+
const payload = codexPayload(obj);
|
|
23822
|
+
if (payload && obj.type === "event_msg" && (payload.type === "agent_message" || payload.type === "user_message")) {
|
|
23823
|
+
lastLine = firstLine(codexMessage(payload));
|
|
23824
|
+
}
|
|
23825
|
+
} catch {
|
|
23826
|
+
}
|
|
23827
|
+
}
|
|
23828
|
+
return {
|
|
23829
|
+
...sessionId ? { sessionId } : {},
|
|
23830
|
+
...cwd ? { cwd } : {},
|
|
23831
|
+
title,
|
|
23832
|
+
lastLine
|
|
23833
|
+
};
|
|
23834
|
+
}
|
|
23835
|
+
|
|
23836
|
+
// src/sessions.ts
|
|
23837
|
+
var LIST_LIMIT = 100;
|
|
23838
|
+
var CANDIDATE_LIMIT = 300;
|
|
23839
|
+
var SLICE_BYTES = 64 * 1024;
|
|
23840
|
+
var HISTORY_TURNS = 100;
|
|
23841
|
+
function archiveRoot(agent) {
|
|
23842
|
+
if (agent === "claude") {
|
|
23843
|
+
return join5(process.env.AGENTLINK_CLAUDE_HOME || join5(homedir5(), ".claude"), "projects");
|
|
23844
|
+
}
|
|
23845
|
+
return join5(
|
|
23846
|
+
process.env.AGENTLINK_CODEX_HOME || process.env.CODEX_HOME || join5(homedir5(), ".codex"),
|
|
23847
|
+
"sessions"
|
|
23848
|
+
);
|
|
23849
|
+
}
|
|
23850
|
+
function validSessionId(id) {
|
|
23851
|
+
return /^[A-Za-z0-9._-]{1,128}$/.test(id);
|
|
23852
|
+
}
|
|
23853
|
+
function archiveFiles(root, maxDepth) {
|
|
23854
|
+
const files = [];
|
|
23855
|
+
const stack = [{ dir: root, depth: 0 }];
|
|
23856
|
+
while (stack.length > 0) {
|
|
23857
|
+
const next = stack.pop();
|
|
23858
|
+
let entries;
|
|
23859
|
+
try {
|
|
23860
|
+
entries = readdirSync3(next.dir, { withFileTypes: true });
|
|
23915
23861
|
} catch {
|
|
23916
23862
|
continue;
|
|
23917
23863
|
}
|
|
23918
|
-
|
|
23919
|
-
|
|
23920
|
-
|
|
23921
|
-
|
|
23922
|
-
|
|
23923
|
-
|
|
23924
|
-
|
|
23925
|
-
|
|
23864
|
+
for (const entry of entries) {
|
|
23865
|
+
const path = join5(next.dir, entry.name);
|
|
23866
|
+
if (entry.isDirectory() && next.depth < maxDepth) {
|
|
23867
|
+
stack.push({ dir: path, depth: next.depth + 1 });
|
|
23868
|
+
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
23869
|
+
try {
|
|
23870
|
+
files.push({ path, mtime: statSync(path).mtimeMs });
|
|
23871
|
+
} catch {
|
|
23872
|
+
}
|
|
23873
|
+
}
|
|
23874
|
+
}
|
|
23926
23875
|
}
|
|
23927
|
-
return
|
|
23876
|
+
return files.sort((a, b) => b.mtime - a.mtime).slice(0, CANDIDATE_LIMIT);
|
|
23928
23877
|
}
|
|
23929
|
-
function
|
|
23930
|
-
const
|
|
23931
|
-
const
|
|
23932
|
-
let projectDirs;
|
|
23878
|
+
function readHeadTail(path) {
|
|
23879
|
+
const size = statSync(path).size;
|
|
23880
|
+
const fd = openSync(path, "r");
|
|
23933
23881
|
try {
|
|
23934
|
-
|
|
23935
|
-
|
|
23936
|
-
|
|
23882
|
+
const headSize = Math.min(size, SLICE_BYTES);
|
|
23883
|
+
const tailSize = Math.min(size, SLICE_BYTES);
|
|
23884
|
+
const head = Buffer.alloc(headSize);
|
|
23885
|
+
const tail = Buffer.alloc(tailSize);
|
|
23886
|
+
readSync(fd, head, 0, headSize, 0);
|
|
23887
|
+
readSync(fd, tail, 0, tailSize, Math.max(0, size - tailSize));
|
|
23888
|
+
return {
|
|
23889
|
+
head: head.toString("utf8"),
|
|
23890
|
+
tail: tail.toString("utf8")
|
|
23891
|
+
};
|
|
23892
|
+
} finally {
|
|
23893
|
+
closeSync(fd);
|
|
23937
23894
|
}
|
|
23938
|
-
|
|
23939
|
-
|
|
23895
|
+
}
|
|
23896
|
+
function scanAgent(agent) {
|
|
23897
|
+
const root = archiveRoot(agent);
|
|
23898
|
+
const depth = agent === "claude" ? 2 : 4;
|
|
23899
|
+
const sessions = [];
|
|
23900
|
+
for (const file2 of archiveFiles(root, depth)) {
|
|
23940
23901
|
try {
|
|
23941
|
-
|
|
23902
|
+
const { head, tail } = readHeadTail(file2.path);
|
|
23903
|
+
const meta3 = agent === "claude" ? claudeArchiveMeta(head, tail) : codexArchiveMeta(head, tail);
|
|
23904
|
+
const filenameId = basename(file2.path, ".jsonl");
|
|
23905
|
+
const sessionId = meta3.sessionId || (agent === "codex" ? filenameId.match(/([A-Za-z0-9]+(?:-[A-Za-z0-9]+){4})$/)?.[1] : filenameId);
|
|
23906
|
+
if (!sessionId || !validSessionId(sessionId)) continue;
|
|
23907
|
+
sessions.push({
|
|
23908
|
+
agent,
|
|
23909
|
+
sessionId,
|
|
23910
|
+
...meta3.cwd ? { cwd: meta3.cwd } : {},
|
|
23911
|
+
title: meta3.title || "(\u65E0\u6807\u9898\u4F1A\u8BDD)",
|
|
23912
|
+
lastLine: meta3.lastLine,
|
|
23913
|
+
mtime: file2.mtime
|
|
23914
|
+
});
|
|
23942
23915
|
} catch {
|
|
23943
23916
|
}
|
|
23944
23917
|
}
|
|
23945
|
-
return
|
|
23918
|
+
return sessions;
|
|
23946
23919
|
}
|
|
23947
|
-
function
|
|
23948
|
-
|
|
23949
|
-
|
|
23950
|
-
|
|
23951
|
-
|
|
23952
|
-
|
|
23953
|
-
|
|
23954
|
-
|
|
23955
|
-
|
|
23956
|
-
|
|
23957
|
-
|
|
23958
|
-
const
|
|
23959
|
-
|
|
23960
|
-
|
|
23961
|
-
|
|
23920
|
+
function scanClaudeSessions() {
|
|
23921
|
+
return scanAgent("claude").slice(0, LIST_LIMIT);
|
|
23922
|
+
}
|
|
23923
|
+
function scanCodexSessions() {
|
|
23924
|
+
return scanAgent("codex").slice(0, LIST_LIMIT);
|
|
23925
|
+
}
|
|
23926
|
+
function scanNativeSessions() {
|
|
23927
|
+
return [...scanClaudeSessions(), ...scanCodexSessions()].sort((a, b) => b.mtime - a.mtime).slice(0, LIST_LIMIT);
|
|
23928
|
+
}
|
|
23929
|
+
function archivePath(agent, sessionId) {
|
|
23930
|
+
if (!validSessionId(sessionId)) return null;
|
|
23931
|
+
const suffix = agent === "claude" ? `${sessionId}.jsonl` : `-${sessionId}.jsonl`;
|
|
23932
|
+
return archiveFiles(archiveRoot(agent), agent === "claude" ? 2 : 4).find((file2) => file2.path.endsWith(suffix))?.path ?? null;
|
|
23933
|
+
}
|
|
23934
|
+
function runsFromParsed(agent, sessionId, parsed) {
|
|
23935
|
+
return parsed.turns.map((turn, index) => ({
|
|
23936
|
+
runId: `native-${agent}-${sessionId}-${index}`,
|
|
23937
|
+
agent,
|
|
23962
23938
|
prompt: turn.prompt,
|
|
23963
23939
|
...parsed.cwd ? { cwd: parsed.cwd } : {},
|
|
23964
23940
|
sessionId,
|
|
23965
23941
|
startedAt: turn.startedAt ?? 0,
|
|
23966
23942
|
status: "done",
|
|
23943
|
+
code: 0,
|
|
23967
23944
|
truncated: false,
|
|
23968
23945
|
events: turn.events
|
|
23969
23946
|
}));
|
|
23970
|
-
|
|
23947
|
+
}
|
|
23948
|
+
function nativeSessionHistory(agent, sessionId) {
|
|
23949
|
+
if (agent !== "claude" && agent !== "codex") {
|
|
23950
|
+
return { error: `session takeover does not support "${agent}"` };
|
|
23951
|
+
}
|
|
23952
|
+
const path = archivePath(agent, sessionId);
|
|
23953
|
+
if (!path) return { error: `${agent} session "${sessionId}" was not found` };
|
|
23954
|
+
try {
|
|
23955
|
+
const text = readFileSync4(path, "utf8");
|
|
23956
|
+
const parsed = agent === "claude" ? parseClaudeArchive(text, { maxTurns: HISTORY_TURNS }) : parseCodexArchive(text, { maxTurns: HISTORY_TURNS });
|
|
23957
|
+
return {
|
|
23958
|
+
runs: runsFromParsed(agent, sessionId, parsed),
|
|
23959
|
+
truncatedEarlier: parsed.truncatedEarlier
|
|
23960
|
+
};
|
|
23961
|
+
} catch (err) {
|
|
23962
|
+
return {
|
|
23963
|
+
error: `cannot read ${agent} session "${sessionId}": ${err instanceof Error ? err.message : String(err)}`
|
|
23964
|
+
};
|
|
23965
|
+
}
|
|
23971
23966
|
}
|
|
23972
23967
|
|
|
23973
23968
|
// src/tunnel.ts
|
|
@@ -24347,26 +24342,43 @@ async function probeVersion(bin) {
|
|
|
24347
24342
|
return first || void 0;
|
|
24348
24343
|
}
|
|
24349
24344
|
async function probeAgents() {
|
|
24350
|
-
|
|
24345
|
+
const agents2 = await Promise.all(
|
|
24351
24346
|
REGISTRY.map(async (def) => {
|
|
24352
|
-
const
|
|
24353
|
-
if (!
|
|
24347
|
+
const adapter = getAgent(def.id);
|
|
24348
|
+
if (!adapter) return { id: def.id, label: def.label, detected: false, models: def.models };
|
|
24354
24349
|
const [version2, probed] = await Promise.all([
|
|
24355
|
-
probeVersion(bin),
|
|
24356
|
-
def.probeModels ? def.probeModels(bin) : Promise.resolve([])
|
|
24350
|
+
probeVersion(resolveBin(def.command ?? def.acp.bin) ?? adapter.bin),
|
|
24351
|
+
def.probeModels ? def.probeModels(adapter.bin) : Promise.resolve([])
|
|
24357
24352
|
]);
|
|
24358
24353
|
const models = [.../* @__PURE__ */ new Set([...probed, ...def.models])];
|
|
24359
24354
|
return {
|
|
24360
24355
|
id: def.id,
|
|
24361
24356
|
label: def.label,
|
|
24362
24357
|
detected: true,
|
|
24363
|
-
bin,
|
|
24358
|
+
bin: adapter.bin,
|
|
24364
24359
|
models,
|
|
24365
24360
|
...version2 ? { version: version2 } : {},
|
|
24366
24361
|
...APPROVAL ? { approval: true } : {}
|
|
24367
24362
|
};
|
|
24368
24363
|
})
|
|
24369
24364
|
);
|
|
24365
|
+
let joycodeBin = resolveBin("joycode");
|
|
24366
|
+
if (!joycodeBin) {
|
|
24367
|
+
try {
|
|
24368
|
+
joycodeBin = realpathSync("/Applications/JoyCode.app/Contents/Resources/app/bin/joycode");
|
|
24369
|
+
} catch {
|
|
24370
|
+
}
|
|
24371
|
+
}
|
|
24372
|
+
if (joycodeBin) {
|
|
24373
|
+
agents2.push({
|
|
24374
|
+
id: "joycode",
|
|
24375
|
+
label: "JoyCode\uFF08\u5DF2\u5B89\u88C5\uFF0C\u6682\u65E0\u63A5\u7BA1\u63A5\u53E3\uFF09",
|
|
24376
|
+
detected: false,
|
|
24377
|
+
bin: joycodeBin,
|
|
24378
|
+
models: []
|
|
24379
|
+
});
|
|
24380
|
+
}
|
|
24381
|
+
return agents2;
|
|
24370
24382
|
}
|
|
24371
24383
|
var running = /* @__PURE__ */ new Map();
|
|
24372
24384
|
var approvalRuns = /* @__PURE__ */ new Map();
|
|
@@ -24397,7 +24409,7 @@ function handleRun(ws, { requestId, agent: agentId, prompt: rawPrompt, sessionId
|
|
|
24397
24409
|
};
|
|
24398
24410
|
const adapter = getAgent(agentId);
|
|
24399
24411
|
if (!adapter) return fail(`agent "${agentId}" not found`);
|
|
24400
|
-
const { plain: prompt
|
|
24412
|
+
const { plain: prompt } = decodePrompt(rawPrompt);
|
|
24401
24413
|
if (typeof prompt !== "string") {
|
|
24402
24414
|
return fail(ENCKEY ? `malformed run message: prompt could not be decrypted (wrong key?)` : `malformed run message: prompt is not a string`);
|
|
24403
24415
|
}
|
|
@@ -24407,13 +24419,6 @@ function handleRun(ws, { requestId, agent: agentId, prompt: rawPrompt, sessionId
|
|
|
24407
24419
|
if (!real) return fail(`cwd rejected: ${error51}`);
|
|
24408
24420
|
runCwd = real;
|
|
24409
24421
|
}
|
|
24410
|
-
const recorder = recordRun(conversationId, {
|
|
24411
|
-
runId: requestId,
|
|
24412
|
-
agent: agentId,
|
|
24413
|
-
prompt: storedPrompt,
|
|
24414
|
-
...cwd !== void 0 ? { cwd } : {},
|
|
24415
|
-
...typeof model === "string" && model ? { model } : {}
|
|
24416
|
-
});
|
|
24417
24422
|
runAcpPath(ws, {
|
|
24418
24423
|
requestId,
|
|
24419
24424
|
adapter,
|
|
@@ -24422,8 +24427,7 @@ function handleRun(ws, { requestId, agent: agentId, prompt: rawPrompt, sessionId
|
|
|
24422
24427
|
runCwd,
|
|
24423
24428
|
model: typeof model === "string" && model ? model : void 0,
|
|
24424
24429
|
conversationId,
|
|
24425
|
-
notifyDetail
|
|
24426
|
-
recorder
|
|
24430
|
+
notifyDetail
|
|
24427
24431
|
});
|
|
24428
24432
|
}
|
|
24429
24433
|
var NOTIFY_TITLE_MAX = 200;
|
|
@@ -24437,7 +24441,7 @@ function approvalNotify(tool, input) {
|
|
|
24437
24441
|
return { title: `\u5BA1\u6279\uFF1A${tool}`.slice(0, NOTIFY_TITLE_MAX), summary: firstLine2.slice(0, NOTIFY_SUMMARY_MAX) };
|
|
24438
24442
|
}
|
|
24439
24443
|
function runAcpPath(ws, opts) {
|
|
24440
|
-
const { requestId, adapter
|
|
24444
|
+
const { requestId, adapter } = opts;
|
|
24441
24445
|
console.log(`[daemon] requestId=${requestId} starting ${adapter.id} via ACP (approval=${APPROVAL}, cwd=${opts.runCwd})`);
|
|
24442
24446
|
let lastText;
|
|
24443
24447
|
const emitEvent = (evt) => {
|
|
@@ -24445,10 +24449,8 @@ function runAcpPath(ws, opts) {
|
|
|
24445
24449
|
if (ENCKEY) {
|
|
24446
24450
|
const capped = truncateEventContent(evt);
|
|
24447
24451
|
const envelope = encryptEvent(ENCKEY, capped);
|
|
24448
|
-
recorder?.event(capped, envelope);
|
|
24449
24452
|
send(ws, { type: "event", requestId, event: envelope });
|
|
24450
24453
|
} else {
|
|
24451
|
-
recorder?.event(evt);
|
|
24452
24454
|
send(ws, { type: "event", requestId, event: evt });
|
|
24453
24455
|
}
|
|
24454
24456
|
};
|
|
@@ -24469,10 +24471,8 @@ function runAcpPath(ws, opts) {
|
|
|
24469
24471
|
if (ENCKEY) {
|
|
24470
24472
|
const capped = truncateEventContent(evt);
|
|
24471
24473
|
wireEvent = encryptEvent(ENCKEY, capped);
|
|
24472
|
-
if (!renotify) recorder?.event(capped, wireEvent);
|
|
24473
24474
|
} else {
|
|
24474
24475
|
wireEvent = evt;
|
|
24475
|
-
if (!renotify) recorder?.event(evt);
|
|
24476
24476
|
}
|
|
24477
24477
|
send(ws, {
|
|
24478
24478
|
type: "permission_request",
|
|
@@ -24521,7 +24521,6 @@ function runAcpPath(ws, opts) {
|
|
|
24521
24521
|
if (error51) done.error = error51;
|
|
24522
24522
|
if (opts.notifyDetail === true) done.notify = { title: adapter.id, summary: notifySummary(lastText) };
|
|
24523
24523
|
console.log(`[daemon] requestId=${requestId} closed code=${code}${error51 ? ` error=${error51}` : ""} (ACP)`);
|
|
24524
|
-
recorder?.done(code, error51);
|
|
24525
24524
|
send(ws, done);
|
|
24526
24525
|
});
|
|
24527
24526
|
}
|
|
@@ -24607,30 +24606,28 @@ function handleListdir(ws, { requestId, path }) {
|
|
|
24607
24606
|
}
|
|
24608
24607
|
}
|
|
24609
24608
|
function handleConvList(ws, requestId) {
|
|
24610
|
-
send(ws, { type: "conv_list_result", requestId, conversations:
|
|
24609
|
+
send(ws, { type: "conv_list_result", requestId, conversations: [] });
|
|
24611
24610
|
}
|
|
24612
|
-
function handleConvPut(ws, requestId,
|
|
24613
|
-
|
|
24614
|
-
|
|
24615
|
-
|
|
24616
|
-
|
|
24617
|
-
|
|
24611
|
+
function handleConvPut(ws, requestId, _conversation) {
|
|
24612
|
+
send(ws, {
|
|
24613
|
+
type: "conv_put_result",
|
|
24614
|
+
requestId,
|
|
24615
|
+
error: "AgentLink conversation storage is disabled; use native sessions"
|
|
24616
|
+
});
|
|
24618
24617
|
}
|
|
24619
|
-
function handleConvDelete(ws, requestId,
|
|
24620
|
-
if (typeof id === "string") deleteConversation(id);
|
|
24618
|
+
function handleConvDelete(ws, requestId, _id) {
|
|
24621
24619
|
send(ws, { type: "conv_delete_result", requestId });
|
|
24622
24620
|
}
|
|
24623
24621
|
function handleConvClear(ws, requestId) {
|
|
24624
|
-
clearConversations();
|
|
24625
24622
|
send(ws, { type: "conv_clear_result", requestId });
|
|
24626
24623
|
}
|
|
24627
|
-
function handleHistory(ws, requestId,
|
|
24628
|
-
send(ws, { type: "history_result", requestId, runs:
|
|
24624
|
+
function handleHistory(ws, requestId, _conversationId) {
|
|
24625
|
+
send(ws, { type: "history_result", requestId, runs: [] });
|
|
24629
24626
|
}
|
|
24630
24627
|
function handleScanSessions(ws, requestId) {
|
|
24631
24628
|
let sessions;
|
|
24632
24629
|
try {
|
|
24633
|
-
const list =
|
|
24630
|
+
const list = scanNativeSessions();
|
|
24634
24631
|
sessions = ENCKEY ? encryptEvent(ENCKEY, list) : list;
|
|
24635
24632
|
} catch (err) {
|
|
24636
24633
|
return send(ws, { type: "scan_sessions_result", requestId, sessions: ENCKEY ? void 0 : [], error: err instanceof Error ? err.message : String(err) });
|
|
@@ -24641,7 +24638,7 @@ function handleSessionHistory(ws, requestId, agent, sessionId) {
|
|
|
24641
24638
|
if (typeof agent !== "string" || typeof sessionId !== "string") {
|
|
24642
24639
|
return send(ws, { type: "session_history_result", requestId, runs: [], error: "malformed session_history: agent and sessionId are required" });
|
|
24643
24640
|
}
|
|
24644
|
-
const result =
|
|
24641
|
+
const result = nativeSessionHistory(agent, sessionId);
|
|
24645
24642
|
if ("error" in result) {
|
|
24646
24643
|
return send(ws, { type: "session_history_result", requestId, runs: [], error: result.error });
|
|
24647
24644
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "alink-cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "一条命令把工作机接入 AgentLink,随时随地遥控本机的编码 agent。One command to link your machine to AgentLink and control your coding agents from anywhere.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -53,6 +53,8 @@
|
|
|
53
53
|
"ws": "^8.18.0"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@agentclientprotocol/
|
|
56
|
+
"@agentclientprotocol/codex-acp": "1.1.9",
|
|
57
|
+
"@agentclientprotocol/sdk": "^1.3.0",
|
|
58
|
+
"@zed-industries/claude-code-acp": "0.16.2"
|
|
57
59
|
}
|
|
58
60
|
}
|