alink-cli 0.8.5 → 0.8.7

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 CHANGED
@@ -1,41 +1,48 @@
1
1
  # alink-cli
2
2
 
3
- 一条命令把当前电脑上的 Claude Code、Codex、Gemini、Qwen、Kimi 等编码 agent 接入 AgentLink
3
+ 在手机上或浏览器里远程控制你电脑上的 Claude Code、Codex、Gemini、Qwen、Kimi 等编码 agent。
4
4
 
5
- ```bash
6
- npx alink-cli
7
- ```
5
+ ## 快速开始
6
+
7
+ 1. **全局安装**
8
+
9
+ ```bash
10
+ npm install -g alink-cli
11
+ ```
12
+
13
+ 2. **运行 CLI**
14
+
15
+ ```bash
16
+ alink-cli
17
+ ```
8
18
 
9
- 直接运行会进入基于 Ink 的交互式 TUI。TUI 采用扁平菜单,最多两级:
19
+ 3. **按提示登录 AgentLink 账号**
10
20
 
11
- - 顶部状态栏实时显示 daemon 是否运行、PID、Hub、凭证状态。
12
- - 主菜单:启动 / 停止 / 重启 daemon、查看状态详情、登录 / 登出、查看机器、设置、帮助、退出。
13
- - 未登录时才显示扫码配对入口。
14
- - 启动 daemon 后它在后台运行,退出 TUI 不会停止它。
21
+ 首次登录后,这台电脑会自动加入你的账号。之后直接运行 `alink-cli` 就能重新上线。
15
22
 
16
- 首次运行输入 AgentLink 账号和密码,这台电脑会自动加入账号并上线。之后直接重复 `npx alink-cli` 即可管理或启动后台服务;账号密码不会保存到本机,登录会话 JWT 缓存于 `~/.agentlink/session`(0600)。
23
+ 4. **打开手机或浏览器访问控制台**
24
+
25
+ 选择你的电脑、选择 agent、选择工作目录,然后发任务。输出会实时流回;锁屏或关掉浏览器后,任务仍会继续在电脑上运行。
26
+
27
+ ## 常用命令
17
28
 
18
29
  ```bash
19
- npm install -g alink-cli
20
- alink-cli help
21
- alink-cli login
22
- alink-cli status
23
- alink-cli machines
24
- alink-cli logout
25
-
26
- npx alink-cli --hub wss://your-hub.example.com
27
- npx alink-cli --dir /path/to/project
30
+ alink-cli # 启动或管理连接
31
+ alink-cli login # 登录并注册这台电脑
32
+ alink-cli status # 查看连接状态
33
+ alink-cli machines # 查看账号下的电脑
34
+ alink-cli logout # 退出登录
28
35
  ```
29
36
 
