agentrejoin-agent 0.1.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/LICENSE +661 -0
- package/README.md +190 -0
- package/bin/agentrejoin-agent.mjs +38 -0
- package/dist/index.cjs +1393 -0
- package/dist/index.d.cts +1 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +1391 -0
- package/package.json +63 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1393 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var commander = require('commander');
|
|
4
|
+
var node_os = require('node:os');
|
|
5
|
+
var node_path = require('node:path');
|
|
6
|
+
var node_fs = require('node:fs');
|
|
7
|
+
var node_crypto = require('node:crypto');
|
|
8
|
+
var tweetnacl = require('tweetnacl');
|
|
9
|
+
var axios = require('axios');
|
|
10
|
+
var qrcode = require('qrcode-terminal');
|
|
11
|
+
var socket_ioClient = require('socket.io-client');
|
|
12
|
+
var node_events = require('node:events');
|
|
13
|
+
|
|
14
|
+
function loadConfig() {
|
|
15
|
+
const serverUrl = (process.env.AGENTREJOIN_SERVER_URL ?? "https://agentrejoin.zhandj.com").replace(/\/+$/, "");
|
|
16
|
+
const homeDir = process.env.AGENTREJOIN_HOME_DIR ?? node_path.join(node_os.homedir(), ".agentrejoin");
|
|
17
|
+
const credentialPath = node_path.join(homeDir, "agent.key");
|
|
18
|
+
return { serverUrl, homeDir, credentialPath };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function encodeBase64(buffer) {
|
|
22
|
+
return Buffer.from(buffer).toString("base64");
|
|
23
|
+
}
|
|
24
|
+
function decodeBase64(base64) {
|
|
25
|
+
return new Uint8Array(Buffer.from(base64, "base64"));
|
|
26
|
+
}
|
|
27
|
+
function encodeBase64Url(buffer) {
|
|
28
|
+
return Buffer.from(buffer).toString("base64").replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
|
|
29
|
+
}
|
|
30
|
+
function getRandomBytes(size) {
|
|
31
|
+
return new Uint8Array(node_crypto.randomBytes(size));
|
|
32
|
+
}
|
|
33
|
+
function hmac_sha512(key, data) {
|
|
34
|
+
const hmac = node_crypto.createHmac("sha512", key);
|
|
35
|
+
hmac.update(data);
|
|
36
|
+
return new Uint8Array(hmac.digest());
|
|
37
|
+
}
|
|
38
|
+
function deriveSecretKeyTreeRoot(seed, usage) {
|
|
39
|
+
const I = hmac_sha512(new TextEncoder().encode(usage + " Master Seed"), seed);
|
|
40
|
+
return {
|
|
41
|
+
key: I.slice(0, 32),
|
|
42
|
+
chainCode: I.slice(32)
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function deriveSecretKeyTreeChild(chainCode, index) {
|
|
46
|
+
const data = new Uint8Array([0, ...new TextEncoder().encode(index)]);
|
|
47
|
+
const I = hmac_sha512(chainCode, data);
|
|
48
|
+
return {
|
|
49
|
+
key: I.slice(0, 32),
|
|
50
|
+
chainCode: I.slice(32)
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function deriveKey(master, usage, path) {
|
|
54
|
+
let state = deriveSecretKeyTreeRoot(master, usage);
|
|
55
|
+
for (const index of path) {
|
|
56
|
+
state = deriveSecretKeyTreeChild(state.chainCode, index);
|
|
57
|
+
}
|
|
58
|
+
return state.key;
|
|
59
|
+
}
|
|
60
|
+
function deriveContentKeyPair(secret) {
|
|
61
|
+
const seed = deriveKey(secret, "AgentRejoin EnCoder", ["content"]);
|
|
62
|
+
const hashedSeed = new Uint8Array(node_crypto.createHash("sha512").update(seed).digest());
|
|
63
|
+
const boxSecretKey = hashedSeed.slice(0, 32);
|
|
64
|
+
const keyPair = tweetnacl.box.keyPair.fromSecretKey(boxSecretKey);
|
|
65
|
+
return { publicKey: keyPair.publicKey, secretKey: keyPair.secretKey };
|
|
66
|
+
}
|
|
67
|
+
function encryptWithDataKey(data, dataKey) {
|
|
68
|
+
const nonce = getRandomBytes(12);
|
|
69
|
+
const cipher = node_crypto.createCipheriv("aes-256-gcm", dataKey, nonce);
|
|
70
|
+
const plaintext = new TextEncoder().encode(JSON.stringify(data));
|
|
71
|
+
const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
72
|
+
const authTag = cipher.getAuthTag();
|
|
73
|
+
const bundle = new Uint8Array(1 + 12 + encrypted.length + 16);
|
|
74
|
+
bundle[0] = 0;
|
|
75
|
+
bundle.set(nonce, 1);
|
|
76
|
+
bundle.set(new Uint8Array(encrypted), 13);
|
|
77
|
+
bundle.set(new Uint8Array(authTag), 13 + encrypted.length);
|
|
78
|
+
return bundle;
|
|
79
|
+
}
|
|
80
|
+
function decryptWithDataKey(bundle, dataKey) {
|
|
81
|
+
if (bundle.length < 1 + 12 + 16) return null;
|
|
82
|
+
if (bundle[0] !== 0) return null;
|
|
83
|
+
const nonce = bundle.slice(1, 13);
|
|
84
|
+
const authTag = bundle.slice(bundle.length - 16);
|
|
85
|
+
const ciphertext = bundle.slice(13, bundle.length - 16);
|
|
86
|
+
try {
|
|
87
|
+
const decipher = node_crypto.createDecipheriv("aes-256-gcm", dataKey, nonce);
|
|
88
|
+
decipher.setAuthTag(authTag);
|
|
89
|
+
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
90
|
+
return JSON.parse(new TextDecoder().decode(decrypted));
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function encryptLegacy(data, secret) {
|
|
96
|
+
const nonce = getRandomBytes(tweetnacl.secretbox.nonceLength);
|
|
97
|
+
const plaintext = new TextEncoder().encode(JSON.stringify(data));
|
|
98
|
+
const encrypted = tweetnacl.secretbox(plaintext, nonce, secret);
|
|
99
|
+
const result = new Uint8Array(nonce.length + encrypted.length);
|
|
100
|
+
result.set(nonce);
|
|
101
|
+
result.set(encrypted, nonce.length);
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
function decryptLegacy(data, secret) {
|
|
105
|
+
try {
|
|
106
|
+
const nonce = data.slice(0, tweetnacl.secretbox.nonceLength);
|
|
107
|
+
const encrypted = data.slice(tweetnacl.secretbox.nonceLength);
|
|
108
|
+
const decrypted = tweetnacl.secretbox.open(encrypted, nonce, secret);
|
|
109
|
+
if (!decrypted) return null;
|
|
110
|
+
return JSON.parse(new TextDecoder().decode(decrypted));
|
|
111
|
+
} catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function encrypt(key, variant, data) {
|
|
116
|
+
if (variant === "legacy") {
|
|
117
|
+
return encryptLegacy(data, key);
|
|
118
|
+
} else {
|
|
119
|
+
return encryptWithDataKey(data, key);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function decrypt(key, variant, data) {
|
|
123
|
+
if (variant === "legacy") {
|
|
124
|
+
return decryptLegacy(data, key);
|
|
125
|
+
} else {
|
|
126
|
+
return decryptWithDataKey(data, key);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function libsodiumEncryptForPublicKey(data, recipientPublicKey) {
|
|
130
|
+
const ephemeralKeyPair = tweetnacl.box.keyPair();
|
|
131
|
+
const nonce = getRandomBytes(tweetnacl.box.nonceLength);
|
|
132
|
+
const encrypted = tweetnacl.box(data, nonce, recipientPublicKey, ephemeralKeyPair.secretKey);
|
|
133
|
+
const result = new Uint8Array(32 + 24 + encrypted.length);
|
|
134
|
+
result.set(ephemeralKeyPair.publicKey, 0);
|
|
135
|
+
result.set(nonce, 32);
|
|
136
|
+
result.set(encrypted, 56);
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
function decryptBoxBundle(bundle, recipientSecretKey) {
|
|
140
|
+
if (bundle.length < 32 + 24) return null;
|
|
141
|
+
const ephemeralPublicKey = bundle.slice(0, 32);
|
|
142
|
+
const nonce = bundle.slice(32, 56);
|
|
143
|
+
const ciphertext = bundle.slice(56);
|
|
144
|
+
const decrypted = tweetnacl.box.open(ciphertext, nonce, ephemeralPublicKey, recipientSecretKey);
|
|
145
|
+
return decrypted ? new Uint8Array(decrypted) : null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function readCredentials(config) {
|
|
149
|
+
try {
|
|
150
|
+
const raw = node_fs.readFileSync(config.credentialPath, "utf-8");
|
|
151
|
+
const parsed = JSON.parse(raw);
|
|
152
|
+
if (!parsed.token || !parsed.secret) return null;
|
|
153
|
+
const secret = decodeBase64(parsed.secret);
|
|
154
|
+
const contentKeyPair = deriveContentKeyPair(secret);
|
|
155
|
+
return {
|
|
156
|
+
token: parsed.token,
|
|
157
|
+
secret,
|
|
158
|
+
contentKeyPair
|
|
159
|
+
};
|
|
160
|
+
} catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function writeCredentials(config, token, secret) {
|
|
165
|
+
node_fs.mkdirSync(node_path.dirname(config.credentialPath), { recursive: true, mode: 448 });
|
|
166
|
+
const data = JSON.stringify({ token, secret: encodeBase64(secret) });
|
|
167
|
+
node_fs.writeFileSync(config.credentialPath, data, { mode: 384 });
|
|
168
|
+
}
|
|
169
|
+
function clearCredentials(config) {
|
|
170
|
+
try {
|
|
171
|
+
node_fs.unlinkSync(config.credentialPath);
|
|
172
|
+
} catch {
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function requireCredentials(config) {
|
|
176
|
+
const creds = readCredentials(config);
|
|
177
|
+
if (!creds) {
|
|
178
|
+
throw new Error("Not authenticated. Run `agentrejoin-agent auth login` first.");
|
|
179
|
+
}
|
|
180
|
+
return creds;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const POLL_INTERVAL_MS = 1e3;
|
|
184
|
+
const AUTH_TIMEOUT_MS = 12e4;
|
|
185
|
+
async function authLogin(config) {
|
|
186
|
+
const seed = getRandomBytes(32);
|
|
187
|
+
const keypair = tweetnacl.box.keyPair.fromSecretKey(seed);
|
|
188
|
+
const publicKeyBase64 = encodeBase64(keypair.publicKey);
|
|
189
|
+
try {
|
|
190
|
+
await axios.post(`${config.serverUrl}/v1/auth/account/request`, {
|
|
191
|
+
publicKey: publicKeyBase64
|
|
192
|
+
}, {
|
|
193
|
+
headers: { "X-AgentRejoin-Client": "cli-control-plane/0.1.0" }
|
|
194
|
+
});
|
|
195
|
+
} catch (err) {
|
|
196
|
+
if (err instanceof axios.AxiosError) {
|
|
197
|
+
throw new Error(`Failed to initiate auth: ${err.message}`);
|
|
198
|
+
}
|
|
199
|
+
throw err;
|
|
200
|
+
}
|
|
201
|
+
const qrData = `agentrejoin:///account?${encodeBase64Url(keypair.publicKey)}`;
|
|
202
|
+
console.log("");
|
|
203
|
+
qrcode.generate(qrData, { small: true }, (code) => {
|
|
204
|
+
console.log(code);
|
|
205
|
+
});
|
|
206
|
+
console.log("## Authentication");
|
|
207
|
+
console.log("- Action: Scan this QR code with the AgentRejoin app");
|
|
208
|
+
console.log("- Path: Settings -> Account -> Link New Device");
|
|
209
|
+
console.log(`- Public Key: \`${publicKeyBase64}\``);
|
|
210
|
+
console.log(`- URL: \`${qrData}\``);
|
|
211
|
+
console.log("");
|
|
212
|
+
const startTime = Date.now();
|
|
213
|
+
while (Date.now() - startTime < AUTH_TIMEOUT_MS) {
|
|
214
|
+
await sleep(POLL_INTERVAL_MS);
|
|
215
|
+
let result;
|
|
216
|
+
try {
|
|
217
|
+
const resp = await axios.post(`${config.serverUrl}/v1/auth/account/request`, {
|
|
218
|
+
publicKey: publicKeyBase64
|
|
219
|
+
}, {
|
|
220
|
+
headers: { "X-AgentRejoin-Client": "cli-control-plane/0.1.0" }
|
|
221
|
+
});
|
|
222
|
+
result = resp.data;
|
|
223
|
+
} catch (err) {
|
|
224
|
+
if (err instanceof axios.AxiosError) {
|
|
225
|
+
throw new Error(`Auth polling failed: ${err.message}`);
|
|
226
|
+
}
|
|
227
|
+
throw err;
|
|
228
|
+
}
|
|
229
|
+
if (result.state === "authorized" && result.token && result.response) {
|
|
230
|
+
const encryptedResponse = decodeBase64(result.response);
|
|
231
|
+
const secret = decryptBoxBundle(encryptedResponse, keypair.secretKey);
|
|
232
|
+
if (!secret) {
|
|
233
|
+
throw new Error("Failed to decrypt auth response");
|
|
234
|
+
}
|
|
235
|
+
writeCredentials(config, result.token, secret);
|
|
236
|
+
console.log("## Authentication");
|
|
237
|
+
console.log("- Status: Authenticated");
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
throw new Error("Authentication timed out. Please try again.");
|
|
242
|
+
}
|
|
243
|
+
async function authLogout(config) {
|
|
244
|
+
clearCredentials(config);
|
|
245
|
+
console.log("## Authentication");
|
|
246
|
+
console.log("- Status: Logged out");
|
|
247
|
+
console.log("- Credentials: Cleared");
|
|
248
|
+
}
|
|
249
|
+
async function authStatus(config) {
|
|
250
|
+
const creds = readCredentials(config);
|
|
251
|
+
console.log("## Authentication");
|
|
252
|
+
if (creds) {
|
|
253
|
+
console.log("- Status: Authenticated");
|
|
254
|
+
console.log(`- Public Key: \`${encodeBase64(creds.contentKeyPair.publicKey)}\``);
|
|
255
|
+
} else {
|
|
256
|
+
console.log("- Status: Not authenticated");
|
|
257
|
+
console.log("- Action: Run `agentrejoin-agent auth login` to authenticate.");
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function sleep(ms) {
|
|
261
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function resolveRecordEncryption(record, creds, recordLabel) {
|
|
265
|
+
if (record.dataEncryptionKey) {
|
|
266
|
+
const encrypted = decodeBase64(record.dataEncryptionKey);
|
|
267
|
+
const bundle = encrypted.slice(1);
|
|
268
|
+
const sessionKey = decryptBoxBundle(bundle, creds.contentKeyPair.secretKey);
|
|
269
|
+
if (!sessionKey) {
|
|
270
|
+
throw new Error(`Failed to decrypt ${recordLabel} key for ${recordLabel} ${record.id}`);
|
|
271
|
+
}
|
|
272
|
+
return { key: sessionKey, variant: "dataKey" };
|
|
273
|
+
}
|
|
274
|
+
return { key: creds.secret, variant: "legacy" };
|
|
275
|
+
}
|
|
276
|
+
function resolveSessionEncryption(session, creds) {
|
|
277
|
+
return resolveRecordEncryption(session, creds, "session");
|
|
278
|
+
}
|
|
279
|
+
function resolveMachineEncryption(machine, creds) {
|
|
280
|
+
return resolveRecordEncryption(machine, creds, "machine");
|
|
281
|
+
}
|
|
282
|
+
function decryptField(encrypted, encryption) {
|
|
283
|
+
if (!encrypted) return null;
|
|
284
|
+
const data = decodeBase64(encrypted);
|
|
285
|
+
if (encryption.variant === "dataKey") {
|
|
286
|
+
return decryptWithDataKey(data, encryption.key);
|
|
287
|
+
}
|
|
288
|
+
return decryptLegacy(data, encryption.key);
|
|
289
|
+
}
|
|
290
|
+
function decryptSession(raw, creds) {
|
|
291
|
+
const encryption = resolveSessionEncryption(raw, creds);
|
|
292
|
+
return {
|
|
293
|
+
id: raw.id,
|
|
294
|
+
seq: raw.seq,
|
|
295
|
+
createdAt: raw.createdAt,
|
|
296
|
+
updatedAt: raw.updatedAt,
|
|
297
|
+
active: raw.active,
|
|
298
|
+
activeAt: raw.activeAt,
|
|
299
|
+
metadata: decryptField(raw.metadata, encryption),
|
|
300
|
+
agentState: decryptField(raw.agentState, encryption),
|
|
301
|
+
dataEncryptionKey: raw.dataEncryptionKey,
|
|
302
|
+
encryption
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
function decryptMachine(raw, creds) {
|
|
306
|
+
const encryption = resolveMachineEncryption(raw, creds);
|
|
307
|
+
return {
|
|
308
|
+
id: raw.id,
|
|
309
|
+
seq: raw.seq,
|
|
310
|
+
createdAt: raw.createdAt,
|
|
311
|
+
updatedAt: raw.updatedAt,
|
|
312
|
+
active: raw.active,
|
|
313
|
+
activeAt: raw.activeAt,
|
|
314
|
+
metadata: decryptField(raw.metadata, encryption),
|
|
315
|
+
metadataVersion: raw.metadataVersion,
|
|
316
|
+
daemonState: decryptField(raw.daemonState, encryption),
|
|
317
|
+
daemonStateVersion: raw.daemonStateVersion,
|
|
318
|
+
dataEncryptionKey: raw.dataEncryptionKey,
|
|
319
|
+
encryption
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function handleApiError(err, context) {
|
|
323
|
+
if (err instanceof axios.AxiosError) {
|
|
324
|
+
const status = err.response?.status;
|
|
325
|
+
if (status === 401) {
|
|
326
|
+
throw new Error("Authentication expired. Run `agentrejoin-agent auth login` to re-authenticate.");
|
|
327
|
+
}
|
|
328
|
+
if (status === 403) {
|
|
329
|
+
throw new Error(`Forbidden: ${context}. Check your account permissions.`);
|
|
330
|
+
}
|
|
331
|
+
if (status === 404) {
|
|
332
|
+
throw new Error(`Not found: ${context}`);
|
|
333
|
+
}
|
|
334
|
+
if (status && status >= 400 && status < 500) {
|
|
335
|
+
const detail = err.response?.data ? `: ${JSON.stringify(err.response.data)}` : "";
|
|
336
|
+
throw new Error(`Request failed (${status})${detail}`);
|
|
337
|
+
}
|
|
338
|
+
if (status && status >= 500) {
|
|
339
|
+
throw new Error(`Server error (${status}): ${context}`);
|
|
340
|
+
}
|
|
341
|
+
throw new Error(`Request failed: ${err.message}`);
|
|
342
|
+
}
|
|
343
|
+
throw err;
|
|
344
|
+
}
|
|
345
|
+
function authHeaders(creds) {
|
|
346
|
+
return {
|
|
347
|
+
Authorization: `Bearer ${creds.token}`,
|
|
348
|
+
"X-AgentRejoin-Client": "cli-control-plane/0.1.0"
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
async function listSessions(config, creds) {
|
|
352
|
+
let data;
|
|
353
|
+
try {
|
|
354
|
+
const resp = await axios.get(`${config.serverUrl}/v1/sessions`, {
|
|
355
|
+
headers: authHeaders(creds)
|
|
356
|
+
});
|
|
357
|
+
data = resp.data;
|
|
358
|
+
} catch (err) {
|
|
359
|
+
handleApiError(err, "listing sessions");
|
|
360
|
+
}
|
|
361
|
+
return data.sessions.map((raw) => decryptSession(raw, creds));
|
|
362
|
+
}
|
|
363
|
+
async function listMachines(config, creds) {
|
|
364
|
+
let data;
|
|
365
|
+
try {
|
|
366
|
+
const resp = await axios.get(`${config.serverUrl}/v1/machines`, {
|
|
367
|
+
headers: authHeaders(creds)
|
|
368
|
+
});
|
|
369
|
+
data = resp.data;
|
|
370
|
+
} catch (err) {
|
|
371
|
+
handleApiError(err, "listing machines");
|
|
372
|
+
}
|
|
373
|
+
return data.map((raw) => decryptMachine(raw, creds));
|
|
374
|
+
}
|
|
375
|
+
async function listActiveSessions(config, creds) {
|
|
376
|
+
let data;
|
|
377
|
+
try {
|
|
378
|
+
const resp = await axios.get(`${config.serverUrl}/v2/sessions/active`, {
|
|
379
|
+
headers: authHeaders(creds)
|
|
380
|
+
});
|
|
381
|
+
data = resp.data;
|
|
382
|
+
} catch (err) {
|
|
383
|
+
handleApiError(err, "listing active sessions");
|
|
384
|
+
}
|
|
385
|
+
return data.sessions.map((raw) => decryptSession(raw, creds));
|
|
386
|
+
}
|
|
387
|
+
async function createSession(config, creds, opts) {
|
|
388
|
+
const sessionKey = getRandomBytes(32);
|
|
389
|
+
const encryptedKey = libsodiumEncryptForPublicKey(sessionKey, creds.contentKeyPair.publicKey);
|
|
390
|
+
const withVersion = new Uint8Array(1 + encryptedKey.length);
|
|
391
|
+
withVersion[0] = 0;
|
|
392
|
+
withVersion.set(encryptedKey, 1);
|
|
393
|
+
const dataEncryptionKeyBase64 = encodeBase64(withVersion);
|
|
394
|
+
const encryptedMetadata = encryptWithDataKey(opts.metadata, sessionKey);
|
|
395
|
+
const metadataBase64 = encodeBase64(encryptedMetadata);
|
|
396
|
+
let data;
|
|
397
|
+
try {
|
|
398
|
+
const resp = await axios.post(
|
|
399
|
+
`${config.serverUrl}/v1/sessions`,
|
|
400
|
+
{
|
|
401
|
+
tag: opts.tag,
|
|
402
|
+
metadata: metadataBase64,
|
|
403
|
+
dataEncryptionKey: dataEncryptionKeyBase64
|
|
404
|
+
},
|
|
405
|
+
{ headers: authHeaders(creds) }
|
|
406
|
+
);
|
|
407
|
+
data = resp.data;
|
|
408
|
+
} catch (err) {
|
|
409
|
+
handleApiError(err, "creating session");
|
|
410
|
+
}
|
|
411
|
+
const decrypted = decryptSession(data.session, creds);
|
|
412
|
+
return { ...decrypted, sessionKey: decrypted.encryption.key };
|
|
413
|
+
}
|
|
414
|
+
async function getSessionMessages(config, creds, sessionId, encryption) {
|
|
415
|
+
let data;
|
|
416
|
+
try {
|
|
417
|
+
const resp = await axios.get(
|
|
418
|
+
`${config.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}/messages`,
|
|
419
|
+
{ headers: authHeaders(creds) }
|
|
420
|
+
);
|
|
421
|
+
data = resp.data;
|
|
422
|
+
} catch (err) {
|
|
423
|
+
handleApiError(err, `session ${sessionId} messages`);
|
|
424
|
+
}
|
|
425
|
+
return data.messages.map((msg) => ({
|
|
426
|
+
id: msg.id,
|
|
427
|
+
seq: msg.seq,
|
|
428
|
+
content: decryptField(msg.content.c, encryption),
|
|
429
|
+
localId: msg.localId ?? null,
|
|
430
|
+
createdAt: msg.createdAt,
|
|
431
|
+
updatedAt: msg.updatedAt
|
|
432
|
+
}));
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function waitForConnect(socket, timeoutMs = 1e4) {
|
|
436
|
+
return new Promise((resolve, reject) => {
|
|
437
|
+
if (socket.connected) {
|
|
438
|
+
resolve();
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const timeout = setTimeout(() => {
|
|
442
|
+
socket.off("connect", onConnect);
|
|
443
|
+
socket.off("connect_error", onError);
|
|
444
|
+
reject(new Error("Timeout waiting for socket connection"));
|
|
445
|
+
}, timeoutMs);
|
|
446
|
+
const onConnect = () => {
|
|
447
|
+
clearTimeout(timeout);
|
|
448
|
+
socket.off("connect_error", onError);
|
|
449
|
+
resolve();
|
|
450
|
+
};
|
|
451
|
+
const onError = (error) => {
|
|
452
|
+
clearTimeout(timeout);
|
|
453
|
+
socket.off("connect", onConnect);
|
|
454
|
+
reject(error);
|
|
455
|
+
};
|
|
456
|
+
socket.once("connect", onConnect);
|
|
457
|
+
socket.once("connect_error", onError);
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
function normalizeRpcError(error, machineId) {
|
|
461
|
+
if (!error) {
|
|
462
|
+
return "RPC call failed";
|
|
463
|
+
}
|
|
464
|
+
if (error === "RPC method not available") {
|
|
465
|
+
return `Machine ${machineId} is offline or its daemon is not connected.`;
|
|
466
|
+
}
|
|
467
|
+
return error;
|
|
468
|
+
}
|
|
469
|
+
async function spawnSessionOnMachine(config, machine, token, options) {
|
|
470
|
+
const socket = socket_ioClient.io(config.serverUrl, {
|
|
471
|
+
auth: {
|
|
472
|
+
token
|
|
473
|
+
},
|
|
474
|
+
path: "/v1/updates",
|
|
475
|
+
transports: ["websocket"],
|
|
476
|
+
autoConnect: false,
|
|
477
|
+
reconnection: false
|
|
478
|
+
});
|
|
479
|
+
socket.connect();
|
|
480
|
+
try {
|
|
481
|
+
await waitForConnect(socket);
|
|
482
|
+
const params = encodeBase64(
|
|
483
|
+
encrypt(machine.encryption.key, machine.encryption.variant, {
|
|
484
|
+
type: "spawn-in-directory",
|
|
485
|
+
directory: options.directory,
|
|
486
|
+
approvedNewDirectoryCreation: options.approvedNewDirectoryCreation ?? false,
|
|
487
|
+
token: options.providerToken,
|
|
488
|
+
agent: options.agent
|
|
489
|
+
})
|
|
490
|
+
);
|
|
491
|
+
const response = await socket.timeout(3e4).emitWithAck("rpc-call", {
|
|
492
|
+
method: `${machine.id}:spawn-agentrejoin-session`,
|
|
493
|
+
params
|
|
494
|
+
});
|
|
495
|
+
if (!response.ok) {
|
|
496
|
+
throw new Error(normalizeRpcError(response.error, machine.id));
|
|
497
|
+
}
|
|
498
|
+
if (!response.result) {
|
|
499
|
+
throw new Error("RPC call returned no result");
|
|
500
|
+
}
|
|
501
|
+
const decrypted = decrypt(
|
|
502
|
+
machine.encryption.key,
|
|
503
|
+
machine.encryption.variant,
|
|
504
|
+
decodeBase64(response.result)
|
|
505
|
+
);
|
|
506
|
+
if (decrypted == null || typeof decrypted !== "object" || Array.isArray(decrypted)) {
|
|
507
|
+
throw new Error("RPC call returned invalid data");
|
|
508
|
+
}
|
|
509
|
+
if ("error" in decrypted && typeof decrypted.error === "string") {
|
|
510
|
+
throw new Error(String(decrypted.error));
|
|
511
|
+
}
|
|
512
|
+
if (!("type" in decrypted) || decrypted.type !== "success" && decrypted.type !== "requestToApproveDirectoryCreation" && decrypted.type !== "error") {
|
|
513
|
+
throw new Error("RPC call returned unexpected data");
|
|
514
|
+
}
|
|
515
|
+
return decrypted;
|
|
516
|
+
} finally {
|
|
517
|
+
socket.close();
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
async function resumeSessionOnMachine(config, machine, token, sessionId) {
|
|
521
|
+
const socket = socket_ioClient.io(config.serverUrl, {
|
|
522
|
+
auth: {
|
|
523
|
+
token
|
|
524
|
+
},
|
|
525
|
+
path: "/v1/updates",
|
|
526
|
+
transports: ["websocket"],
|
|
527
|
+
autoConnect: false,
|
|
528
|
+
reconnection: false
|
|
529
|
+
});
|
|
530
|
+
socket.connect();
|
|
531
|
+
try {
|
|
532
|
+
await waitForConnect(socket);
|
|
533
|
+
const params = encodeBase64(
|
|
534
|
+
encrypt(machine.encryption.key, machine.encryption.variant, {
|
|
535
|
+
sessionId
|
|
536
|
+
})
|
|
537
|
+
);
|
|
538
|
+
const response = await socket.timeout(3e4).emitWithAck("rpc-call", {
|
|
539
|
+
method: `${machine.id}:resume-agentrejoin-session`,
|
|
540
|
+
params
|
|
541
|
+
});
|
|
542
|
+
if (!response.ok) {
|
|
543
|
+
throw new Error(normalizeRpcError(response.error, machine.id));
|
|
544
|
+
}
|
|
545
|
+
if (!response.result) {
|
|
546
|
+
throw new Error("RPC call returned no result");
|
|
547
|
+
}
|
|
548
|
+
const decrypted = decrypt(
|
|
549
|
+
machine.encryption.key,
|
|
550
|
+
machine.encryption.variant,
|
|
551
|
+
decodeBase64(response.result)
|
|
552
|
+
);
|
|
553
|
+
if (decrypted == null || typeof decrypted !== "object" || Array.isArray(decrypted)) {
|
|
554
|
+
throw new Error("RPC call returned invalid data");
|
|
555
|
+
}
|
|
556
|
+
if ("error" in decrypted && typeof decrypted.error === "string") {
|
|
557
|
+
throw new Error(String(decrypted.error));
|
|
558
|
+
}
|
|
559
|
+
if (!("type" in decrypted) || decrypted.type !== "success" && decrypted.type !== "requestToApproveDirectoryCreation" && decrypted.type !== "error") {
|
|
560
|
+
throw new Error("RPC call returned unexpected data");
|
|
561
|
+
}
|
|
562
|
+
return decrypted;
|
|
563
|
+
} finally {
|
|
564
|
+
socket.close();
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function checkIdleState(metadata, agentState) {
|
|
569
|
+
const meta = metadata;
|
|
570
|
+
if (meta?.lifecycleState === "archived") {
|
|
571
|
+
return "archived";
|
|
572
|
+
}
|
|
573
|
+
const state = agentState;
|
|
574
|
+
if (!state) {
|
|
575
|
+
return false;
|
|
576
|
+
}
|
|
577
|
+
const controlledByUser = state.controlledByUser === true;
|
|
578
|
+
const requests = state.requests;
|
|
579
|
+
const hasRequests = requests != null && typeof requests === "object" && !Array.isArray(requests) && Object.keys(requests).length > 0;
|
|
580
|
+
return !controlledByUser && !hasRequests;
|
|
581
|
+
}
|
|
582
|
+
function getTurnEvent(content) {
|
|
583
|
+
if (content == null || typeof content !== "object" || Array.isArray(content)) {
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
const envelope = content;
|
|
587
|
+
if (envelope.role !== "session") {
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
const body = envelope.content;
|
|
591
|
+
if (body == null || typeof body !== "object" || Array.isArray(body)) {
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
if (body.ev?.t !== "turn-start" && body.ev?.t !== "turn-end") {
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
return {
|
|
598
|
+
type: body.ev.t,
|
|
599
|
+
turnId: typeof body.turn === "string" ? body.turn : null
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
function isReadyEvent(content) {
|
|
603
|
+
if (content == null || typeof content !== "object" || Array.isArray(content)) {
|
|
604
|
+
return false;
|
|
605
|
+
}
|
|
606
|
+
const envelope = content;
|
|
607
|
+
if (envelope.role !== "agent") {
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
610
|
+
const body = envelope.content;
|
|
611
|
+
if (body == null || typeof body !== "object" || Array.isArray(body)) {
|
|
612
|
+
return false;
|
|
613
|
+
}
|
|
614
|
+
return body.type === "event" && body.data?.type === "ready";
|
|
615
|
+
}
|
|
616
|
+
class SessionClient extends node_events.EventEmitter {
|
|
617
|
+
sessionId;
|
|
618
|
+
encryptionKey;
|
|
619
|
+
encryptionVariant;
|
|
620
|
+
socket;
|
|
621
|
+
metadata = null;
|
|
622
|
+
metadataVersion = 0;
|
|
623
|
+
agentState = null;
|
|
624
|
+
agentStateVersion = 0;
|
|
625
|
+
constructor(opts) {
|
|
626
|
+
super();
|
|
627
|
+
this.sessionId = opts.sessionId;
|
|
628
|
+
this.encryptionKey = opts.encryptionKey;
|
|
629
|
+
this.encryptionVariant = opts.encryptionVariant;
|
|
630
|
+
if (opts.initialAgentState !== void 0) {
|
|
631
|
+
this.agentState = opts.initialAgentState;
|
|
632
|
+
}
|
|
633
|
+
this.on("error", () => {
|
|
634
|
+
});
|
|
635
|
+
this.socket = socket_ioClient.io(opts.serverUrl, {
|
|
636
|
+
auth: {
|
|
637
|
+
token: opts.token,
|
|
638
|
+
clientType: "session-scoped",
|
|
639
|
+
sessionId: opts.sessionId
|
|
640
|
+
},
|
|
641
|
+
path: "/v1/updates",
|
|
642
|
+
reconnection: true,
|
|
643
|
+
reconnectionAttempts: Infinity,
|
|
644
|
+
reconnectionDelay: 1e3,
|
|
645
|
+
reconnectionDelayMax: 5e3,
|
|
646
|
+
transports: ["websocket"],
|
|
647
|
+
autoConnect: false
|
|
648
|
+
});
|
|
649
|
+
this.socket.on("connect", () => {
|
|
650
|
+
this.emit("connected");
|
|
651
|
+
});
|
|
652
|
+
this.socket.on("disconnect", (reason) => {
|
|
653
|
+
this.emit("disconnected", reason);
|
|
654
|
+
});
|
|
655
|
+
this.socket.on("connect_error", (error) => {
|
|
656
|
+
this.emit("connect_error", error);
|
|
657
|
+
});
|
|
658
|
+
this.socket.on("update", (data) => {
|
|
659
|
+
try {
|
|
660
|
+
const body = data?.body;
|
|
661
|
+
if (!body) return;
|
|
662
|
+
if (body.t === "new-message" && body.message?.content?.t === "encrypted") {
|
|
663
|
+
const msg = body.message;
|
|
664
|
+
const decrypted = decrypt(
|
|
665
|
+
this.encryptionKey,
|
|
666
|
+
this.encryptionVariant,
|
|
667
|
+
decodeBase64(msg.content.c)
|
|
668
|
+
);
|
|
669
|
+
if (decrypted === null) return;
|
|
670
|
+
this.emit("message", {
|
|
671
|
+
id: msg.id,
|
|
672
|
+
seq: msg.seq,
|
|
673
|
+
content: decrypted,
|
|
674
|
+
localId: msg.localId,
|
|
675
|
+
createdAt: msg.createdAt,
|
|
676
|
+
updatedAt: msg.updatedAt
|
|
677
|
+
});
|
|
678
|
+
} else if (body.t === "update-session") {
|
|
679
|
+
if (body.metadata && body.metadata.version > this.metadataVersion) {
|
|
680
|
+
this.metadata = decrypt(
|
|
681
|
+
this.encryptionKey,
|
|
682
|
+
this.encryptionVariant,
|
|
683
|
+
decodeBase64(body.metadata.value)
|
|
684
|
+
);
|
|
685
|
+
this.metadataVersion = body.metadata.version;
|
|
686
|
+
}
|
|
687
|
+
if (body.agentState && body.agentState.version > this.agentStateVersion) {
|
|
688
|
+
this.agentState = body.agentState.value ? decrypt(
|
|
689
|
+
this.encryptionKey,
|
|
690
|
+
this.encryptionVariant,
|
|
691
|
+
decodeBase64(body.agentState.value)
|
|
692
|
+
) : null;
|
|
693
|
+
this.agentStateVersion = body.agentState.version;
|
|
694
|
+
}
|
|
695
|
+
this.emit("state-change", {
|
|
696
|
+
metadata: this.metadata,
|
|
697
|
+
agentState: this.agentState
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
} catch (err) {
|
|
701
|
+
this.emit("error", err);
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
this.socket.connect();
|
|
705
|
+
}
|
|
706
|
+
sendMessage(text, meta) {
|
|
707
|
+
const content = {
|
|
708
|
+
role: "user",
|
|
709
|
+
content: {
|
|
710
|
+
type: "text",
|
|
711
|
+
text
|
|
712
|
+
},
|
|
713
|
+
meta: {
|
|
714
|
+
sentFrom: "agentrejoin-agent",
|
|
715
|
+
...meta
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
const encrypted = encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, content));
|
|
719
|
+
this.socket.emit("message", {
|
|
720
|
+
sid: this.sessionId,
|
|
721
|
+
message: encrypted
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
getMetadata() {
|
|
725
|
+
return this.metadata;
|
|
726
|
+
}
|
|
727
|
+
getAgentState() {
|
|
728
|
+
return this.agentState;
|
|
729
|
+
}
|
|
730
|
+
waitForConnect(timeoutMs = 1e4) {
|
|
731
|
+
return new Promise((resolve, reject) => {
|
|
732
|
+
if (this.socket.connected) {
|
|
733
|
+
resolve();
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
const timeout = setTimeout(() => {
|
|
737
|
+
this.removeListener("connected", onConnect);
|
|
738
|
+
this.removeListener("connect_error", onError);
|
|
739
|
+
reject(new Error("Timeout waiting for socket connection"));
|
|
740
|
+
}, timeoutMs);
|
|
741
|
+
const onConnect = () => {
|
|
742
|
+
clearTimeout(timeout);
|
|
743
|
+
this.removeListener("connect_error", onError);
|
|
744
|
+
resolve();
|
|
745
|
+
};
|
|
746
|
+
const onError = (err) => {
|
|
747
|
+
clearTimeout(timeout);
|
|
748
|
+
this.removeListener("connected", onConnect);
|
|
749
|
+
reject(err);
|
|
750
|
+
};
|
|
751
|
+
this.once("connected", onConnect);
|
|
752
|
+
this.once("connect_error", onError);
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
waitForIdle(timeoutMs = 3e5) {
|
|
756
|
+
return new Promise((resolve, reject) => {
|
|
757
|
+
const cleanup = () => {
|
|
758
|
+
clearTimeout(timeout);
|
|
759
|
+
this.removeListener("state-change", onStateChange);
|
|
760
|
+
this.removeListener("disconnected", onDisconnect);
|
|
761
|
+
};
|
|
762
|
+
const result = checkIdleState(this.metadata, this.agentState);
|
|
763
|
+
if (result === "archived") {
|
|
764
|
+
reject(new Error("Session is archived"));
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
if (result === true) {
|
|
768
|
+
resolve();
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
const timeout = setTimeout(() => {
|
|
772
|
+
cleanup();
|
|
773
|
+
reject(new Error("Timeout waiting for agent to become idle"));
|
|
774
|
+
}, timeoutMs);
|
|
775
|
+
const onStateChange = () => {
|
|
776
|
+
const r = checkIdleState(this.metadata, this.agentState);
|
|
777
|
+
if (r === "archived") {
|
|
778
|
+
cleanup();
|
|
779
|
+
reject(new Error("Session is archived"));
|
|
780
|
+
} else if (r === true) {
|
|
781
|
+
cleanup();
|
|
782
|
+
resolve();
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
const onDisconnect = () => {
|
|
786
|
+
cleanup();
|
|
787
|
+
reject(new Error("Socket disconnected while waiting for agent to become idle"));
|
|
788
|
+
};
|
|
789
|
+
this.on("state-change", onStateChange);
|
|
790
|
+
this.on("disconnected", onDisconnect);
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
waitForTurnCompletion(timeoutMs = 3e5) {
|
|
794
|
+
return new Promise((resolve, reject) => {
|
|
795
|
+
let sawActivity = false;
|
|
796
|
+
let activeTurnId = null;
|
|
797
|
+
let sawTurnStart = false;
|
|
798
|
+
let sawNonReadyMessage = false;
|
|
799
|
+
const cleanup = () => {
|
|
800
|
+
clearTimeout(timeout);
|
|
801
|
+
this.removeListener("message", onMessage);
|
|
802
|
+
this.removeListener("state-change", onStateChange);
|
|
803
|
+
this.removeListener("disconnected", onDisconnect);
|
|
804
|
+
};
|
|
805
|
+
const finish = (error) => {
|
|
806
|
+
cleanup();
|
|
807
|
+
if (error) {
|
|
808
|
+
reject(error);
|
|
809
|
+
} else {
|
|
810
|
+
resolve();
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
const timeout = setTimeout(() => {
|
|
814
|
+
finish(new Error("Timeout waiting for agent turn completion"));
|
|
815
|
+
}, timeoutMs);
|
|
816
|
+
const onMessage = (message) => {
|
|
817
|
+
sawActivity = true;
|
|
818
|
+
const turnEvent = getTurnEvent(message.content);
|
|
819
|
+
if (turnEvent) {
|
|
820
|
+
if (turnEvent.type === "turn-start") {
|
|
821
|
+
sawTurnStart = true;
|
|
822
|
+
sawNonReadyMessage = true;
|
|
823
|
+
activeTurnId = turnEvent.turnId;
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
if (activeTurnId == null || turnEvent.turnId == null || turnEvent.turnId === activeTurnId) {
|
|
827
|
+
finish();
|
|
828
|
+
}
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
if (isReadyEvent(message.content)) {
|
|
832
|
+
if (sawTurnStart || sawNonReadyMessage) {
|
|
833
|
+
finish();
|
|
834
|
+
}
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
sawNonReadyMessage = true;
|
|
838
|
+
};
|
|
839
|
+
const onStateChange = () => {
|
|
840
|
+
if (!sawActivity || sawTurnStart) {
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
const result = checkIdleState(this.metadata, this.agentState);
|
|
844
|
+
if (result === "archived") {
|
|
845
|
+
finish(new Error("Session is archived"));
|
|
846
|
+
} else if (result === true) {
|
|
847
|
+
finish();
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
const onDisconnect = () => {
|
|
851
|
+
finish(new Error("Socket disconnected while waiting for agent turn completion"));
|
|
852
|
+
};
|
|
853
|
+
this.on("message", onMessage);
|
|
854
|
+
this.on("state-change", onStateChange);
|
|
855
|
+
this.on("disconnected", onDisconnect);
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
sendStop() {
|
|
859
|
+
this.socket.emit("session-end", {
|
|
860
|
+
sid: this.sessionId,
|
|
861
|
+
time: Date.now()
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
close() {
|
|
865
|
+
this.socket.close();
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function formatTime(ts) {
|
|
870
|
+
if (!ts) return "-";
|
|
871
|
+
const date = new Date(ts);
|
|
872
|
+
const now = /* @__PURE__ */ new Date();
|
|
873
|
+
const diffMs = now.getTime() - date.getTime();
|
|
874
|
+
const diffMin = Math.floor(diffMs / 6e4);
|
|
875
|
+
if (diffMin < 1) return "just now";
|
|
876
|
+
if (diffMin < 60) return `${diffMin}m ago`;
|
|
877
|
+
const diffHr = Math.floor(diffMin / 60);
|
|
878
|
+
if (diffHr < 24) return `${diffHr}h ago`;
|
|
879
|
+
const diffDay = Math.floor(diffHr / 24);
|
|
880
|
+
return `${diffDay}d ago`;
|
|
881
|
+
}
|
|
882
|
+
function formatIsoTime(ts) {
|
|
883
|
+
if (!ts) return "-";
|
|
884
|
+
const date = new Date(ts);
|
|
885
|
+
if (Number.isNaN(date.getTime())) return "-";
|
|
886
|
+
return date.toISOString();
|
|
887
|
+
}
|
|
888
|
+
function formatLastActive(ts) {
|
|
889
|
+
const relative = formatTime(ts);
|
|
890
|
+
const absolute = formatIsoTime(ts);
|
|
891
|
+
if (absolute === "-") return relative;
|
|
892
|
+
return `${relative} (${absolute})`;
|
|
893
|
+
}
|
|
894
|
+
function toMarkdownInline(value) {
|
|
895
|
+
const escaped = value.replace(/`/g, "\\`");
|
|
896
|
+
return `\`${escaped}\``;
|
|
897
|
+
}
|
|
898
|
+
function normalizeCodeBlockText(value) {
|
|
899
|
+
const text = value.trim().length > 0 ? value : "(empty)";
|
|
900
|
+
return text.replace(/```/g, "``\\`");
|
|
901
|
+
}
|
|
902
|
+
function normalizeListValue(value) {
|
|
903
|
+
return value.replace(/\r?\n/g, " ").trim();
|
|
904
|
+
}
|
|
905
|
+
function toNonEmptyString(value) {
|
|
906
|
+
return typeof value === "string" && value.trim().length > 0 ? value : void 0;
|
|
907
|
+
}
|
|
908
|
+
function extractSessionSummary(meta) {
|
|
909
|
+
const direct = toNonEmptyString(meta.summary);
|
|
910
|
+
if (direct) return direct;
|
|
911
|
+
if (meta.summary != null && typeof meta.summary === "object") {
|
|
912
|
+
return toNonEmptyString(meta.summary.text);
|
|
913
|
+
}
|
|
914
|
+
return void 0;
|
|
915
|
+
}
|
|
916
|
+
function formatSessionTable(sessions) {
|
|
917
|
+
if (sessions.length === 0) {
|
|
918
|
+
return "## Sessions\n\n- Total: 0\n- Items: none";
|
|
919
|
+
}
|
|
920
|
+
const sections = sessions.map((s, index) => {
|
|
921
|
+
const meta = s.metadata ?? {};
|
|
922
|
+
const name = normalizeListValue(extractSessionSummary(meta) ?? toNonEmptyString(meta.tag) ?? "-");
|
|
923
|
+
const path = normalizeListValue(toNonEmptyString(meta.path) ?? "-");
|
|
924
|
+
const status = s.active ? "active" : "inactive";
|
|
925
|
+
const lastActive = normalizeListValue(formatLastActive(s.activeAt));
|
|
926
|
+
return [
|
|
927
|
+
`### Session ${index + 1}`,
|
|
928
|
+
`- ID: ${toMarkdownInline(s.id)}`,
|
|
929
|
+
`- Name: ${name}`,
|
|
930
|
+
`- Path: ${path}`,
|
|
931
|
+
`- Status: ${status}`,
|
|
932
|
+
`- Last Active: ${lastActive}`
|
|
933
|
+
].join("\n");
|
|
934
|
+
});
|
|
935
|
+
return `## Sessions
|
|
936
|
+
|
|
937
|
+
- Total: ${sessions.length}
|
|
938
|
+
|
|
939
|
+
${sections.join("\n\n")}`;
|
|
940
|
+
}
|
|
941
|
+
function formatMachineTable(machines) {
|
|
942
|
+
if (machines.length === 0) {
|
|
943
|
+
return "## Machines\n\n- Total: 0\n- Items: none";
|
|
944
|
+
}
|
|
945
|
+
const sections = machines.map((machine, index) => {
|
|
946
|
+
const metadata = machine.metadata ?? {};
|
|
947
|
+
const daemonState = machine.daemonState ?? null;
|
|
948
|
+
const host = normalizeListValue(toNonEmptyString(metadata.host) ?? "-");
|
|
949
|
+
const platform = normalizeListValue(toNonEmptyString(metadata.platform) ?? "-");
|
|
950
|
+
const status = machine.active ? toNonEmptyString(daemonState?.status) ?? "online" : "offline";
|
|
951
|
+
const homeDir = normalizeListValue(toNonEmptyString(metadata.homeDir) ?? "-");
|
|
952
|
+
return [
|
|
953
|
+
`### Machine ${index + 1}`,
|
|
954
|
+
`- ID: ${toMarkdownInline(machine.id)}`,
|
|
955
|
+
`- Host: ${host}`,
|
|
956
|
+
`- Platform: ${platform}`,
|
|
957
|
+
`- Status: ${status}`,
|
|
958
|
+
`- Home: ${homeDir}`,
|
|
959
|
+
`- Last Active: ${normalizeListValue(formatLastActive(machine.activeAt))}`
|
|
960
|
+
].join("\n");
|
|
961
|
+
});
|
|
962
|
+
return `## Machines
|
|
963
|
+
|
|
964
|
+
- Total: ${machines.length}
|
|
965
|
+
|
|
966
|
+
${sections.join("\n\n")}`;
|
|
967
|
+
}
|
|
968
|
+
function formatSessionStatus(session) {
|
|
969
|
+
const meta = session.metadata ?? {};
|
|
970
|
+
const state = session.agentState ?? null;
|
|
971
|
+
const tag = toNonEmptyString(meta.tag);
|
|
972
|
+
const summary = extractSessionSummary(meta);
|
|
973
|
+
const path = toNonEmptyString(meta.path);
|
|
974
|
+
const host = toNonEmptyString(meta.host);
|
|
975
|
+
const lifecycleState = toNonEmptyString(meta.lifecycleState);
|
|
976
|
+
const lines = [
|
|
977
|
+
"## Session Status",
|
|
978
|
+
"",
|
|
979
|
+
`- Session ID: ${toMarkdownInline(session.id)}`
|
|
980
|
+
];
|
|
981
|
+
if (tag) lines.push(`- Tag: ${tag}`);
|
|
982
|
+
if (summary) lines.push(`- Summary: ${summary}`);
|
|
983
|
+
if (path) lines.push(`- Path: ${path}`);
|
|
984
|
+
if (host) lines.push(`- Host: ${host}`);
|
|
985
|
+
if (lifecycleState) lines.push(`- Lifecycle: ${lifecycleState}`);
|
|
986
|
+
lines.push(`- Active: ${session.active ? "yes" : "no"}`);
|
|
987
|
+
lines.push(`- Last Active: ${formatLastActive(session.activeAt)}`);
|
|
988
|
+
if (state) {
|
|
989
|
+
const requests = state.requests != null && typeof state.requests === "object" ? Object.keys(state.requests).length : 0;
|
|
990
|
+
const busy = state.controlledByUser === true || requests > 0;
|
|
991
|
+
const agentStatus = busy ? "busy" : "idle";
|
|
992
|
+
lines.push(`- Agent: ${agentStatus}`);
|
|
993
|
+
if (requests > 0) {
|
|
994
|
+
lines.push(`- Pending Requests: ${requests}`);
|
|
995
|
+
}
|
|
996
|
+
} else {
|
|
997
|
+
lines.push("- Agent: no state");
|
|
998
|
+
}
|
|
999
|
+
return lines.join("\n");
|
|
1000
|
+
}
|
|
1001
|
+
function formatMessageHistory(messages) {
|
|
1002
|
+
if (messages.length === 0) {
|
|
1003
|
+
return "## Message History\n\n- Count: 0\n- Items: none";
|
|
1004
|
+
}
|
|
1005
|
+
const sections = messages.map((msg, index) => {
|
|
1006
|
+
const content = msg.content;
|
|
1007
|
+
const role = content?.role ?? "unknown";
|
|
1008
|
+
const timestamp = formatIsoTime(msg.createdAt);
|
|
1009
|
+
let text;
|
|
1010
|
+
if (content?.content && typeof content.content === "object" && content.content.text) {
|
|
1011
|
+
text = String(content.content.text);
|
|
1012
|
+
} else if (content?.content && typeof content.content === "string") {
|
|
1013
|
+
text = content.content;
|
|
1014
|
+
} else {
|
|
1015
|
+
text = JSON.stringify(content);
|
|
1016
|
+
}
|
|
1017
|
+
return [
|
|
1018
|
+
`### Message ${index + 1}`,
|
|
1019
|
+
`- ID: ${toMarkdownInline(msg.id)}`,
|
|
1020
|
+
`- Time: ${timestamp}`,
|
|
1021
|
+
`- Role: ${role}`,
|
|
1022
|
+
"- Text:",
|
|
1023
|
+
"```text",
|
|
1024
|
+
normalizeCodeBlockText(text),
|
|
1025
|
+
"```"
|
|
1026
|
+
].join("\n");
|
|
1027
|
+
});
|
|
1028
|
+
return `## Message History
|
|
1029
|
+
|
|
1030
|
+
- Count: ${messages.length}
|
|
1031
|
+
|
|
1032
|
+
${sections.join("\n\n")}`;
|
|
1033
|
+
}
|
|
1034
|
+
function formatJson(data) {
|
|
1035
|
+
return JSON.stringify(data, (key, value) => {
|
|
1036
|
+
if (key === "encryption" || key === "dataEncryptionKey") return void 0;
|
|
1037
|
+
if (value instanceof Uint8Array) {
|
|
1038
|
+
return Buffer.from(value).toString("base64");
|
|
1039
|
+
}
|
|
1040
|
+
return value;
|
|
1041
|
+
}, 2);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
const SUPPORTED_AGENTS = ["claude", "codex", "gemini", "openclaw", "agy"];
|
|
1045
|
+
function resolveByPrefix(items, value, label) {
|
|
1046
|
+
if (!value || value.trim().length === 0) {
|
|
1047
|
+
throw new Error(`${label} is required`);
|
|
1048
|
+
}
|
|
1049
|
+
const matches = items.filter((item) => item.id.startsWith(value));
|
|
1050
|
+
if (matches.length === 0) {
|
|
1051
|
+
throw new Error(`No ${label.toLowerCase()} found matching "${value}"`);
|
|
1052
|
+
}
|
|
1053
|
+
if (matches.length > 1) {
|
|
1054
|
+
throw new Error(`Ambiguous ${label.toLowerCase()} "${value}" matches ${matches.length} records. Be more specific.`);
|
|
1055
|
+
}
|
|
1056
|
+
return matches[0];
|
|
1057
|
+
}
|
|
1058
|
+
async function resolveSession(config, creds, sessionId) {
|
|
1059
|
+
const sessions = await listSessions(config, creds);
|
|
1060
|
+
return resolveByPrefix(sessions, sessionId, "Session ID");
|
|
1061
|
+
}
|
|
1062
|
+
async function resolveMachine(config, creds, machineId) {
|
|
1063
|
+
const machines = await listMachines(config, creds);
|
|
1064
|
+
return resolveByPrefix(machines, machineId, "Machine ID");
|
|
1065
|
+
}
|
|
1066
|
+
function createClient(session, creds, config) {
|
|
1067
|
+
return new SessionClient({
|
|
1068
|
+
sessionId: session.id,
|
|
1069
|
+
encryptionKey: session.encryption.key,
|
|
1070
|
+
encryptionVariant: session.encryption.variant,
|
|
1071
|
+
token: creds.token,
|
|
1072
|
+
serverUrl: config.serverUrl,
|
|
1073
|
+
initialAgentState: session.agentState ?? null
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
function resolveRemotePath(rawPath, machine) {
|
|
1077
|
+
const metadata = machine.metadata ?? {};
|
|
1078
|
+
const homeDir = typeof metadata.homeDir === "string" && metadata.homeDir.trim().length > 0 ? metadata.homeDir : void 0;
|
|
1079
|
+
const path = rawPath ?? homeDir;
|
|
1080
|
+
if (!path) {
|
|
1081
|
+
throw new Error("Machine metadata does not include a home directory. Pass --path explicitly.");
|
|
1082
|
+
}
|
|
1083
|
+
if (path === "~") {
|
|
1084
|
+
if (!homeDir) {
|
|
1085
|
+
throw new Error("Machine metadata does not include a home directory, so `~` cannot be resolved. Pass an absolute --path.");
|
|
1086
|
+
}
|
|
1087
|
+
return homeDir;
|
|
1088
|
+
}
|
|
1089
|
+
if (path.startsWith("~/")) {
|
|
1090
|
+
if (!homeDir) {
|
|
1091
|
+
throw new Error("Machine metadata does not include a home directory, so `~/...` cannot be resolved. Pass an absolute --path.");
|
|
1092
|
+
}
|
|
1093
|
+
const normalizedHome = homeDir.endsWith("/") || homeDir.endsWith("\\") ? homeDir.slice(0, -1) : homeDir;
|
|
1094
|
+
const separator = normalizedHome.includes("\\") && !normalizedHome.includes("/") ? "\\" : "/";
|
|
1095
|
+
return node_path.join(normalizedHome, path.slice(2)).replaceAll("/", separator);
|
|
1096
|
+
}
|
|
1097
|
+
return path;
|
|
1098
|
+
}
|
|
1099
|
+
function resolveSessionMachineId(session) {
|
|
1100
|
+
const metadata = session.metadata ?? {};
|
|
1101
|
+
if (typeof metadata.machineId !== "string" || metadata.machineId.trim().length === 0) {
|
|
1102
|
+
throw new Error(`Session ${session.id} is missing machine metadata and cannot be resumed.`);
|
|
1103
|
+
}
|
|
1104
|
+
return metadata.machineId;
|
|
1105
|
+
}
|
|
1106
|
+
function ensureMachineCanResume(machine) {
|
|
1107
|
+
const metadata = machine.metadata ?? {};
|
|
1108
|
+
if (metadata.resumeSupport?.rpcAvailable === true) {
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
if (metadata.resumeSupport?.agentRejoinAgentAuthenticated === false) {
|
|
1112
|
+
throw new Error("Resume is unavailable on this machine. Run `agentrejoin-agent auth login` in that machine environment first.");
|
|
1113
|
+
}
|
|
1114
|
+
throw new Error("Resume RPC is unavailable on this machine right now.");
|
|
1115
|
+
}
|
|
1116
|
+
const program = new commander.Command();
|
|
1117
|
+
program.name("agentrejoin-agent").description("CLI client for controlling AgentRejoin sessions remotely").version("0.1.0");
|
|
1118
|
+
program.command("auth").description("Manage authentication").addCommand(
|
|
1119
|
+
new commander.Command("login").description("Authenticate via QR code").action(async () => {
|
|
1120
|
+
const config = loadConfig();
|
|
1121
|
+
await authLogin(config);
|
|
1122
|
+
})
|
|
1123
|
+
).addCommand(
|
|
1124
|
+
new commander.Command("logout").description("Clear stored credentials").action(async () => {
|
|
1125
|
+
const config = loadConfig();
|
|
1126
|
+
await authLogout(config);
|
|
1127
|
+
})
|
|
1128
|
+
).addCommand(
|
|
1129
|
+
new commander.Command("status").description("Show authentication status").action(async () => {
|
|
1130
|
+
const config = loadConfig();
|
|
1131
|
+
await authStatus(config);
|
|
1132
|
+
})
|
|
1133
|
+
);
|
|
1134
|
+
program.command("machines").description("List all machines").option("--active", "Show only active machines").option("--json", "Output as JSON").action(async (opts) => {
|
|
1135
|
+
const config = loadConfig();
|
|
1136
|
+
const creds = requireCredentials(config);
|
|
1137
|
+
const machines = await listMachines(config, creds);
|
|
1138
|
+
const filtered = opts.active ? machines.filter((machine) => machine.active) : machines;
|
|
1139
|
+
if (opts.json) {
|
|
1140
|
+
console.log(formatJson(filtered));
|
|
1141
|
+
} else {
|
|
1142
|
+
console.log(formatMachineTable(filtered));
|
|
1143
|
+
}
|
|
1144
|
+
});
|
|
1145
|
+
program.command("list").description("List all sessions").option("--active", "Show only active sessions").option("--json", "Output as JSON").action(async (opts) => {
|
|
1146
|
+
const config = loadConfig();
|
|
1147
|
+
const creds = requireCredentials(config);
|
|
1148
|
+
const sessions = opts.active ? await listActiveSessions(config, creds) : await listSessions(config, creds);
|
|
1149
|
+
if (opts.json) {
|
|
1150
|
+
console.log(formatJson(sessions));
|
|
1151
|
+
} else {
|
|
1152
|
+
console.log(formatSessionTable(sessions));
|
|
1153
|
+
}
|
|
1154
|
+
});
|
|
1155
|
+
program.command("status").description("Get live session state").argument("<session-id>", "Session ID or prefix").option("--json", "Output as JSON").action(async (sessionId, opts) => {
|
|
1156
|
+
const config = loadConfig();
|
|
1157
|
+
const creds = requireCredentials(config);
|
|
1158
|
+
const session = await resolveSession(config, creds, sessionId);
|
|
1159
|
+
const client = createClient(session, creds, config);
|
|
1160
|
+
let liveData = false;
|
|
1161
|
+
try {
|
|
1162
|
+
await new Promise((resolve) => {
|
|
1163
|
+
let resolved = false;
|
|
1164
|
+
const done = () => {
|
|
1165
|
+
if (resolved) return;
|
|
1166
|
+
resolved = true;
|
|
1167
|
+
clearTimeout(timeout);
|
|
1168
|
+
client.removeAllListeners("state-change");
|
|
1169
|
+
client.removeAllListeners("connect_error");
|
|
1170
|
+
resolve();
|
|
1171
|
+
};
|
|
1172
|
+
const timeout = setTimeout(done, 3e3);
|
|
1173
|
+
client.once("state-change", (data) => {
|
|
1174
|
+
session.metadata = data.metadata ?? session.metadata;
|
|
1175
|
+
session.agentState = data.agentState ?? session.agentState;
|
|
1176
|
+
liveData = true;
|
|
1177
|
+
done();
|
|
1178
|
+
});
|
|
1179
|
+
client.once("connect_error", () => {
|
|
1180
|
+
done();
|
|
1181
|
+
});
|
|
1182
|
+
});
|
|
1183
|
+
} finally {
|
|
1184
|
+
client.close();
|
|
1185
|
+
}
|
|
1186
|
+
if (opts.json) {
|
|
1187
|
+
console.log(formatJson(session));
|
|
1188
|
+
} else {
|
|
1189
|
+
if (!liveData) {
|
|
1190
|
+
console.log("> Note: showing cached data (could not get live status).");
|
|
1191
|
+
}
|
|
1192
|
+
console.log(formatSessionStatus(session));
|
|
1193
|
+
}
|
|
1194
|
+
});
|
|
1195
|
+
program.command("spawn").description("Spawn a new session on a machine").requiredOption("--machine <machine-id>", "Machine ID or prefix").option("--path <path>", "Working directory path (defaults to machine home directory)").option("--agent <agent>", `Agent to start (${SUPPORTED_AGENTS.join(", ")})`, (value) => {
|
|
1196
|
+
if (!SUPPORTED_AGENTS.includes(value)) {
|
|
1197
|
+
throw new Error(`--agent must be one of: ${SUPPORTED_AGENTS.join(", ")}`);
|
|
1198
|
+
}
|
|
1199
|
+
return value;
|
|
1200
|
+
}).option("--create-dir", "Allow creating the directory if it does not exist").option("--json", "Output as JSON").action(async (opts) => {
|
|
1201
|
+
const config = loadConfig();
|
|
1202
|
+
const creds = requireCredentials(config);
|
|
1203
|
+
const machine = await resolveMachine(config, creds, opts.machine);
|
|
1204
|
+
const directory = resolveRemotePath(opts.path, machine);
|
|
1205
|
+
const result = await spawnSessionOnMachine(config, machine, creds.token, {
|
|
1206
|
+
directory,
|
|
1207
|
+
approvedNewDirectoryCreation: opts.createDir,
|
|
1208
|
+
agent: opts.agent
|
|
1209
|
+
});
|
|
1210
|
+
const payload = {
|
|
1211
|
+
machineId: machine.id,
|
|
1212
|
+
directory,
|
|
1213
|
+
agent: opts.agent ?? null,
|
|
1214
|
+
...result
|
|
1215
|
+
};
|
|
1216
|
+
if (opts.json) {
|
|
1217
|
+
console.log(formatJson(payload));
|
|
1218
|
+
if (result.type !== "success") {
|
|
1219
|
+
process.exitCode = 1;
|
|
1220
|
+
}
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
switch (result.type) {
|
|
1224
|
+
case "success":
|
|
1225
|
+
console.log([
|
|
1226
|
+
"## Session Spawned",
|
|
1227
|
+
"",
|
|
1228
|
+
`- Machine ID: \`${machine.id}\``,
|
|
1229
|
+
`- Session ID: \`${result.sessionId}\``,
|
|
1230
|
+
`- Path: ${directory}`,
|
|
1231
|
+
`- Agent: ${opts.agent ?? "default"}`
|
|
1232
|
+
].join("\n"));
|
|
1233
|
+
break;
|
|
1234
|
+
case "requestToApproveDirectoryCreation":
|
|
1235
|
+
throw new Error(`The directory '${result.directory}' does not exist. Re-run with --create-dir to allow creating it.`);
|
|
1236
|
+
case "error":
|
|
1237
|
+
throw new Error(result.errorMessage);
|
|
1238
|
+
}
|
|
1239
|
+
});
|
|
1240
|
+
program.command("resume").description("Resume a session on its original machine").argument("<session-id>", "Session ID or prefix").option("--json", "Output as JSON").action(async (sessionId, opts) => {
|
|
1241
|
+
const config = loadConfig();
|
|
1242
|
+
const creds = requireCredentials(config);
|
|
1243
|
+
const session = await resolveSession(config, creds, sessionId);
|
|
1244
|
+
const machineId = resolveSessionMachineId(session);
|
|
1245
|
+
const machine = await resolveMachine(config, creds, machineId);
|
|
1246
|
+
ensureMachineCanResume(machine);
|
|
1247
|
+
const result = await resumeSessionOnMachine(config, machine, creds.token, session.id);
|
|
1248
|
+
const payload = {
|
|
1249
|
+
sourceSessionId: session.id,
|
|
1250
|
+
machineId: machine.id,
|
|
1251
|
+
...result
|
|
1252
|
+
};
|
|
1253
|
+
if (opts.json) {
|
|
1254
|
+
console.log(formatJson(payload));
|
|
1255
|
+
if (result.type !== "success") {
|
|
1256
|
+
process.exitCode = 1;
|
|
1257
|
+
}
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
switch (result.type) {
|
|
1261
|
+
case "success":
|
|
1262
|
+
console.log([
|
|
1263
|
+
"## Session Resumed",
|
|
1264
|
+
"",
|
|
1265
|
+
`- Machine ID: \`${machine.id}\``,
|
|
1266
|
+
`- Source Session ID: \`${session.id}\``,
|
|
1267
|
+
`- Resumed Session ID: \`${result.sessionId}\``
|
|
1268
|
+
].join("\n"));
|
|
1269
|
+
break;
|
|
1270
|
+
case "requestToApproveDirectoryCreation":
|
|
1271
|
+
throw new Error(`Resume unexpectedly requested directory creation for '${result.directory}'. Resume should reuse the saved path.`);
|
|
1272
|
+
case "error":
|
|
1273
|
+
throw new Error(result.errorMessage);
|
|
1274
|
+
}
|
|
1275
|
+
});
|
|
1276
|
+
program.command("create").description("Create a new session").requiredOption("--tag <tag>", "Session tag").option("--path <path>", "Working directory path").option("--json", "Output as JSON").action(async (opts) => {
|
|
1277
|
+
const config = loadConfig();
|
|
1278
|
+
const creds = requireCredentials(config);
|
|
1279
|
+
const metadata = {
|
|
1280
|
+
tag: opts.tag,
|
|
1281
|
+
path: opts.path ?? process.cwd(),
|
|
1282
|
+
host: node_os.hostname()
|
|
1283
|
+
};
|
|
1284
|
+
const session = await createSession(config, creds, {
|
|
1285
|
+
tag: opts.tag,
|
|
1286
|
+
metadata
|
|
1287
|
+
});
|
|
1288
|
+
if (opts.json) {
|
|
1289
|
+
console.log(formatJson(session));
|
|
1290
|
+
} else {
|
|
1291
|
+
console.log([
|
|
1292
|
+
"## Session Created",
|
|
1293
|
+
"",
|
|
1294
|
+
`- Session ID: \`${session.id}\``
|
|
1295
|
+
].join("\n"));
|
|
1296
|
+
}
|
|
1297
|
+
});
|
|
1298
|
+
program.command("send").description("Send a message to a session").argument("<session-id>", "Session ID or prefix").argument("<message>", "Message text").option("--yolo", "Send with permissionMode=yolo").option("--wait", "Wait for agent to become idle").option("--json", "Output as JSON").action(async (sessionId, message, opts) => {
|
|
1299
|
+
const config = loadConfig();
|
|
1300
|
+
const creds = requireCredentials(config);
|
|
1301
|
+
const session = await resolveSession(config, creds, sessionId);
|
|
1302
|
+
const permissionMode = opts.yolo ? "yolo" : null;
|
|
1303
|
+
const client = createClient(session, creds, config);
|
|
1304
|
+
try {
|
|
1305
|
+
await client.waitForConnect();
|
|
1306
|
+
const completion = opts.wait ? client.waitForTurnCompletion() : null;
|
|
1307
|
+
client.sendMessage(message, permissionMode ? { permissionMode } : void 0);
|
|
1308
|
+
if (completion) {
|
|
1309
|
+
await completion;
|
|
1310
|
+
} else {
|
|
1311
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1312
|
+
}
|
|
1313
|
+
} finally {
|
|
1314
|
+
client.close();
|
|
1315
|
+
}
|
|
1316
|
+
if (opts.json) {
|
|
1317
|
+
console.log(formatJson({ sessionId: session.id, message, sent: true, permissionMode }));
|
|
1318
|
+
} else {
|
|
1319
|
+
console.log([
|
|
1320
|
+
"## Message Sent",
|
|
1321
|
+
"",
|
|
1322
|
+
`- Session ID: \`${session.id}\``,
|
|
1323
|
+
`- Permission Mode: ${permissionMode ?? "default"}`,
|
|
1324
|
+
`- Waited For Idle: ${opts.wait ? "yes" : "no"}`
|
|
1325
|
+
].join("\n"));
|
|
1326
|
+
}
|
|
1327
|
+
});
|
|
1328
|
+
program.command("history").description("Read message history").argument("<session-id>", "Session ID or prefix").option("--limit <n>", "Limit number of messages", (v) => {
|
|
1329
|
+
const n = parseInt(v, 10);
|
|
1330
|
+
if (isNaN(n) || n <= 0) throw new Error("--limit must be a positive integer");
|
|
1331
|
+
return n;
|
|
1332
|
+
}).option("--json", "Output as JSON").action(async (sessionId, opts) => {
|
|
1333
|
+
const config = loadConfig();
|
|
1334
|
+
const creds = requireCredentials(config);
|
|
1335
|
+
const session = await resolveSession(config, creds, sessionId);
|
|
1336
|
+
let messages = await getSessionMessages(config, creds, session.id, session.encryption);
|
|
1337
|
+
messages.sort((a, b) => a.createdAt - b.createdAt);
|
|
1338
|
+
if (opts.limit && opts.limit > 0) {
|
|
1339
|
+
messages = messages.slice(-opts.limit);
|
|
1340
|
+
}
|
|
1341
|
+
if (opts.json) {
|
|
1342
|
+
console.log(formatJson(messages));
|
|
1343
|
+
} else {
|
|
1344
|
+
console.log(formatMessageHistory(messages));
|
|
1345
|
+
}
|
|
1346
|
+
});
|
|
1347
|
+
program.command("stop").description("Stop a session").argument("<session-id>", "Session ID or prefix").action(async (sessionId) => {
|
|
1348
|
+
const config = loadConfig();
|
|
1349
|
+
const creds = requireCredentials(config);
|
|
1350
|
+
const session = await resolveSession(config, creds, sessionId);
|
|
1351
|
+
const client = createClient(session, creds, config);
|
|
1352
|
+
try {
|
|
1353
|
+
await client.waitForConnect();
|
|
1354
|
+
client.sendStop();
|
|
1355
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1356
|
+
} finally {
|
|
1357
|
+
client.close();
|
|
1358
|
+
}
|
|
1359
|
+
console.log([
|
|
1360
|
+
"## Session Stopped",
|
|
1361
|
+
"",
|
|
1362
|
+
`- Session ID: \`${session.id}\``
|
|
1363
|
+
].join("\n"));
|
|
1364
|
+
});
|
|
1365
|
+
program.command("wait").description("Wait for agent to become idle").argument("<session-id>", "Session ID or prefix").option("--timeout <seconds>", "Timeout in seconds", (v) => {
|
|
1366
|
+
const n = parseInt(v, 10);
|
|
1367
|
+
if (isNaN(n) || n <= 0) throw new Error("--timeout must be a positive integer");
|
|
1368
|
+
return n;
|
|
1369
|
+
}, 300).action(async (sessionId, opts) => {
|
|
1370
|
+
const config = loadConfig();
|
|
1371
|
+
const creds = requireCredentials(config);
|
|
1372
|
+
const session = await resolveSession(config, creds, sessionId);
|
|
1373
|
+
const client = createClient(session, creds, config);
|
|
1374
|
+
try {
|
|
1375
|
+
await client.waitForConnect();
|
|
1376
|
+
await client.waitForIdle(opts.timeout * 1e3);
|
|
1377
|
+
console.log([
|
|
1378
|
+
"## Session Idle",
|
|
1379
|
+
"",
|
|
1380
|
+
`- Session ID: \`${session.id}\``
|
|
1381
|
+
].join("\n"));
|
|
1382
|
+
} catch (err) {
|
|
1383
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1384
|
+
console.error(msg);
|
|
1385
|
+
process.exitCode = 1;
|
|
1386
|
+
} finally {
|
|
1387
|
+
client.close();
|
|
1388
|
+
}
|
|
1389
|
+
});
|
|
1390
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
1391
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
1392
|
+
process.exitCode = 1;
|
|
1393
|
+
});
|