alink-cli 0.8.0 → 0.8.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 CHANGED
@@ -9,10 +9,11 @@ npx alink-cli
9
9
  直接运行会进入基于 Ink 的交互式 TUI。TUI 采用扁平菜单,最多两级:
10
10
 
11
11
  - 顶部状态栏实时显示 daemon 是否运行、PID、Hub、凭证状态。
12
- - 主菜单:启动 / 停止 / 重启 daemon、查看状态详情、登录 / 配对 / 登出、查看机器、设置、帮助、退出。
12
+ - 主菜单:启动 / 停止 / 重启 daemon、查看状态详情、登录 / 登出、查看机器、设置、帮助、退出。
13
+ - 未登录时才显示扫码配对入口。
13
14
  - 启动 daemon 后它在后台运行,退出 TUI 不会停止它。
14
15
 
15
- 首次运行输入 AgentLink 账号和密码,这台电脑会自动加入账号并上线。之后直接重复 `npx alink-cli` 即可管理或启动后台服务;密码不会保存到本机。
16
+ 首次运行输入 AgentLink 账号和密码,这台电脑会自动加入账号并上线。之后直接重复 `npx alink-cli` 即可管理或启动后台服务;账号密码不会保存到本机,登录会话 JWT 缓存于 `~/.agentlink/session`(0600)。
16
17
 
17
18
  ```bash
18
19
  npm install -g alink-cli
@@ -29,7 +30,7 @@ npx alink-cli --dir /path/to/project
29
30
  - 全局安装后可直接使用 `alink-cli`(兼容旧命令 `agentlink`)。
30
31
  - `login`:登录账号并注册这台电脑,但不启动服务。
31
32
  - `status`:查看当前登录状态和 Hub。
32
- - `machines`:验证账号密码后查看账号下的所有机器,不展示内部凭证。
33
+ - `machines`:查看账号下的所有机器(在线/离线),不展示内部凭证;首次会验证一次账号密码,之后使用缓存的会话 JWT,无需重复输入。
33
34
  - `logout`:退出当前账号并清除本机登录记录,下次运行重新输入账号密码。
34
35
  - `--hub`:Hub 地址,默认 `wss://link.harmopath.com`。
35
36
  - `--dir`:默认工作目录,缺省为当前目录。
@@ -134,12 +134,58 @@ export async function registerAgentLinkAccountMachine({
134
134
  { onConflict: "owner_id,machine_id" },
135
135
  );
136
136
  if (error) throw new Error(`保存账号机器记录失败:${error.message}`);
137
- return { credential: `${authToken}.${enckey}`, machineId };
137
+ return { credential: `${authToken}.${enckey}`, machineId, jwt: login.jwt };
138
138
  } finally {
139
139
  await auth.signOut().catch(() => undefined);
140
140
  }
141
141
  }
142
142
 
