alink-cli 0.7.2 → 0.7.4
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 +3 -6
- package/bin/agentlink-account.js +133 -0
- package/bin/agentlink.js +95 -12
- package/dist/Config-Bj2ZPCsP.mjs +1221 -0
- package/dist/Config-Bj2ZPCsP.mjs.map +1 -0
- package/dist/NodeHttpServer-BGJlR_Kf.mjs +826 -0
- package/dist/NodeHttpServer-BGJlR_Kf.mjs.map +1 -0
- package/dist/NodeServices-DDZTiw5K.mjs +1984 -0
- package/dist/NodeServices-DDZTiw5K.mjs.map +1 -0
- package/dist/NodeSocket-08w4osAf.mjs +11198 -0
- package/dist/NodeSocket-08w4osAf.mjs.map +1 -0
- package/dist/NodeSqliteClient-Byu521pt.mjs +578 -0
- package/dist/NodeSqliteClient-Byu521pt.mjs.map +1 -0
- package/dist/Schema-B3i-HrZQ.mjs +40423 -0
- package/dist/Schema-B3i-HrZQ.mjs.map +1 -0
- package/dist/SqlClient-DcM69ZvI.mjs +1554 -0
- package/dist/SqlClient-DcM69ZvI.mjs.map +1 -0
- package/dist/bin.mjs +47577 -26766
- package/dist/bin.mjs.map +1 -1
- package/package.json +4 -3
- package/dist/NodeSqliteClient-BUlNQAY0.mjs +0 -191
- package/dist/NodeSqliteClient-BUlNQAY0.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -6,20 +6,17 @@
|
|
|
6
6
|
npx alink-cli
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
首次运行输入 AgentLink 账号和密码,这台电脑会自动加入账号并上线。之后直接重复 `npx alink-cli` 即可上线;密码不会保存到本机。
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
npx alink-cli --hub wss://your-hub.example.com
|
|
13
13
|
npx alink-cli --dir /path/to/project
|
|
14
|
-
npx alink-cli
|
|
15
|
-
npx alink-cli qr
|
|
16
|
-
npx alink-cli credential status
|
|
14
|
+
npx alink-cli logout
|
|
17
15
|
```
|
|
18
16
|
|
|
19
17
|
- `--hub`:Hub 地址,默认 `wss://link.harmopath.com`。
|
|
20
18
|
- `--dir`:默认工作目录,缺省为当前目录。
|
|
21
|
-
-
|
|
22
|
-
- `--token`:导入完整机器凭证。
|
|
19
|
+
- `logout`:退出当前账号并清除本机登录记录,下次运行重新输入账号密码。
|
|
23
20
|
- `--legacy`:临时运行旧 daemon;默认始终是 daemon-t3。
|
|
24
21
|
|
|
25
22
|
Node.js 22.16+。连接为出站 WebSocket,无需端口转发;Hub 只转发 E2EE 密文。
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
const MACHINE_BUCKET = "agentlink-machines";
|
|
4
|
+
const REQUEST_TIMEOUT_MS = 10_000;
|
|
5
|
+
|
|
6
|
+
function httpUrl(hub, pathname) {
|
|
7
|
+
const url = new URL(pathname, hub);
|
|
8
|
+
if (url.protocol === "ws:") url.protocol = "http:";
|
|
9
|
+
if (url.protocol === "wss:") url.protocol = "https:";
|
|
10
|
+
return url;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function json(response) {
|
|
14
|
+
return response.json().catch(() => ({}));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function loadAgentLinkAccountConfig(hub, fetchFn = fetch) {
|
|
18
|
+
const response = await fetchFn(httpUrl(hub, "/api/config"), {
|
|
19
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
20
|
+
});
|
|
21
|
+
if (!response.ok) throw new Error(`Hub 配置读取失败 (${response.status})。`);
|
|
22
|
+
return json(response);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function machineIdFromToken(authToken) {
|
|
26
|
+
try {
|
|
27
|
+
const payload = JSON.parse(
|
|
28
|
+
Buffer.from(authToken.split(".")[1] || "", "base64url").toString("utf8"),
|
|
29
|
+
);
|
|
30
|
+
return typeof payload.mid === "string" ? payload.mid : "";
|
|
31
|
+
} catch {
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function readMachines(bucket, path) {
|
|
37
|
+
const { data, error } = await bucket.download(path);
|
|
38
|
+
if (error) {
|
|
39
|
+
if (/not found|no such|404/i.test(error.message || "")) return [];
|
|
40
|
+
throw new Error(`读取账号机器列表失败:${error.message}`);
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const parsed = JSON.parse(await data.text());
|
|
44
|
+
return Array.isArray(parsed.machines) ? parsed.machines : [];
|
|
45
|
+
} catch {
|
|
46
|
+
throw new Error("账号机器列表格式损坏,请在 Web 端移除异常记录后重试。");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function registerAgentLinkAccountMachine({
|
|
51
|
+
hub,
|
|
52
|
+
config,
|
|
53
|
+
identifier,
|
|
54
|
+
password,
|
|
55
|
+
machineName,
|
|
56
|
+
fetchFn = fetch,
|
|
57
|
+
cloudbaseSdk,
|
|
58
|
+
randomBytesFn = randomBytes,
|
|
59
|
+
}) {
|
|
60
|
+
if (!config?.cloudbaseEnvId) throw new Error("当前 Hub 尚未配置账号机器目录。");
|
|
61
|
+
|
|
62
|
+
const loginResponse = await fetchFn(httpUrl(hub, "/auth/password/login"), {
|
|
63
|
+
method: "POST",
|
|
64
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
65
|
+
headers: { "Content-Type": "application/json", "X-Requested-With": "agentlink" },
|
|
66
|
+
body: JSON.stringify({ identifier, password, client: "cli" }),
|
|
67
|
+
});
|
|
68
|
+
const login = await json(loginResponse);
|
|
69
|
+
if (!loginResponse.ok || typeof login.jwt !== "string" || !login.jwt) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
login.error === "invalid_account_credentials"
|
|
72
|
+
? "账号或密码错误。"
|
|
73
|
+
: login.error || "账号登录失败。",
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const mintResponse = await fetchFn(httpUrl(hub, "/api/machines/mint"), {
|
|
78
|
+
method: "POST",
|
|
79
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
80
|
+
headers: {
|
|
81
|
+
Authorization: `Bearer ${login.jwt}`,
|
|
82
|
+
"Content-Type": "application/json",
|
|
83
|
+
"X-Requested-With": "agentlink",
|
|
84
|
+
},
|
|
85
|
+
body: JSON.stringify({ name: machineName }),
|
|
86
|
+
});
|
|
87
|
+
const minted = await json(mintResponse);
|
|
88
|
+
const authToken = typeof minted.authToken === "string" ? minted.authToken : "";
|
|
89
|
+
const machineId =
|
|
90
|
+
typeof minted.machineId === "string" ? minted.machineId : machineIdFromToken(authToken);
|
|
91
|
+
if (!mintResponse.ok || !authToken.startsWith("al1.") || !machineId) {
|
|
92
|
+
throw new Error(minted.error || "机器注册失败。");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const sdk = cloudbaseSdk ?? (await import("@cloudbase/js-sdk")).default;
|
|
96
|
+
const app = sdk.init({ env: config.cloudbaseEnvId, region: "ap-shanghai" });
|
|
97
|
+
const auth = app.auth();
|
|
98
|
+
const signedIn = await auth.signInWithPassword(
|
|
99
|
+
identifier.includes("@") ? { email: identifier, password } : { username: identifier, password },
|
|
100
|
+
);
|
|
101
|
+
if (signedIn?.error) throw new Error(signedIn.error.message || "CloudBase 账号登录失败。");
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const user = await auth.getCurrentUser();
|
|
105
|
+
if (!user?.uid) throw new Error("CloudBase 账号登录状态无效。");
|
|
106
|
+
const path = `${user.uid}/machines.json`;
|
|
107
|
+
const bucket = app.storage.from(MACHINE_BUCKET);
|
|
108
|
+
const machines = await readMachines(bucket, path);
|
|
109
|
+
const current = machines.find((machine) => machine?.machineId === machineId);
|
|
110
|
+
const enckey = randomBytesFn(32).toString("base64url");
|
|
111
|
+
const machine = {
|
|
112
|
+
machineId,
|
|
113
|
+
name: machineName,
|
|
114
|
+
hostname: machineName,
|
|
115
|
+
createdAt: current?.createdAt ?? Date.now(),
|
|
116
|
+
enckey,
|
|
117
|
+
};
|
|
118
|
+
// ponytail: read-modify-write is enough for normal first-time setup; add
|
|
119
|
+
// versioned retries if concurrent registrations for one account become common.
|
|
120
|
+
const { error } = await bucket.update(
|
|
121
|
+
path,
|
|
122
|
+
JSON.stringify({
|
|
123
|
+
version: 2,
|
|
124
|
+
machines: [machine, ...machines.filter((entry) => entry?.machineId !== machineId)],
|
|
125
|
+
}),
|
|
126
|
+
{ contentType: "application/json" },
|
|
127
|
+
);
|
|
128
|
+
if (error) throw new Error(`保存账号机器列表失败:${error.message}`);
|
|
129
|
+
return { credential: `${authToken}.${enckey}`, machineId };
|
|
130
|
+
} finally {
|
|
131
|
+
await auth.signOut().catch(() => undefined);
|
|
132
|
+
}
|
|
133
|
+
}
|
package/bin/agentlink.js
CHANGED
|
@@ -4,9 +4,15 @@ import { randomBytes } from "node:crypto";
|
|
|
4
4
|
import { chmodSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { hostname, homedir } from "node:os";
|
|
6
6
|
import { join } from "node:path";
|
|
7
|
+
import { createInterface } from "node:readline/promises";
|
|
7
8
|
import { fileURLToPath } from "node:url";
|
|
8
9
|
import WebSocket from "ws";
|
|
9
10
|
|
|
11
|
+
import {
|
|
12
|
+
loadAgentLinkAccountConfig,
|
|
13
|
+
registerAgentLinkAccountMachine,
|
|
14
|
+
} from "./agentlink-account.js";
|
|
15
|
+
|
|
10
16
|
const OFFICIAL_HUB = process.env.AGENTLINK_DEFAULT_HUB || "wss://link.harmopath.com";
|
|
11
17
|
const STATE_DIR = process.env.AGENTLINK_STATE_DIR || join(homedir(), ".agentlink");
|
|
12
18
|
const CREDENTIAL_FILE = join(STATE_DIR, "credential");
|
|
@@ -65,15 +71,6 @@ function parseCredential(credential) {
|
|
|
65
71
|
return { machineToken: parts.slice(0, 3).join("."), enckey: parts[3] };
|
|
66
72
|
}
|
|
67
73
|
|
|
68
|
-
async function hubMode(hub) {
|
|
69
|
-
const url = new URL("/api/config", hub);
|
|
70
|
-
if (url.protocol === "ws:") url.protocol = "http:";
|
|
71
|
-
if (url.protocol === "wss:") url.protocol = "https:";
|
|
72
|
-
const response = await fetch(url);
|
|
73
|
-
if (!response.ok) throw new Error(`Hub 配置读取失败 (${response.status})。`);
|
|
74
|
-
return (await response.json()).mode;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
74
|
async function createSingleCredential(hub) {
|
|
78
75
|
const token = randomBytes(24).toString("base64url");
|
|
79
76
|
const enckey = randomBytes(32).toString("base64url");
|
|
@@ -90,6 +87,75 @@ async function createSingleCredential(hub) {
|
|
|
90
87
|
return credential;
|
|
91
88
|
}
|
|
92
89
|
|
|
90
|
+
async function readPassword(label) {
|
|
91
|
+
if (!process.stdin.isTTY || typeof process.stdin.setRawMode !== "function") {
|
|
92
|
+
throw new Error(
|
|
93
|
+
"当前终端无法安全读取密码,请在交互式终端运行,或设置 AGENTLINK_ACCOUNT 和 AGENTLINK_PASSWORD。",
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
process.stdout.write(label);
|
|
97
|
+
return new Promise((resolve, reject) => {
|
|
98
|
+
let password = "";
|
|
99
|
+
const wasRaw = process.stdin.isRaw;
|
|
100
|
+
const finish = () => {
|
|
101
|
+
process.stdin.off("data", onData);
|
|
102
|
+
process.stdin.setRawMode(wasRaw);
|
|
103
|
+
process.stdin.pause();
|
|
104
|
+
process.stdout.write("\n");
|
|
105
|
+
};
|
|
106
|
+
const onData = (chunk) => {
|
|
107
|
+
for (const char of String(chunk)) {
|
|
108
|
+
if (char === "\u0003") {
|
|
109
|
+
finish();
|
|
110
|
+
reject(new Error("已取消登录。"));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (char === "\r" || char === "\n") {
|
|
114
|
+
finish();
|
|
115
|
+
resolve(password);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (char === "\u007f" || char === "\b") password = password.slice(0, -1);
|
|
119
|
+
else if (char >= " ") password += char;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
process.stdin.setRawMode(true);
|
|
123
|
+
process.stdin.setEncoding("utf8");
|
|
124
|
+
process.stdin.resume();
|
|
125
|
+
process.stdin.on("data", onData);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function accountCredentials() {
|
|
130
|
+
const fromEnv = process.env.AGENTLINK_ACCOUNT?.trim();
|
|
131
|
+
const passwordFromEnv = process.env.AGENTLINK_PASSWORD;
|
|
132
|
+
if (fromEnv && passwordFromEnv) return { identifier: fromEnv, password: passwordFromEnv };
|
|
133
|
+
if (!process.stdin.isTTY) {
|
|
134
|
+
throw new Error("首次运行需要登录账号,请在交互式终端运行。");
|
|
135
|
+
}
|
|
136
|
+
const readline = createInterface({ input: process.stdin, output: process.stdout });
|
|
137
|
+
const identifier = (await readline.question("AgentLink 账号:")).trim();
|
|
138
|
+
readline.close();
|
|
139
|
+
const password = await readPassword("AgentLink 密码:");
|
|
140
|
+
if (!identifier || !password) throw new Error("账号和密码不能为空。");
|
|
141
|
+
return { identifier, password };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function loginAndCreateCredential(hub, config) {
|
|
145
|
+
console.log("[agentlink] 首次使用,请登录 AgentLink 账号。");
|
|
146
|
+
const { identifier, password } = await accountCredentials();
|
|
147
|
+
const result = await registerAgentLinkAccountMachine({
|
|
148
|
+
hub,
|
|
149
|
+
config,
|
|
150
|
+
identifier,
|
|
151
|
+
password,
|
|
152
|
+
machineName: hostname(),
|
|
153
|
+
});
|
|
154
|
+
saveCredential(result.credential, hub);
|
|
155
|
+
console.log("[agentlink] 登录成功,这台电脑已加入账号。");
|
|
156
|
+
return result.credential;
|
|
157
|
+
}
|
|
158
|
+
|
|
93
159
|
async function printQr(text) {
|
|
94
160
|
const { renderQr } = await import(new URL("../dist/qr.js", import.meta.url));
|
|
95
161
|
renderQr(text);
|
|
@@ -130,7 +196,9 @@ async function pair(hub) {
|
|
|
130
196
|
socket.close();
|
|
131
197
|
const credential = `${message.token}.${enckey}`;
|
|
132
198
|
saveCredential(credential, hub);
|
|
133
|
-
console.log(
|
|
199
|
+
console.log(
|
|
200
|
+
`[agentlink] 配对成功。alp1 配对串只能使用这一次;已转换为本机凭证并保存到 ${CREDENTIAL_FILE}(0600)。`,
|
|
201
|
+
);
|
|
134
202
|
resolve(credential);
|
|
135
203
|
}
|
|
136
204
|
});
|
|
@@ -140,11 +208,17 @@ async function pair(hub) {
|
|
|
140
208
|
|
|
141
209
|
if (args.includes("--help") || args.includes("-h")) {
|
|
142
210
|
console.log(
|
|
143
|
-
`用法: npx alink-cli [--hub <wss://…>] [--dir <目录>]
|
|
211
|
+
`用法: npx alink-cli [--hub <wss://…>] [--dir <目录>]\n npx alink-cli logout\n\n首次运行输入 AgentLink 账号密码,这台电脑会自动加入账号;以后直接运行即可上线。\nlogout 退出当前账号;--legacy 临时运行旧 daemon。`,
|
|
144
212
|
);
|
|
145
213
|
process.exit(0);
|
|
146
214
|
}
|
|
147
215
|
|
|
216
|
+
if (args[0] === "logout") {
|
|
217
|
+
rmSync(CREDENTIAL_FILE, { force: true });
|
|
218
|
+
console.log("[agentlink] 已退出账号;下次启动时需要重新登录。");
|
|
219
|
+
process.exit(0);
|
|
220
|
+
}
|
|
221
|
+
|
|
148
222
|
if (args[0] === "credential") {
|
|
149
223
|
const hub = value("hub") || OFFICIAL_HUB;
|
|
150
224
|
const command = args[1] || "status";
|
|
@@ -171,11 +245,20 @@ if (args.includes("--legacy")) {
|
|
|
171
245
|
} else {
|
|
172
246
|
const hub = value("hub") || OFFICIAL_HUB;
|
|
173
247
|
const explicit = value("token") || process.env.AGENTLINK_TOKEN;
|
|
248
|
+
const config =
|
|
249
|
+
explicit || savedCredential(hub) || args.includes("--pair")
|
|
250
|
+
? null
|
|
251
|
+
: await loadAgentLinkAccountConfig(hub);
|
|
174
252
|
const credential =
|
|
175
253
|
explicit ||
|
|
176
254
|
(!args.includes("--pair") && savedCredential(hub)) ||
|
|
177
|
-
(
|
|
255
|
+
(config?.mode === "multi"
|
|
256
|
+
? await loginAndCreateCredential(hub, config)
|
|
257
|
+
: await createSingleCredential(hub));
|
|
178
258
|
if (explicit && explicit !== savedCredential(hub)) saveCredential(explicit, hub);
|
|
259
|
+
if (!explicit && savedCredential(hub) && !args.includes("--pair")) {
|
|
260
|
+
console.log("[agentlink] 已登录,正在让这台电脑上线。");
|
|
261
|
+
}
|
|
179
262
|
const { machineToken, enckey } = parseCredential(credential);
|
|
180
263
|
const daemon = fileURLToPath(new URL("../dist/bin.mjs", import.meta.url));
|
|
181
264
|
const cwd = value("dir") || process.cwd();
|