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 +4 -3
- package/bin/agentlink-account.js +47 -1
- package/bin/agentlink.js +102 -7
- package/dist/ui.js +2 -2
- package/package.json +1 -1
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`:默认工作目录,缺省为当前目录。
|
package/bin/agentlink-account.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
212
|
-
|
|
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"
|
|
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]
|
|
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