30
- - 全局安装后可直接使用 `alink-cli`(兼容旧命令 `agentlink`)。
31
- - `login`:登录账号并注册这台电脑,但不启动服务。
32
- - `status`:查看当前登录状态和 Hub。
33
- - `machines`:查看账号下的所有机器(在线/离线),不展示内部凭证;首次会验证一次账号密码,之后使用缓存的会话 JWT,无需重复输入。
34
- - `logout`:退出当前账号并清除本机登录记录,下次运行重新输入账号密码。
35
- - `--hub`:Hub 地址,默认 `wss://link.harmopath.com`。
36
- - `--dir`:默认工作目录,缺省为当前目录。
37
- - `--legacy`:临时运行旧 daemon;默认始终是 daemon-t3。
38
- - `--no-tui` / `AGENTLINK_TUI=0`:跳过交互式 TUI,按旧行为前台启动 daemon(适合脚本或非 TTY 环境)。
39
- - `CI=true` 或非 TTY 环境会自动跳过 TUI。
40
-
41
- Node.js 22.16+。连接为出站 WebSocket,无需端口转发;Hub 只转发 E2EE 密文。
37
+ ## 常用选项
38
+
39
+ - `--dir <path>`:设置默认工作目录。
40
+ - `--hub <url>`:连接到自己的 hub(自托管时使用)。
41
+ - `--no-tui`:跳过交互界面,直接前台运行(适合脚本或非交互环境)。
42
+
43
+ 需要 Node.js 22.16 或更高版本。
44
+
45
+ ## 了解更多
46
+
47
+ - 完整文档:[github.com/baichen99/agentlink](https://github.com/baichen99/agentlink)
48
+ - 提交问题:[github.com/baichen99/agentlink/issues](https://github.com/baichen99/agentlink/issues)
package/dist/bin.mjs CHANGED
@@ -52350,6 +52350,7 @@ function isEnvelope(value) {
52350
52350
  //#endregion
52351
52351
  //#region src/tunnel/bridge.ts
52352
52352
  const TUNNEL_SCOPES = [AuthOrchestrationReadScope, AuthOrchestrationOperateScope];
52353
+ const hubFrameDecoder = new TextDecoder();
52353
52354
  const ResponseDefectEncoded = (cause) => ({
52354
52355
  _tag: "Defect",
52355
52356
  defect: cause instanceof Error ? {
@@ -52433,6 +52434,12 @@ const splitHubFrame = (text) => {
52433
52434
  rest: text.slice(spaceAt + 1)
52434
52435
  };
52435
52436
  };
52437
+ function isHttpTunnelRequestFrame(value) {
52438
+ return typeof value === "object" && value !== null && value._tag === "HttpRequest" && typeof value.id === "string" && typeof value.method === "string" && typeof value.path === "string" && Array.isArray(value.headers) && (value.body === null || typeof value.body === "string");
52439
+ }
52440
+ function encodeHttpTunnelResponseFrame(frame) {
52441
+ return JSON.stringify(frame);
52442
+ }
52436
52443
  const makeTunnelSocket = (input) => {
52437
52444
  const runRaw = (handler, options) => scopedWith((scope) => gen(function* () {
52438
52445
  const fiberSet = yield* make$62().pipe(provide$2(scope));
@@ -52478,7 +52485,7 @@ const runHubTunnel = (config) => gen(function* () {
52478
52485
  const writeRaw = yield* socket.writer;
52479
52486
  yield* forever(take$1(outbox).pipe(flatMap$1(writeRaw)));
52480
52487
  }));
52481
- const attachTunnel = (tunnelId, live) => gen(function* () {
52488
+ const attachRpcTunnel = (tunnelId, live) => gen(function* () {
52482
52489
  yield* addFinalizer(() => sync(() => {
52483
52490
  if (tunnels.get(tunnelId) !== live) return;
52484
52491
  tunnels.delete(tunnelId);
@@ -52515,14 +52522,121 @@ const runHubTunnel = (config) => gen(function* () {
52515
52522
  type: "tunnel_open",
52516
52523
  tunnelId
52517
52524
  }))) dropTunnel(tunnelId);
52518
- yield* logInfo("hub tunnel: attached", { tunnelId });
52525
+ yield* logInfo("hub tunnel: attached", {
52526
+ tunnelId,
52527
+ purpose: live.purpose
52528
+ });
52519
52529
  yield* _await(live.closed);
52520
52530
  }).pipe(catch_((error) => logWarning$1("hub tunnel: attach failed", {
52521
52531
  tunnelId,
52522
52532
  error
52523
52533
  })));
52524
- yield* socket.runRaw((data) => {
52525
- if (typeof data !== "string") return;
52534
+ const attachHttpTunnel = (tunnelId, live) => gen(function* () {
52535
+ yield* addFinalizer(() => sync(() => {
52536
+ if (tunnels.get(tunnelId) !== live) return;
52537
+ tunnels.delete(tunnelId);
52538
+ writeHub(JSON.stringify({
52539
+ type: "tunnel_close",
52540
+ tunnelId,
52541
+ code: 1011,
52542
+ reason: "daemon http tunnel ended"
52543
+ }));
52544
+ }));
52545
+ const issued = yield* serverAuth.issueSession({
52546
+ subject: "hub-tunnel-http",
52547
+ label: "AgentLink hub tunnel HTTP proxy",
52548
+ scopes: [...TUNNEL_SCOPES]
52549
+ });
52550
+ yield* addFinalizer(() => sessionsStore.markDisconnected(issued.sessionId).pipe(andThen(serverAuth.revokeSession(issued.sessionId)), ignore$1));
52551
+ yield* sessionsStore.markConnected(issued.sessionId);
52552
+ const serverConfig = yield* ServerConfig;
52553
+ const httpClient = yield* HttpClient;
52554
+ const localBaseUrl = `http://127.0.0.1:${serverConfig.port}`;
52555
+ const sendEncrypted = (frame) => {
52556
+ try {
52557
+ const envelope = utf8ToEnvelope(config.enckey, encodeHttpTunnelResponseFrame(frame));
52558
+ if (!writeHub(`${tunnelId} ${JSON.stringify(envelope)}`)) dropTunnel(tunnelId);
52559
+ } catch {
52560
+ dropTunnel(tunnelId);
52561
+ }
52562
+ };
52563
+ live.onDecryptedFrame = (frame) => {
52564
+ let requestFrame;
52565
+ try {
52566
+ const parsed = JSON.parse(frame);
52567
+ if (!isHttpTunnelRequestFrame(parsed)) {
52568
+ sendEncrypted({
52569
+ _tag: "HttpResponse",
52570
+ id: typeof parsed?.id === "string" ? parsed.id : "unknown",
52571
+ status: 400,
52572
+ statusText: "Bad Request",
52573
+ headers: [["content-type", "text/plain"]],
52574
+ body: Buffer.from("Invalid HTTP tunnel request frame").toString("base64")
52575
+ });
52576
+ return;
52577
+ }
52578
+ requestFrame = parsed;
52579
+ } catch {
52580
+ sendEncrypted({
52581
+ _tag: "HttpResponse",
52582
+ id: "unknown",
52583
+ status: 400,
52584
+ statusText: "Bad Request",
52585
+ headers: [["content-type", "text/plain"]],
52586
+ body: Buffer.from("Malformed HTTP tunnel request frame").toString("base64")
52587
+ });
52588
+ return;
52589
+ }
52590
+ runFork(gen(function* () {
52591
+ const targetUrl = `${localBaseUrl}${requestFrame.path}`;
52592
+ const headers = { authorization: `Bearer ${issued.token}` };
52593
+ for (const [key, value] of requestFrame.headers) {
52594
+ const lower = key.toLowerCase();
52595
+ if (lower === "authorization" || lower === "cookie") continue;
52596
+ headers[lower] = value;
52597
+ }
52598
+ let request = make$61(requestFrame.method)(targetUrl).pipe(setHeaders(headers));
52599
+ if (requestFrame.body !== null) request = setBody(uint8Array(Buffer.from(requestFrame.body, "base64")))(request);
52600
+ const response = yield* httpClient.execute(request);
52601
+ const responseBody = yield* response.arrayBuffer;
52602
+ const responseHeaders = [];
52603
+ for (const [key, value] of Object.entries(response.headers)) if (typeof value === "string") responseHeaders.push([key, value]);
52604
+ return {
52605
+ _tag: "HttpResponse",
52606
+ id: requestFrame.id,
52607
+ status: response.status,
52608
+ statusText: "",
52609
+ headers: responseHeaders,
52610
+ body: Buffer.from(responseBody).toString("base64")
52611
+ };
52612
+ }).pipe(match$2({
52613
+ onFailure: (cause) => ({
52614
+ _tag: "HttpResponse",
52615
+ id: requestFrame.id,
52616
+ status: 502,
52617
+ statusText: "Bad Gateway",
52618
+ headers: [["content-type", "text/plain"]],
52619
+ body: Buffer.from(cause instanceof Error ? cause.message : String(cause)).toString("base64")
52620
+ }),
52621
+ onSuccess: (response) => response
52622
+ }), tap((frame) => sync(() => sendEncrypted(frame)))));
52623
+ };
52624
+ if (!writeHub(JSON.stringify({
52625
+ type: "tunnel_open",
52626
+ tunnelId
52627
+ }))) dropTunnel(tunnelId);
52628
+ yield* logInfo("hub tunnel: attached", {
52629
+ tunnelId,
52630
+ purpose: live.purpose
52631
+ });
52632
+ yield* _await(live.closed);
52633
+ }).pipe(catch_((error) => logWarning$1("hub tunnel: attach failed", {
52634
+ tunnelId,
52635
+ error
52636
+ })));
52637
+ yield* socket.runRaw((rawData) => {
52638
+ const data = typeof rawData === "string" ? rawData : rawData instanceof Uint8Array ? hubFrameDecoder.decode(rawData) : null;
52639
+ if (data === null) return;
52526
52640
  if (data.startsWith("{")) {
52527
52641
  let control;
52528
52642
  try {
@@ -52534,13 +52648,16 @@ const runHubTunnel = (config) => gen(function* () {
52534
52648
  const tunnelId = control.tunnelId;
52535
52649
  if (tunnels.has(tunnelId)) return;
52536
52650
  return gen(function* () {
52651
+ const closed = yield* make$70();
52652
+ const purpose = control.purpose === "http" ? "http" : "rpc";
52537
52653
  const live = {
52538
- closed: yield* make$70(),
52654
+ closed,
52539
52655
  pendingFrames: [],
52656
+ purpose,
52540
52657
  onDecryptedFrame: null
52541
52658
  };
52542
52659
  tunnels.set(tunnelId, live);
52543
- yield* forkScoped(scoped(attachTunnel(tunnelId, live)));
52660
+ yield* forkScoped(scoped((purpose === "http" ? attachHttpTunnel : attachRpcTunnel)(tunnelId, live)));
52544
52661
  });
52545
52662
  }
52546
52663
  if (control.type === "tunnel_close" && typeof control.tunnelId === "string") dropTunnel(control.tunnelId);