143
+ export async function listAgentLinkAccountMachinesFromHub({ hub, jwt, fetchFn = fetch }) {
144
+ if (!jwt) throw new Error("没有可用的登录会话,请先运行 alink-cli login。");
145
+ const response = await fetchFn(httpUrl(hub, "/api/machines"), {
146
+ method: "GET",
147
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
148
+ headers: {
149
+ Authorization: `Bearer ${jwt}`,
150
+ "X-Requested-With": "agentlink",
151
+ },
152
+ });
153
+ const body = await json(response);
154
+ if (!response.ok) {
155
+ throw new Error(body.error || `读取机器列表失败 (${response.status})。`);
156
+ }
157
+ const machines = Array.isArray(body.machines) ? body.machines : [];
158
+ return machines.map((row) => ({
159
+ machineId: typeof row.machineId === "string" ? row.machineId : "",
160
+ online: row.online === true,
161
+ name: typeof row.name === "string" ? row.name : undefined,
162
+ hostname: typeof row.hostname === "string" ? row.hostname : undefined,
163
+ connectedAt: timestamp(row.connectedAt),
164
+ createdAt: timestamp(row.createdAt),
165
+ updatedAt: timestamp(row.updatedAt),
166
+ })).filter((m) => m.machineId);
167
+ }
168
+
169
+ export async function exchangeCredentialForSessionJwt({ hub, credential, fetchFn = fetch }) {
170
+ if (!credential) throw new Error("没有可用的机器凭证。");
171
+ // The hub only accepts the three-segment auth half; strip the E2EE enckey.
172
+ const authHalf = credential.split(".").slice(0, 3).join(".");
173
+ const response = await fetchFn(httpUrl(hub, "/auth/credential"), {
174
+ method: "POST",
175
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
176
+ headers: {
177
+ "Content-Type": "application/json",
178
+ "X-Requested-With": "agentlink",
179
+ },
180
+ body: JSON.stringify({ credential: authHalf }),
181
+ });
182
+ const body = await json(response);
183
+ if (!response.ok || typeof body.jwt !== "string" || !body.jwt) {
184
+ throw new Error(body.error || "凭证换取会话失败。");
185
+ }
186
+ return body.jwt;
187
+ }
188
+
143
189
  export async function listAgentLinkAccountMachines({ config, identifier, password, cloudbaseSdk }) {
144
190
  if (!config?.cloudbaseEnvId) throw new Error("当前 Hub 尚未配置账号机器目录。");
145
191
  const sdk = cloudbaseSdk ?? (await import("@cloudbase/js-sdk")).default;
package/bin/agentlink.js CHANGED
@@ -9,15 +9,56 @@ import { fileURLToPath } from "node:url";
9
9
  import WebSocket from "ws";
10
10
 
11
11
  import {
12
+ exchangeCredentialForSessionJwt,
12
13
  listAgentLinkAccountMachines,
14
+ listAgentLinkAccountMachinesFromHub,
13
15
  loadAgentLinkAccountConfig,
14
16
  registerAgentLinkAccountMachine,
15
17
  } from "./agentlink-account.js";
16
18
  import { accountCredentials } from "./agentlink-prompts.js";
17
19
 
20
+ function savedSessionJwt(hub = OFFICIAL_HUB) {
21
+ try {
22
+ const raw = readFileSync(SESSION_FILE, "utf8").trim();
23
+ if (!raw) return undefined;
24
+ if (!raw.startsWith("{")) {
25
+ return normalizeHub(hub) === normalizeHub(OFFICIAL_HUB) ? raw : undefined;
26
+ }
27
+ const saved = JSON.parse(raw);
28
+ return saved.hub === normalizeHub(hub) && typeof saved.jwt === "string" ? saved.jwt : undefined;
29
+ } catch {
30
+ return undefined;
31
+ }
32
+ }
33
+
34
+ function saveSessionJwt(jwt, hub = OFFICIAL_HUB) {
35
+ mkdirSync(STATE_DIR, { recursive: true });
36
+ writeFileSync(SESSION_FILE, JSON.stringify({ hub: normalizeHub(hub), jwt }), { mode: 0o600 });
37
+ chmodSync(SESSION_FILE, 0o600);
38
+ }
39
+
40
+ function clearSessionJwt(hub = OFFICIAL_HUB) {
41
+ try {
42
+ const raw = readFileSync(SESSION_FILE, "utf8").trim();
43
+ if (!raw) {
44
+ rmSync(SESSION_FILE, { force: true });
45
+ return;
46
+ }
47
+ if (!raw.startsWith("{")) {
48
+ if (normalizeHub(hub) === normalizeHub(OFFICIAL_HUB)) rmSync(SESSION_FILE, { force: true });
49
+ return;
50
+ }
51
+ const saved = JSON.parse(raw);
52
+ if (saved.hub === normalizeHub(hub)) rmSync(SESSION_FILE, { force: true });
53
+ } catch {
54
+ rmSync(SESSION_FILE, { force: true });
55
+ }
56
+ }
57
+
18
58
  const OFFICIAL_HUB = process.env.AGENTLINK_DEFAULT_HUB || "wss://link.harmopath.com";
19
59
  const STATE_DIR = process.env.AGENTLINK_STATE_DIR || join(homedir(), ".agentlink");
20
60
  const CREDENTIAL_FILE = join(STATE_DIR, "credential");
61
+ const SESSION_FILE = join(STATE_DIR, "session");
21
62
  const args = process.argv.slice(2);
22
63
  const legacyCredentialCommands = process.env.AGENTLINK_ENABLE_LEGACY_CREDENTIALS === "1";
23
64
  const forceTui = process.env.AGENTLINK_TUI === "1";
@@ -44,6 +85,13 @@ function normalizeHub(hub) {
44
85
  return url.toString();
45
86
  }
46
87
 
88
+ function httpUrl(hub, pathname) {
89
+ const url = new URL(pathname, hub);
90
+ if (url.protocol === "ws:") url.protocol = "http:";
91
+ if (url.protocol === "wss:") url.protocol = "https:";
92
+ return url;
93
+ }
94
+
47
95
  function savedCredential(hub = OFFICIAL_HUB) {
48
96
  try {
49
97
  const raw = readFileSync(CREDENTIAL_FILE, "utf8").trim();
@@ -108,6 +156,7 @@ async function loginAndCreateCredential(hub, config) {
108
156
  machineName: hostname(),
109
157
  });
110
158
  saveCredential(result.credential, hub);
159
+ if (result.jwt) saveSessionJwt(result.jwt, hub);
111
160
  console.log("[agentlink] 登录成功,这台电脑已加入账号。");
112
161
  return result.credential;
113
162
  }
@@ -172,15 +221,27 @@ async function showHelp() {
172
221
  );
173
222
  }
