alink-cli 0.6.0 → 0.6.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 +12 -1
- package/bin/agentlink.js +69 -6
- package/dist/daemon.js +30 -9
- package/package.json +1 -1
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
|
@@ -23197,7 +23197,12 @@ function toolContentText(content) {
|
|
|
23197
23197
|
}
|
|
23198
23198
|
return parts.filter(Boolean).join("\n");
|
|
23199
23199
|
}
|
|
23200
|
-
function
|
|
23200
|
+
function cleanTitle(v) {
|
|
23201
|
+
if (typeof v !== "string") return void 0;
|
|
23202
|
+
const title = v.split("\n").find((line) => line.trim())?.trim();
|
|
23203
|
+
return title ? title.slice(0, 200) : void 0;
|
|
23204
|
+
}
|
|
23205
|
+
function routeUpdate(update, onEvent, onTitle) {
|
|
23201
23206
|
switch (update.sessionUpdate) {
|
|
23202
23207
|
case "agent_message_chunk": {
|
|
23203
23208
|
const t = textOf(update.content);
|
|
@@ -23222,6 +23227,11 @@ function routeUpdate(update, onEvent) {
|
|
|
23222
23227
|
}
|
|
23223
23228
|
return;
|
|
23224
23229
|
}
|
|
23230
|
+
case "session_info_update": {
|
|
23231
|
+
const title = cleanTitle(update.title);
|
|
23232
|
+
if (title) onTitle?.(title);
|
|
23233
|
+
return;
|
|
23234
|
+
}
|
|
23225
23235
|
default:
|
|
23226
23236
|
return;
|
|
23227
23237
|
}
|
|
@@ -23254,7 +23264,7 @@ async function runAcp(o) {
|
|
|
23254
23264
|
Readable.toWeb(child.stdout)
|
|
23255
23265
|
);
|
|
23256
23266
|
const app = client({ name: "agentlink" }).onNotification("session/update", async (ctx) => {
|
|
23257
|
-
routeUpdate(ctx.params.update, o.onEvent);
|
|
23267
|
+
routeUpdate(ctx.params.update, o.onEvent, o.onTitle);
|
|
23258
23268
|
}).onRequest("session/request_permission", async (ctx) => handlePermission(ctx.params, o.requestPermission));
|
|
23259
23269
|
const killChild = () => {
|
|
23260
23270
|
try {
|
|
@@ -23802,7 +23812,8 @@ function parseClaudeArchive(text, opts) {
|
|
|
23802
23812
|
function claudeArchiveMeta(headText, tailText) {
|
|
23803
23813
|
let sessionId;
|
|
23804
23814
|
let cwd;
|
|
23805
|
-
let
|
|
23815
|
+
let promptTitle = "";
|
|
23816
|
+
let aiTitle = "";
|
|
23806
23817
|
for (const raw of headText.split("\n")) {
|
|
23807
23818
|
if (!raw.trim()) continue;
|
|
23808
23819
|
let obj;
|
|
@@ -23813,28 +23824,35 @@ function claudeArchiveMeta(headText, tailText) {
|
|
|
23813
23824
|
}
|
|
23814
23825
|
if (typeof obj.sessionId === "string" && !sessionId) sessionId = obj.sessionId;
|
|
23815
23826
|
if (typeof obj.cwd === "string" && !cwd) cwd = obj.cwd;
|
|
23816
|
-
if (
|
|
23827
|
+
if (obj.type === "ai-title" && typeof obj.title === "string" && obj.title.trim()) {
|
|
23828
|
+
aiTitle = firstLine(obj.title);
|
|
23829
|
+
}
|
|
23830
|
+
if (!promptTitle && obj.type === "user" && obj.message && typeof obj.message === "object") {
|
|
23817
23831
|
const msg = obj.message;
|
|
23818
|
-
if (isHumanPrompt(msg))
|
|
23832
|
+
if (isHumanPrompt(msg)) promptTitle = firstLine(userText(msg));
|
|
23819
23833
|
}
|
|
23820
|
-
if (sessionId && cwd &&
|
|
23834
|
+
if (sessionId && cwd && aiTitle) break;
|
|
23821
23835
|
}
|
|
23822
23836
|
let lastLine = "";
|
|
23837
|
+
let tailAiTitle = "";
|
|
23823
23838
|
const tailLines = tailText.split("\n").filter((l) => l.trim());
|
|
23824
|
-
for (let i = tailLines.length - 1; i >= 0 && !lastLine; i--) {
|
|
23839
|
+
for (let i = tailLines.length - 1; i >= 0 && (!lastLine || !tailAiTitle); i--) {
|
|
23825
23840
|
let obj;
|
|
23826
23841
|
try {
|
|
23827
23842
|
obj = JSON.parse(tailLines[i]);
|
|
23828
23843
|
} catch {
|
|
23829
23844
|
continue;
|
|
23830
23845
|
}
|
|
23846
|
+
if (!tailAiTitle && obj.type === "ai-title" && typeof obj.title === "string" && obj.title.trim()) {
|
|
23847
|
+
tailAiTitle = firstLine(obj.title);
|
|
23848
|
+
}
|
|
23831
23849
|
if ((obj.type === "assistant" || obj.type === "user") && obj.message && typeof obj.message === "object") {
|
|
23832
23850
|
const msg = obj.message;
|
|
23833
23851
|
const text = obj.type === "assistant" ? assistantText(msg) : isHumanPrompt(msg) ? userText(msg) : "";
|
|
23834
|
-
if (text.trim()) lastLine = firstLine(text);
|
|
23852
|
+
if (!lastLine && text.trim()) lastLine = firstLine(text);
|
|
23835
23853
|
}
|
|
23836
23854
|
}
|
|
23837
|
-
return { ...sessionId ? { sessionId } : {}, ...cwd ? { cwd } : {}, title, lastLine };
|
|
23855
|
+
return { ...sessionId ? { sessionId } : {}, ...cwd ? { cwd } : {}, title: tailAiTitle || aiTitle || promptTitle, lastLine };
|
|
23838
23856
|
}
|
|
23839
23857
|
function assistantText(message) {
|
|
23840
23858
|
const content = message.content;
|
|
@@ -24500,6 +24518,9 @@ function runAcpPath(ws, opts) {
|
|
|
24500
24518
|
acpArgs: adapter.acp.args,
|
|
24501
24519
|
env: spawnEnv(),
|
|
24502
24520
|
onEvent: emitEvent,
|
|
24521
|
+
onTitle: (title) => {
|
|
24522
|
+
if (opts.conversationId) putConversation({ id: opts.conversationId, title });
|
|
24523
|
+
},
|
|
24503
24524
|
requestPermission,
|
|
24504
24525
|
signal: controller.signal,
|
|
24505
24526
|
onSpawn: (child) => void running.set(requestId, child)
|
package/package.json
CHANGED