alink-cli 0.10.1 → 0.11.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 CHANGED
@@ -1,55 +1,27 @@
1
- # alink-cli
1
+ # AgentLink
2
2
 
3
- 在手机或浏览器上,继续使用运行在你自己电脑上的 Claude Code、Codex、Gemini、Qwen、Kimi 等编码 agent。
3
+ AgentLink 让你随时从手机或浏览器继续使用运行在自己电脑上的 Claude Code、Codex、Gemini、Qwen、Kimi 等编码 agent。
4
4
 
5
- > **Local-first:** agent 在你的电脑上运行,项目文件和任务记录保存在本机。AgentLink 只负责加密连接设备,Hub 不保存你的对话。
5
+ **官网:[https://link.harmopath.com/marketing](https://link.harmopath.com/marketing)**
6
6
 
7
- ## 三步开始
7
+ ## 本地运行,随时继续
8
8
 
9
- 需要 Node.js 22.16 或更高版本。
10
-
11
- 1. 安装 CLI
12
-
13
- ```bash
14
- npm install -g alink-cli
15
- ```
16
-
17
- 2. 启动 AgentLink,并按提示登录
18
-
19
- ```bash
20
- alink-cli
21
- ```
22
-
23
- 3. 在手机或浏览器打开 [AgentLink 控制台](https://link.harmopath.com/login)
24
-
25
- 登录同一个账号,你的电脑会自动出现。选择 agent 和工作目录,就可以开始任务或继续已有工作。
9
+ - **Local-first:** agent、项目文件和任务记录都保存在你的电脑上。
10
+ - **安全连接:** 设备之间端到端加密,Hub 不保存你的对话。
11
+ - **跨设备继续:** 离开电脑后,仍可查看进度、回复消息和处理授权。
12
+ - **一个账号:** 登录后自动发现自己的电脑,无需手动配置连接。
26
13
 
27
- 关闭浏览器或锁屏后,任务仍会在你的电脑上继续运行。
14
+ ## 开始使用
28
15
 
29
- ## Codex 会话衔接
30
-
31
- 对于已经由 AgentLink 绑定的 Codex 原生会话,AgentLink 会先只读同步 Codex 中的终态消息。Codex 释放写入权后,从 AgentLink 发送的新消息会恢复同一个原生 thread,不会另建分叉会话。
32
-
33
- 当前不会扫描或自动导入只在 Codex App 中新建、尚未绑定到 AgentLink 的会话;这类会话请先从 AgentLink 新建或绑定后再继续。
34
-
35
- ## 为什么是 local-first
36
-
37
- - **本地运行:** agent 和命令都在你的电脑上执行。
38
- - **本地保存:** AgentLink 不会把你的项目复制到云端,任务记录保存在本机。
39
- - **安全连接:** 设备之间端到端加密;Hub 只转发连接,不保存对话内容。
40
- - **账号隔离:** 只有登录你账号的设备才能看到你的电脑。
41
-
42
- ## 常用命令
16
+ 需要 Node.js 22.16 或更高版本。
43
17
 
44
18
  ```bash
45
- alink-cli # 启动或管理连接
46
- alink-cli login # 登录并添加这台电脑
47
- alink-cli status # 查看连接状态
48
- alink-cli machines # 查看账号下的电脑
49
- alink-cli logout # 退出登录
19
+ npm install -g alink-cli
20
+ alink-cli
50
21
  ```
51
22
 
52
- ## 了解更多
23
+ 按提示登录,然后在手机或浏览器打开 [AgentLink 控制台](https://link.harmopath.com/login),即可开始任务或继续已有工作。
24
+
25
+ `alink-cli login` 会打开浏览器,支持使用 GitHub 或已有 AgentLink 账号登录并添加当前电脑。
53
26
 
54
- - [完整文档](https://github.com/baichen99/agentlink)
55
- - [提交问题](https://github.com/baichen99/agentlink/issues)
27
+ [GitHub](https://github.com/baichen99/agentlink) · [提交问题](https://github.com/baichen99/agentlink/issues)
@@ -140,6 +140,31 @@ export async function registerAgentLinkAccountMachine({
140
140
  }
141
141
  }
142
142
 
143
+ export async function registerAgentLinkSessionMachine({
144
+ hub,
145
+ jwt,
146
+ machineName,
147
+ fetchFn = fetch,
148
+ randomBytesFn = randomBytes,
149
+ }) {
150
+ const enckey = randomBytesFn(32).toString("base64url");
151
+ const response = await fetchFn(httpUrl(hub, "/api/machines/mint"), {
152
+ method: "POST",
153
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
154
+ headers: {
155
+ Authorization: `Bearer ${jwt}`,
156
+ "Content-Type": "application/json",
157
+ "X-Requested-With": "agentlink",
158
+ },
159
+ body: JSON.stringify({ name: machineName, enckey }),
160
+ });
161
+ const minted = await json(response);
162
+ if (!response.ok || typeof minted.authToken !== "string" || !minted.authToken.startsWith("al1.")) {
163
+ throw new Error(minted.error || "机器注册失败。");
164
+ }
165
+ return { credential: `${minted.authToken}.${enckey}`, machineId: minted.machineId, jwt };
166
+ }
167
+
143
168
  export async function listAgentLinkAccountMachinesFromHub({ hub, jwt, fetchFn = fetch }) {
144
169
  if (!jwt) throw new Error("没有可用的登录会话,请先运行 alink-cli login。");
145
170
  const response = await fetchFn(httpUrl(hub, "/api/machines"), {
package/bin/agentlink.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from "node:child_process";
3
3
  import { randomBytes } from "node:crypto";
4
+ import { createServer } from "node:http";
4
5
  import { existsSync } from "node:fs";
5
6
  import { chmodSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
6
7
  import { hostname, homedir } from "node:os";
@@ -14,6 +15,7 @@ import {
14
15
  listAgentLinkAccountMachinesFromHub,
15
16
  loadAgentLinkAccountConfig,
16
17
  registerAgentLinkAccountMachine,
18
+ registerAgentLinkSessionMachine,
17
19
  } from "./agentlink-account.js";
18
20
  import { accountCredentials } from "./agentlink-prompts.js";
19
21
 
@@ -146,21 +148,75 @@ async function createSingleCredential(hub) {
146
148
  }
147
149
 
148
150
  async function loginAndCreateCredential(hub, config) {
149
- console.log("[agentlink] 首次使用,请登录 AgentLink 账号。");
150
- const { identifier, password } = await accountCredentials();
151
- const result = await registerAgentLinkAccountMachine({
152
- hub,
153
- config,
154
- identifier,
155
- password,
156
- machineName: hostname(),
157
- });
151
+ const envLogin = process.env.AGENTLINK_ACCOUNT && process.env.AGENTLINK_PASSWORD;
152
+ const result = envLogin
153
+ ? await registerAgentLinkAccountMachine({
154
+ hub,
155
+ config,
156
+ ...(await accountCredentials()),
157
+ machineName: hostname(),
158
+ })
159
+ : await browserLogin(hub).then((jwt) =>
160
+ registerAgentLinkSessionMachine({ hub, jwt, machineName: hostname() }),
161
+ );
158
162
  saveCredential(result.credential, hub);
159
163
  if (result.jwt) saveSessionJwt(result.jwt, hub);
160
164
  console.log("[agentlink] 登录成功,这台电脑已加入账号。");
161
165
  return result.credential;
162
166
  }
163
167
 
168
+ async function browserLogin(hub) {
169
+ const state = randomBytes(24).toString("base64url");
170
+ return new Promise((resolve, reject) => {
171
+ const server = createServer((req, res) => {
172
+ if (req.method !== "POST") {
173
+ res.writeHead(405).end();
174
+ return;
175
+ }
176
+ let body = "";
177
+ req.setEncoding("utf8");
178
+ req.on("data", (chunk) => {
179
+ body += chunk;
180
+ if (body.length > 16_384) req.destroy();
181
+ });
182
+ req.on("end", () => {
183
+ const value = new URLSearchParams(body);
184
+ const jwt = value.get("jwt");
185
+ if (value.get("state") !== state || !jwt) {
186
+ res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" }).end("登录请求无效。");
187
+ return;
188
+ }
189
+ res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" }).end("登录成功,可以关闭此页面。");
190
+ clearTimeout(timer);
191
+ server.close();
192
+ resolve(jwt);
193
+ });
194
+ });
195
+ const timer = setTimeout(() => {
196
+ server.close();
197
+ reject(new Error("浏览器登录超时,请重试。"));
198
+ }, 10 * 60_000);
199
+ server.listen(0, "127.0.0.1", () => {
200
+ const address = server.address();
201
+ if (!address || typeof address === "string") {
202
+ clearTimeout(timer);
203
+ server.close();
204
+ reject(new Error("无法启动本地登录回调。"));
205
+ return;
206
+ }
207
+ const url = httpUrl(hub, "/login");
208
+ url.searchParams.set("cli_callback", `http://127.0.0.1:${address.port}/callback`);
209
+ url.searchParams.set("cli_state", state);
210
+ console.log(`\n请在浏览器完成 GitHub 登录:\n${url}\n`);
211
+ const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
212
+ const commandArgs = process.platform === "win32" ? ["/c", "start", "", url.toString()] : [url.toString()];
213
+ const opener = spawn(command, commandArgs, { detached: true, stdio: "ignore" });
214
+ opener.on("error", () => undefined);
215
+ opener.unref();
216
+ });
217
+ });
218
+ }
219
+
164
220
  async function printQr(text) {
165
221
  const { renderQr } = await import(new URL("../dist/qr.js", import.meta.url));
166
222
  renderQr(text);
package/dist/bin.mjs CHANGED
@@ -53573,8 +53573,9 @@ const runHubTunnel = (config) => gen(function* () {
53573
53573
  }
53574
53574
  }, { onOpen: logInfo("hub tunnel: connected to hub").pipe(tap(() => sync(() => void runFork(upload)))) }).pipe(tapDefect((cause) => logWarning$1("hub tunnel: hub socket defect", { cause: String(cause) })));
53575
53575
  yield* logWarning$1("hub tunnel: hub connection closed");
53576
+ return yield* fail(/* @__PURE__ */ new Error("hub tunnel closed"));
53576
53577
  }).pipe(scoped, tapError((error) => logWarning$1("hub tunnel: connection error", { error }))), {
53577
- schedule: spaced("2 seconds"),
53578
+ schedule: spaced("500 millis"),
53578
53579
  while: (error) => !(SocketError.is(error) && error.reason._tag === "SocketCloseError" && (error.reason.code === 4401 || error.reason.code === 4403))
53579
53580
  });
53580
53581
  }).pipe(tapDefect((cause) => logError("hub tunnel: worker defect", { cause: String(cause) })), catchCause((cause) => logError("hub tunnel: worker failed", { cause: String(cause) })));
@@ -89460,7 +89461,7 @@ const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, gen
89460
89461
  });
89461
89462
  const reconcileThread = (threadId, options) => withThreadLock(threadId, reconcileThreadUnlocked(threadId, options)).pipe(mapError((cause) => cause instanceof ProviderRuntimeReconciliationError ? cause : new ProviderRuntimeReconciliationError({
89462
89463
  threadId,
89463
- detail: cause instanceof Error ? cause.message : "Provider snapshot sync failed.",
89464
+ detail: cause instanceof Error && cause.message.toLowerCase().includes("active writer") ? "对话正在另一端运行,请等待另一端结束后再继续。" : cause instanceof Error ? cause.message : "Provider snapshot sync failed.",
89464
89465
  cause
89465
89466
  })));
89466
89467
  const processDomainEvent = (_event) => void_$1;