174
223
 
224
+ async function showVersion() {
225
+ try {
226
+ const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
227
+ console.log(pkg.version);
228
+ } catch {
229
+ console.log("0.0.0");
230
+ }
231
+ }
232
+
175
233
  async function showStatus() {
176
234
  const hub = value("hub") || OFFICIAL_HUB;
177
235
  console.log(`[agentlink] 登录状态:${savedCredential(hub) ? "已登录" : "未登录"}`);
236
+ console.log(`[agentlink] 会话状态:${savedSessionJwt(hub) ? "已缓存" : "未缓存"}`);
178
237
  console.log(`[agentlink] Hub:${normalizeHub(hub)}`);
179
238
  }
180
239
 
181
240
  async function doLogout() {
182
- const loggedIn = savedCredential(value("hub") || OFFICIAL_HUB);
241
+ const hub = value("hub") || OFFICIAL_HUB;
242
+ const loggedIn = savedCredential(hub);
183
243
  if (loggedIn) rmSync(CREDENTIAL_FILE, { force: true });
244
+ clearSessionJwt(hub);
184
245
  console.log(
185
246
  loggedIn ? "[agentlink] 已退出账号;下次启动时需要重新登录。" : "[agentlink] 当前未登录。",
186
247
  );
@@ -197,20 +258,49 @@ async function doLogin() {
197
258
  await loginAndCreateCredential(hub, config);
198
259
  }
199
260
 
261
+ async function ensureSessionJwt(hub, config) {
262
+ const jwt = savedSessionJwt(hub);
263
+ if (jwt) return jwt;
264
+ console.log("[agentlink] 需要验证 AgentLink 账号密码以获取机器列表。");
265
+ const { identifier, password } = await accountCredentials();
266
+ const loginResponse = await fetch(httpUrl(hub, "/auth/password/login"), {
267
+ method: "POST",
268
+ signal: AbortSignal.timeout(10_000),
269
+ headers: { "Content-Type": "application/json", "X-Requested-With": "agentlink" },
270
+ body: JSON.stringify({ identifier, password, client: "cli" }),
271
+ });
272
+ const login = await loginResponse.json().catch(() => ({}));
273
+ if (!loginResponse.ok || typeof login.jwt !== "string" || !login.jwt) {
274
+ throw new Error(
275
+ login.error === "invalid_account_credentials" ? "账号或密码错误。" : login.error || "账号登录失败。",
276
+ );
277
+ }
278
+ saveSessionJwt(login.jwt, hub);
279
+ return login.jwt;
280
+ }
281
+
200
282
  async function doMachines() {
201
283
  const hub = value("hub") || OFFICIAL_HUB;
202
284
  const config = await loadAgentLinkAccountConfig(hub);
203
285
  if (config?.mode !== "multi") throw new Error("当前 Hub 不使用账号机器目录。");
204
- console.log("[agentlink] 查看账号机器,请验证 AgentLink 账号。");
205
- const { identifier, password } = await accountCredentials();
206
- const machines = await listAgentLinkAccountMachines({ config, identifier, password });
286
+ let jwt = savedSessionJwt(hub);
287
+ if (!jwt) {
288
+ const credential = savedCredential(hub);
289
+ if (credential) {
290
+ jwt = await exchangeCredentialForSessionJwt({ hub, credential });
291
+ } else {
292
+ jwt = await ensureSessionJwt(hub, config);
293
+ }
294
+ saveSessionJwt(jwt, hub);
295
+ }
296
+ const machines = await listAgentLinkAccountMachinesFromHub({ hub, jwt });
207
297
  if (machines.length === 0) console.log("[agentlink] 账号下还没有机器。");
208
298
  else {
209
299
  console.log(`[agentlink] 账号下共 ${machines.length} 台机器:`);
210
300
  for (const machine of machines) {
211
- console.log(
212
- `- ${machine.name || machine.hostname || machine.machineId} (${machine.machineId})`,
213
- );
301
+ const onlineMarker = machine.online ? "●" : "○";
302
+ const label = machine.name || machine.hostname || machine.machineId;
303
+ console.log(` ${onlineMarker} ${label} (${machine.machineId})`);
214
304
  }
215
305
  }
216
306
  }
@@ -272,6 +362,11 @@ async function main() {
272
362
  // Strip internal-only flags before dispatching so downstream handlers see clean args.
273
363
  const publicArgs = args.filter((a) => a !== "--no-tui");
274
364
 
365
+ if (publicArgs.includes("--version") || publicArgs.includes("-v")) {
366
+ await showVersion();
367
+ return;
368
+ }
369
+
275
370
  if (publicArgs[0] === "help" || publicArgs.includes("--help") || publicArgs.includes("-h")) {
276
371
  await showHelp();
277
372
  return;
package/dist/ui.js CHANGED
@@ -48519,7 +48519,7 @@ function App2() {
48519
48519
  }
48520
48520
  items.push({ label: "\u67E5\u770B\u72B6\u6001\u8BE6\u60C5", value: "status" });
48521
48521
  items.push({ label: loggedIn ? "\u9000\u51FA\u8D26\u53F7" : "\u767B\u5F55\u8D26\u53F7", value: loggedIn ? "logout" : "login" });
48522
- items.push({ label: "\u626B\u7801\u914D\u5BF9", value: "pair", disabled: loggedIn });
48522
+ if (!loggedIn) items.push({ label: "\u626B\u7801\u914D\u5BF9", value: "pair" });
48523
48523
  items.push({ label: "\u67E5\u770B\u673A\u5668", value: "machines", disabled: !loggedIn });
48524
48524
  items.push({ label: "\u8BBE\u7F6E", value: "settings" });
48525
48525
  items.push({ label: "\u5E2E\u52A9", value: "help" });
@@ -48892,7 +48892,7 @@ function MachinesView({ onDone }) {
48892
48892
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { color: "gray", children: "Enter \u7EE7\u7EED \xB7 Esc \u53D6\u6D88" })
48893
48893
  ] });
48894
48894
  }
48895
- var invokedByCli = typeof process.argv[1] === "string" && (process.argv[1].endsWith("/bin/agentlink.js") || process.argv[1].endsWith("\\bin\\agentlink.js"));
48895
+ var invokedByCli = typeof process.argv[1] === "string" && (/[\/\\](?:bin[\/\\])?agentlink\.js$/.test(process.argv[1]) || /[\/\\]alink-cli$/.test(process.argv[1]) || /[\/\\]agentlink$/.test(process.argv[1]));
48896
48896
  if (invokedByCli) {
48897
48897
  render_default(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(App2, {}));
48898
48898
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alink-cli",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "一条命令把工作机接入 AgentLink,随时随地遥控本机的编码 agent。One command to link your machine to AgentLink and control your coding agents from anywhere.",
5
5
  "keywords": [
6
6
  "acp",