@zhihand/mcp 0.30.0 → 0.32.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/bin/zhihand +318 -106
- package/dist/core/command.js +4 -3
- package/dist/core/config.d.ts +35 -20
- package/dist/core/config.js +129 -55
- package/dist/core/device.d.ts +3 -3
- package/dist/core/device.js +22 -14
- package/dist/core/logger.d.ts +17 -0
- package/dist/core/logger.js +32 -0
- package/dist/core/pair.d.ts +39 -31
- package/dist/core/pair.js +188 -84
- package/dist/core/registry.d.ts +23 -30
- package/dist/core/registry.js +321 -194
- package/dist/core/screenshot.js +3 -2
- package/dist/core/sse.d.ts +32 -7
- package/dist/core/sse.js +90 -22
- package/dist/core/ws.d.ts +92 -0
- package/dist/core/ws.js +327 -0
- package/dist/daemon/dispatcher.js +1 -1
- package/dist/daemon/heartbeat.js +1 -1
- package/dist/daemon/index.js +3 -3
- package/dist/daemon/prompt-listener.d.ts +5 -6
- package/dist/daemon/prompt-listener.js +58 -94
- package/dist/index.d.ts +1 -1
- package/dist/index.js +18 -16
- package/dist/tools/control.js +1 -1
- package/dist/tools/pair.d.ts +1 -1
- package/dist/tools/pair.js +22 -25
- package/dist/tools/system.js +1 -1
- package/package.json +3 -1
package/bin/zhihand
CHANGED
|
@@ -1,26 +1,36 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import os from "node:os";
|
|
4
|
+
import fs from "node:fs";
|
|
4
5
|
import { parseArgs } from "node:util";
|
|
5
6
|
import { startStdioServer } from "../dist/index.js";
|
|
6
7
|
import { startDaemon, stopDaemon, isAlreadyRunning } from "../dist/daemon/index.js";
|
|
7
8
|
import { detectCLITools, formatDetectedTools } from "../dist/cli/detect.js";
|
|
8
9
|
import {
|
|
9
10
|
loadConfig,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
listUsers,
|
|
12
|
+
getUserRecord,
|
|
13
|
+
removeUser,
|
|
14
|
+
removeDeviceFromUser,
|
|
15
|
+
findDeviceOwner,
|
|
16
|
+
updateDeviceLabel,
|
|
17
|
+
updateControllerToken,
|
|
14
18
|
loadBackendConfig,
|
|
15
19
|
saveBackendConfig,
|
|
20
|
+
addUser,
|
|
16
21
|
DEFAULT_MODELS,
|
|
17
22
|
resolveConfig,
|
|
23
|
+
resolveDefaultEndpoint,
|
|
18
24
|
} from "../dist/core/config.js";
|
|
19
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
executePairingNewUser,
|
|
27
|
+
executePairingAddDevice,
|
|
28
|
+
} from "../dist/core/pair.js";
|
|
29
|
+
import { fetchUserCredentials } from "../dist/core/ws.js";
|
|
20
30
|
import { configureMCP, displayName } from "../dist/cli/mcp-config.js";
|
|
21
31
|
|
|
22
32
|
const DEFAULT_ENDPOINT = "https://api.zhihand.com";
|
|
23
|
-
const VERSION = "0.
|
|
33
|
+
const VERSION = "0.32.0";
|
|
24
34
|
|
|
25
35
|
const CLI_TOOL_MAP = {
|
|
26
36
|
claude: "claudecode",
|
|
@@ -66,11 +76,14 @@ Usage:
|
|
|
66
76
|
zhihand codex Switch backend to Codex CLI
|
|
67
77
|
|
|
68
78
|
zhihand setup Interactive setup: pair + configure + start
|
|
69
|
-
zhihand pair [--label X] Pair
|
|
70
|
-
zhihand
|
|
71
|
-
zhihand
|
|
72
|
-
zhihand
|
|
73
|
-
zhihand
|
|
79
|
+
zhihand pair [--label X] Pair new user + first device + auto-configure MCP
|
|
80
|
+
zhihand pair <user_id> Add device to existing user
|
|
81
|
+
zhihand list [<user_id>] List users/devices with real-time online status
|
|
82
|
+
zhihand unpair <id> Remove user (usr_*) or device (credential)
|
|
83
|
+
zhihand rename <cred> <n> Rename a device (server-side + local)
|
|
84
|
+
zhihand export <user_id> Export user credentials as JSON to stdout
|
|
85
|
+
zhihand import <file> Import user credentials from JSON file
|
|
86
|
+
zhihand rotate <user_id> Rotate controller token
|
|
74
87
|
zhihand detect Detect available CLI tools
|
|
75
88
|
|
|
76
89
|
zhihand test [cred] [ids] Run device tests (all, specific ids, or for a credential)
|
|
@@ -202,103 +215,309 @@ switch (command) {
|
|
|
202
215
|
}
|
|
203
216
|
|
|
204
217
|
case "pair": {
|
|
205
|
-
const
|
|
218
|
+
const arg1 = positionals[1];
|
|
206
219
|
const label = values.label ?? undefined;
|
|
207
|
-
|
|
220
|
+
|
|
221
|
+
if (arg1 && arg1.startsWith("usr_")) {
|
|
222
|
+
// Add device to existing user
|
|
223
|
+
const deviceRecord = await executePairingAddDevice(arg1, label);
|
|
224
|
+
console.log(`\nDevice paired: ${deviceRecord.label} (${deviceRecord.credential_id})`);
|
|
225
|
+
} else {
|
|
226
|
+
// New user + first device
|
|
227
|
+
const { userRecord, deviceRecord } = await executePairingNewUser(label);
|
|
228
|
+
console.log(`\nUser created: ${userRecord.label} (${userRecord.user_id})`);
|
|
229
|
+
console.log(`Device paired: ${deviceRecord.label} (${deviceRecord.credential_id})`);
|
|
230
|
+
console.log(`\nAdd another device: zhihand pair ${userRecord.user_id}`);
|
|
231
|
+
|
|
232
|
+
// Auto-configure MCP hosts
|
|
233
|
+
const tools = await detectCLITools();
|
|
234
|
+
if (tools.length > 0) {
|
|
235
|
+
const best = tools.find((t) => t.loggedIn) ?? tools[0];
|
|
236
|
+
console.log(`\nAuto-configuring MCP for ${displayName(best.name)}...`);
|
|
237
|
+
const backendCfg = loadBackendConfig();
|
|
238
|
+
configureMCP(best.name, backendCfg.activeBackend);
|
|
239
|
+
saveBackendConfig({ activeBackend: best.name });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
208
242
|
break;
|
|
209
243
|
}
|
|
210
244
|
|
|
211
245
|
case "list": {
|
|
212
|
-
const
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
246
|
+
const userIdFilter = positionals[1];
|
|
247
|
+
const users = listUsers();
|
|
248
|
+
const endpoint = resolveDefaultEndpoint();
|
|
249
|
+
|
|
250
|
+
if (users.length === 0) {
|
|
251
|
+
console.log("No users configured. Run: zhihand pair");
|
|
216
252
|
break;
|
|
217
253
|
}
|
|
218
|
-
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
254
|
+
|
|
255
|
+
const filteredUsers = userIdFilter
|
|
256
|
+
? users.filter((u) => u.user_id === userIdFilter)
|
|
257
|
+
: users;
|
|
258
|
+
|
|
259
|
+
if (filteredUsers.length === 0) {
|
|
260
|
+
console.error(`User '${userIdFilter}' not found.`);
|
|
261
|
+
process.exit(1);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
for (const user of filteredUsers) {
|
|
265
|
+
console.log(`\nUSER: ${user.label} (${user.user_id})`);
|
|
266
|
+
|
|
267
|
+
// Fetch real-time online status from server
|
|
268
|
+
let onlineMap = new Map();
|
|
269
|
+
try {
|
|
270
|
+
const creds = await fetchUserCredentials(endpoint, user.user_id, user.controller_token);
|
|
271
|
+
for (const c of creds) {
|
|
272
|
+
onlineMap.set(c.credential_id, c.online ?? false);
|
|
273
|
+
}
|
|
274
|
+
} catch {
|
|
275
|
+
// Fallback: no online status
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (user.devices.length === 0) {
|
|
279
|
+
console.log(" (no devices)");
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const header = ["DEVICE_ID", "LABEL", "PLATFORM", "ONLINE", "PAIRED"];
|
|
284
|
+
const rows = user.devices.map((d) => [
|
|
285
|
+
d.credential_id,
|
|
286
|
+
d.label,
|
|
287
|
+
d.platform,
|
|
288
|
+
onlineMap.has(d.credential_id)
|
|
289
|
+
? (onlineMap.get(d.credential_id) ? "yes" : "no")
|
|
290
|
+
: "?",
|
|
291
|
+
d.paired_at,
|
|
292
|
+
]);
|
|
293
|
+
const widths = header.map((h, i) => Math.max(h.length, ...rows.map((row) => row[i].length)));
|
|
294
|
+
const fmt = (row) => row.map((c, i) => c.padEnd(widths[i])).join(" ");
|
|
295
|
+
console.log(" " + fmt(header));
|
|
296
|
+
console.log(" " + widths.map((w) => "-".repeat(w)).join(" "));
|
|
297
|
+
for (const r of rows) console.log(" " + fmt(r));
|
|
298
|
+
}
|
|
232
299
|
break;
|
|
233
300
|
}
|
|
234
301
|
|
|
235
302
|
case "unpair": {
|
|
303
|
+
const id = positionals[1];
|
|
304
|
+
if (!id) {
|
|
305
|
+
console.error("Usage: zhihand unpair <user_id | credential_id>");
|
|
306
|
+
process.exit(1);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const endpoint = resolveDefaultEndpoint();
|
|
310
|
+
|
|
311
|
+
if (id.startsWith("usr_")) {
|
|
312
|
+
// Delete user (cascade)
|
|
313
|
+
const user = getUserRecord(id);
|
|
314
|
+
if (!user) {
|
|
315
|
+
console.error(`User '${id}' not found.`);
|
|
316
|
+
process.exit(1);
|
|
317
|
+
}
|
|
318
|
+
// Best-effort server-side delete
|
|
319
|
+
try {
|
|
320
|
+
const res = await fetch(`${endpoint}/v1/users/${encodeURIComponent(id)}`, {
|
|
321
|
+
method: "DELETE",
|
|
322
|
+
headers: { "Authorization": `Bearer ${user.controller_token}` },
|
|
323
|
+
signal: AbortSignal.timeout(5000),
|
|
324
|
+
});
|
|
325
|
+
if (!res.ok) {
|
|
326
|
+
console.warn(`Warning: server delete returned ${res.status} (continuing to remove locally)`);
|
|
327
|
+
}
|
|
328
|
+
} catch (err) {
|
|
329
|
+
console.warn(`Warning: server delete failed: ${err.message} (continuing to remove locally)`);
|
|
330
|
+
}
|
|
331
|
+
removeUser(id);
|
|
332
|
+
console.log(`Removed user: ${id} (${user.label})`);
|
|
333
|
+
} else {
|
|
334
|
+
// Delete single credential
|
|
335
|
+
const owner = findDeviceOwner(id);
|
|
336
|
+
if (!owner) {
|
|
337
|
+
console.error(`Device '${id}' not found.`);
|
|
338
|
+
process.exit(1);
|
|
339
|
+
}
|
|
340
|
+
// Best-effort server-side delete
|
|
341
|
+
try {
|
|
342
|
+
const res = await fetch(`${endpoint}/v1/credentials/${encodeURIComponent(id)}`, {
|
|
343
|
+
method: "DELETE",
|
|
344
|
+
headers: { "Authorization": `Bearer ${owner.user.controller_token}` },
|
|
345
|
+
signal: AbortSignal.timeout(5000),
|
|
346
|
+
});
|
|
347
|
+
if (!res.ok) {
|
|
348
|
+
console.warn(`Warning: server delete returned ${res.status} (continuing to remove locally)`);
|
|
349
|
+
}
|
|
350
|
+
} catch (err) {
|
|
351
|
+
console.warn(`Warning: server delete failed: ${err.message} (continuing to remove locally)`);
|
|
352
|
+
}
|
|
353
|
+
removeDeviceFromUser(owner.user.user_id, id);
|
|
354
|
+
console.log(`Removed device: ${id}`);
|
|
355
|
+
}
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
case "rename": {
|
|
236
360
|
const credId = positionals[1];
|
|
237
|
-
|
|
238
|
-
|
|
361
|
+
const newLabel = positionals[2];
|
|
362
|
+
if (!credId || !newLabel) {
|
|
363
|
+
console.error("Usage: zhihand rename <credential_id> <new_label>");
|
|
239
364
|
process.exit(1);
|
|
240
365
|
}
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
if (!record) {
|
|
366
|
+
const owner = findDeviceOwner(credId);
|
|
367
|
+
if (!owner) {
|
|
244
368
|
console.error(`Device '${credId}' not found.`);
|
|
245
369
|
process.exit(1);
|
|
246
370
|
}
|
|
247
|
-
//
|
|
371
|
+
// Server-side PATCH
|
|
372
|
+
const endpoint = resolveDefaultEndpoint();
|
|
248
373
|
try {
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
374
|
+
const res = await fetch(`${endpoint}/v1/credentials/${encodeURIComponent(credId)}`, {
|
|
375
|
+
method: "PATCH",
|
|
376
|
+
headers: {
|
|
377
|
+
"Content-Type": "application/json",
|
|
378
|
+
"Authorization": `Bearer ${owner.user.controller_token}`,
|
|
379
|
+
},
|
|
380
|
+
body: JSON.stringify({ device_label: newLabel }),
|
|
253
381
|
signal: AbortSignal.timeout(5000),
|
|
254
382
|
});
|
|
255
383
|
if (!res.ok) {
|
|
256
|
-
console.warn(`Warning: server
|
|
384
|
+
console.warn(`Warning: server rename returned ${res.status}`);
|
|
257
385
|
}
|
|
258
386
|
} catch (err) {
|
|
259
|
-
console.warn(`Warning: server
|
|
387
|
+
console.warn(`Warning: server rename failed: ${err.message}`);
|
|
260
388
|
}
|
|
261
|
-
|
|
262
|
-
console.log(`
|
|
389
|
+
updateDeviceLabel(owner.user.user_id, credId, newLabel);
|
|
390
|
+
console.log(`Renamed ${credId} to '${newLabel}'`);
|
|
263
391
|
break;
|
|
264
392
|
}
|
|
265
393
|
|
|
266
|
-
case "
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
console.error("Usage: zhihand rename <credential_id> <new_label>");
|
|
394
|
+
case "export": {
|
|
395
|
+
const userId = positionals[1];
|
|
396
|
+
if (!userId) {
|
|
397
|
+
console.error("Usage: zhihand export <user_id>");
|
|
271
398
|
process.exit(1);
|
|
272
399
|
}
|
|
273
|
-
|
|
274
|
-
|
|
400
|
+
const user = getUserRecord(userId);
|
|
401
|
+
if (!user) {
|
|
402
|
+
console.error(`User '${userId}' not found.`);
|
|
403
|
+
process.exit(1);
|
|
404
|
+
}
|
|
405
|
+
// Plain JSON to stdout
|
|
406
|
+
console.log(JSON.stringify({ user_id: user.user_id, controller_token: user.controller_token }, null, 2));
|
|
275
407
|
break;
|
|
276
408
|
}
|
|
277
409
|
|
|
278
|
-
case "
|
|
279
|
-
const
|
|
280
|
-
if (!
|
|
281
|
-
console.error("Usage: zhihand
|
|
410
|
+
case "import": {
|
|
411
|
+
const filePath = positionals[1];
|
|
412
|
+
if (!filePath) {
|
|
413
|
+
console.error("Usage: zhihand import <file>");
|
|
414
|
+
process.exit(1);
|
|
415
|
+
}
|
|
416
|
+
let data;
|
|
417
|
+
try {
|
|
418
|
+
data = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
419
|
+
} catch (err) {
|
|
420
|
+
console.error(`Error reading file: ${err.message}`);
|
|
421
|
+
process.exit(1);
|
|
422
|
+
}
|
|
423
|
+
if (!data.user_id || !data.controller_token) {
|
|
424
|
+
console.error("Invalid import file: must contain user_id and controller_token");
|
|
425
|
+
process.exit(1);
|
|
426
|
+
}
|
|
427
|
+
// Validate by fetching user info from server
|
|
428
|
+
const endpoint = resolveDefaultEndpoint();
|
|
429
|
+
let serverUser;
|
|
430
|
+
try {
|
|
431
|
+
const res = await fetch(`${endpoint}/v1/users/${encodeURIComponent(data.user_id)}`, {
|
|
432
|
+
headers: { "Authorization": `Bearer ${data.controller_token}` },
|
|
433
|
+
signal: AbortSignal.timeout(10000),
|
|
434
|
+
});
|
|
435
|
+
if (!res.ok) {
|
|
436
|
+
console.error(`Server validation failed: ${res.status}`);
|
|
437
|
+
process.exit(1);
|
|
438
|
+
}
|
|
439
|
+
serverUser = await res.json();
|
|
440
|
+
} catch (err) {
|
|
441
|
+
console.error(`Server validation failed: ${err.message}`);
|
|
442
|
+
process.exit(1);
|
|
443
|
+
}
|
|
444
|
+
// Fetch credentials
|
|
445
|
+
let creds = [];
|
|
446
|
+
try {
|
|
447
|
+
creds = await fetchUserCredentials(endpoint, data.user_id, data.controller_token);
|
|
448
|
+
} catch {
|
|
449
|
+
// Non-fatal
|
|
450
|
+
}
|
|
451
|
+
const devices = creds.map((c) => ({
|
|
452
|
+
credential_id: c.credential_id,
|
|
453
|
+
label: c.label ?? c.credential_id,
|
|
454
|
+
platform: c.platform ?? "unknown",
|
|
455
|
+
paired_at: c.paired_at ?? new Date().toISOString(),
|
|
456
|
+
last_seen_at: c.last_seen_at ?? new Date().toISOString(),
|
|
457
|
+
}));
|
|
458
|
+
const userRecord = {
|
|
459
|
+
user_id: data.user_id,
|
|
460
|
+
controller_token: data.controller_token,
|
|
461
|
+
label: serverUser.label ?? data.user_id,
|
|
462
|
+
created_at: serverUser.created_at ?? new Date().toISOString(),
|
|
463
|
+
devices,
|
|
464
|
+
};
|
|
465
|
+
addUser(userRecord);
|
|
466
|
+
console.log(`Imported user: ${userRecord.label} (${userRecord.user_id}) with ${devices.length} device(s)`);
|
|
467
|
+
break;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
case "rotate": {
|
|
471
|
+
const userId = positionals[1];
|
|
472
|
+
if (!userId) {
|
|
473
|
+
console.error("Usage: zhihand rotate <user_id>");
|
|
474
|
+
process.exit(1);
|
|
475
|
+
}
|
|
476
|
+
const user = getUserRecord(userId);
|
|
477
|
+
if (!user) {
|
|
478
|
+
console.error(`User '${userId}' not found.`);
|
|
479
|
+
process.exit(1);
|
|
480
|
+
}
|
|
481
|
+
const endpoint = resolveDefaultEndpoint();
|
|
482
|
+
// Find current token ID (we need to pass it in the URL)
|
|
483
|
+
// The API is POST /v1/users/{id}/controller-tokens/{token}/rotate
|
|
484
|
+
try {
|
|
485
|
+
const res = await fetch(
|
|
486
|
+
`${endpoint}/v1/users/${encodeURIComponent(userId)}/controller-tokens/${encodeURIComponent(user.controller_token)}/rotate`,
|
|
487
|
+
{
|
|
488
|
+
method: "POST",
|
|
489
|
+
headers: { "Authorization": `Bearer ${user.controller_token}` },
|
|
490
|
+
signal: AbortSignal.timeout(10000),
|
|
491
|
+
},
|
|
492
|
+
);
|
|
493
|
+
if (!res.ok) {
|
|
494
|
+
console.error(`Rotate failed: ${res.status}`);
|
|
495
|
+
process.exit(1);
|
|
496
|
+
}
|
|
497
|
+
const result = await res.json();
|
|
498
|
+
updateControllerToken(userId, result.new_token);
|
|
499
|
+
console.log(`Token rotated for ${userId}. New token saved.`);
|
|
500
|
+
} catch (err) {
|
|
501
|
+
console.error(`Rotate failed: ${err.message}`);
|
|
282
502
|
process.exit(1);
|
|
283
503
|
}
|
|
284
|
-
setDefaultDevice(credId);
|
|
285
|
-
console.log(`Default device set to ${credId}`);
|
|
286
504
|
break;
|
|
287
505
|
}
|
|
288
506
|
|
|
289
507
|
case "status": {
|
|
290
|
-
const
|
|
291
|
-
const cfg = loadConfig();
|
|
508
|
+
const users = listUsers();
|
|
292
509
|
const backend = loadBackendConfig();
|
|
293
510
|
const daemonPid = isAlreadyRunning();
|
|
294
511
|
|
|
295
|
-
if (
|
|
296
|
-
console.log("No
|
|
512
|
+
if (users.length === 0) {
|
|
513
|
+
console.log("No users configured. Run: zhihand setup");
|
|
297
514
|
} else {
|
|
298
|
-
console.log(`
|
|
299
|
-
for (const
|
|
300
|
-
|
|
301
|
-
|
|
515
|
+
console.log(`Users: ${users.length}`);
|
|
516
|
+
for (const u of users) {
|
|
517
|
+
console.log(` ${u.user_id} (${u.label}) — ${u.devices.length} device(s)`);
|
|
518
|
+
for (const d of u.devices) {
|
|
519
|
+
console.log(` ${d.credential_id} (${d.label}, ${d.platform})`);
|
|
520
|
+
}
|
|
302
521
|
}
|
|
303
522
|
}
|
|
304
523
|
const backendLabel = backend.activeBackend ? displayName(backend.activeBackend) : "(none)";
|
|
@@ -331,17 +550,11 @@ switch (command) {
|
|
|
331
550
|
}
|
|
332
551
|
|
|
333
552
|
case "setup": {
|
|
334
|
-
const
|
|
335
|
-
if (
|
|
336
|
-
console.log("No
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
}
|
|
340
|
-
const updated = listDeviceRecords();
|
|
341
|
-
if (updated.length > 0) {
|
|
342
|
-
const cfg = loadConfig();
|
|
343
|
-
const def = updated.find((r) => r.credential_id === cfg.default_credential_id) ?? updated[0];
|
|
344
|
-
console.log(`\nPaired: ${def.label} (${def.credential_id})\n`);
|
|
553
|
+
const users = listUsers();
|
|
554
|
+
if (users.length === 0) {
|
|
555
|
+
console.log("No users found. Starting pairing...\n");
|
|
556
|
+
const { userRecord } = await executePairingNewUser(values.label);
|
|
557
|
+
console.log(`\nUser created: ${userRecord.label} (${userRecord.user_id})\n`);
|
|
345
558
|
}
|
|
346
559
|
|
|
347
560
|
const tools = await detectCLITools();
|
|
@@ -416,7 +629,7 @@ switch (command) {
|
|
|
416
629
|
{ id: 38, phase: "Media", label: "Stop", kind: "system", params: { action: "stop" } },
|
|
417
630
|
{ id: 39, phase: "Hardware", label: "Brightness up", kind: "system", params: { action: "brightness_up" } },
|
|
418
631
|
{ id: 40, phase: "Hardware", label: "Brightness down", kind: "system", params: { action: "brightness_down" } },
|
|
419
|
-
{ id: 41, phase: "Hardware", label: "Power button (
|
|
632
|
+
{ id: 41, phase: "Hardware", label: "Power button (may lock screen)", kind: "system", params: { action: "power" }, unsafe: true },
|
|
420
633
|
];
|
|
421
634
|
|
|
422
635
|
// Parse first positional: credential_id (crd_*) or test ids
|
|
@@ -464,7 +677,7 @@ switch (command) {
|
|
|
464
677
|
}
|
|
465
678
|
}
|
|
466
679
|
|
|
467
|
-
console.log("
|
|
680
|
+
console.log("ZhiHand Device Test");
|
|
468
681
|
console.log(` Device: ${testConfig.credentialId}`);
|
|
469
682
|
console.log(` Endpoint: ${testConfig.controlPlaneEndpoint}\n`);
|
|
470
683
|
|
|
@@ -484,16 +697,16 @@ switch (command) {
|
|
|
484
697
|
} catch { /* non-fatal */ }
|
|
485
698
|
const getDevicePlatform = () => currentProfile?.platform ?? "unknown";
|
|
486
699
|
|
|
487
|
-
console.log("
|
|
700
|
+
console.log(" -- Capability readiness --");
|
|
488
701
|
if (!currentCaps) {
|
|
489
|
-
console.log("
|
|
702
|
+
console.log(" [!] Device profile not loaded — all capability gates will allow tests through.");
|
|
490
703
|
} else {
|
|
491
|
-
const fmt = (name, cap) => ` ${cap.ready ? "
|
|
704
|
+
const fmt = (name, cap) => ` ${cap.ready ? "[ok]" : "[!]"} ${name.padEnd(14)} ${cap.ready ? "ready" : "NOT ready"} — ${cap.reason}`;
|
|
492
705
|
console.log(fmt("screen_sharing", currentCaps.screen_sharing));
|
|
493
706
|
console.log(fmt("hid", currentCaps.hid));
|
|
494
707
|
console.log(fmt("live_session", currentCaps.live_session));
|
|
495
708
|
const ageStr = currentCaps.profile.age_ms >= 0 ? `${(currentCaps.profile.age_ms / 1000).toFixed(1)}s` : "unknown";
|
|
496
|
-
console.log(` ${currentCaps.profile.stale ? "
|
|
709
|
+
console.log(` ${currentCaps.profile.stale ? "[!]" : "[ok]"} profile age=${ageStr}${currentCaps.profile.stale ? " (STALE)" : ""}`);
|
|
497
710
|
if (forceRun) {
|
|
498
711
|
console.log(" --force passed: capability gates disabled.");
|
|
499
712
|
}
|
|
@@ -531,18 +744,18 @@ switch (command) {
|
|
|
531
744
|
const ackStatus = ack.command?.ack_status ?? "ok";
|
|
532
745
|
const resultInfo = ack.command?.ack_result ? ` ${JSON.stringify(ack.command.ack_result)}` : "";
|
|
533
746
|
if (ackStatus === "ok") {
|
|
534
|
-
console.log(
|
|
747
|
+
console.log(`[PASS] (${ms}ms)${resultInfo}`);
|
|
535
748
|
passed++;
|
|
536
749
|
} else {
|
|
537
|
-
console.log(
|
|
750
|
+
console.log(`[FAIL] [${ackStatus}] (${ms}ms)${resultInfo}`);
|
|
538
751
|
failed++;
|
|
539
752
|
}
|
|
540
753
|
} else {
|
|
541
|
-
console.log(
|
|
754
|
+
console.log(`[TIMEOUT] (${ms}ms)`);
|
|
542
755
|
failed++;
|
|
543
756
|
}
|
|
544
757
|
} catch (err) {
|
|
545
|
-
console.log(
|
|
758
|
+
console.log(`[FAIL] ${err.message} (${Date.now() - t0}ms)`);
|
|
546
759
|
failed++;
|
|
547
760
|
}
|
|
548
761
|
}
|
|
@@ -551,7 +764,7 @@ switch (command) {
|
|
|
551
764
|
const currentPlatform = getDevicePlatform();
|
|
552
765
|
if (t.platform && t.platform !== currentPlatform) {
|
|
553
766
|
totalSteps++; skipped++;
|
|
554
|
-
console.log(` ${String(t.id).padStart(2)}. ${t.label}...
|
|
767
|
+
console.log(` ${String(t.id).padStart(2)}. ${t.label}... [SKIP] (${t.platform}-only, device is ${currentPlatform})`);
|
|
555
768
|
return;
|
|
556
769
|
}
|
|
557
770
|
if (!forceRun && currentCaps) {
|
|
@@ -560,7 +773,7 @@ switch (command) {
|
|
|
560
773
|
const gate = requiredCap === "screen" ? currentCaps.screen_sharing : currentCaps.hid;
|
|
561
774
|
if (!gate.ready) {
|
|
562
775
|
totalSteps++; skipped++;
|
|
563
|
-
console.log(` ${String(t.id).padStart(2)}. ${t.label}...
|
|
776
|
+
console.log(` ${String(t.id).padStart(2)}. ${t.label}... [SKIP] (${requiredCap} not ready: ${gate.reason})`);
|
|
564
777
|
return;
|
|
565
778
|
}
|
|
566
779
|
}
|
|
@@ -580,14 +793,14 @@ switch (command) {
|
|
|
580
793
|
currentProfile = extractStatic(currentRawAttrs);
|
|
581
794
|
currentCaps = computeCapabilities(currentRawAttrs, profileReceivedAtMs);
|
|
582
795
|
const s = currentProfile;
|
|
583
|
-
console.log(
|
|
796
|
+
console.log(`[PASS] ${s.platform} ${s.model}, ${s.osVersion}, ${s.screenWidthPx}x${s.screenHeightPx} (${ms}ms)`);
|
|
584
797
|
passed++;
|
|
585
798
|
} else {
|
|
586
|
-
console.log(
|
|
799
|
+
console.log(`[!] Loaded but empty (${ms}ms)`);
|
|
587
800
|
failed++;
|
|
588
801
|
}
|
|
589
802
|
} catch (err) {
|
|
590
|
-
console.log(
|
|
803
|
+
console.log(`[FAIL] ${err.message} (${Date.now() - t0}ms)`);
|
|
591
804
|
failed++;
|
|
592
805
|
}
|
|
593
806
|
break;
|
|
@@ -598,6 +811,8 @@ switch (command) {
|
|
|
598
811
|
try {
|
|
599
812
|
const state = {
|
|
600
813
|
credentialId: testConfig.credentialId,
|
|
814
|
+
userId: "",
|
|
815
|
+
userLabel: "",
|
|
601
816
|
label: "(test)",
|
|
602
817
|
platform: getDevicePlatform(),
|
|
603
818
|
online: true,
|
|
@@ -606,10 +821,7 @@ switch (command) {
|
|
|
606
821
|
capabilities: currentCaps,
|
|
607
822
|
profileReceivedAtMs,
|
|
608
823
|
rawAttributes: currentRawAttrs,
|
|
609
|
-
|
|
610
|
-
sseConnected: false,
|
|
611
|
-
heartbeatTimer: null,
|
|
612
|
-
record: { credential_id: testConfig.credentialId, controller_token: "", endpoint: "", label: "", platform: "unknown", paired_at: "", last_seen_at: "" },
|
|
824
|
+
record: { credential_id: testConfig.credentialId, label: "", platform: "unknown", paired_at: "", last_seen_at: "" },
|
|
613
825
|
};
|
|
614
826
|
const status = formatDeviceStatus(state);
|
|
615
827
|
const topLevel = Object.keys(status).filter((k) => k !== "raw" && k !== "capabilities");
|
|
@@ -618,12 +830,12 @@ switch (command) {
|
|
|
618
830
|
const capReadySummary = ["screen_sharing", "hid", "live_session"]
|
|
619
831
|
.map((k) => `${k}=${caps[k]?.ready ? "ready" : "not-ready"}`)
|
|
620
832
|
.join(", ");
|
|
621
|
-
console.log(
|
|
833
|
+
console.log(`[PASS] ${topLevel.length} curated + ${rawKeys.length} raw attributes; ${capReadySummary}`);
|
|
622
834
|
console.log(` curated: ${topLevel.join(", ")}`);
|
|
623
835
|
console.log(` raw: ${rawKeys.join(", ")}`);
|
|
624
836
|
passed++;
|
|
625
837
|
} catch (err) {
|
|
626
|
-
console.log(
|
|
838
|
+
console.log(`[FAIL] ${err.message}`);
|
|
627
839
|
failed++;
|
|
628
840
|
}
|
|
629
841
|
break;
|
|
@@ -637,7 +849,7 @@ switch (command) {
|
|
|
637
849
|
const queued = await enqueueCommand(testConfig, cmd);
|
|
638
850
|
const ack = await waitForCommandAck(testConfig, { commandId: queued.id, timeoutMs: 10_000 });
|
|
639
851
|
if (!ack.acked) {
|
|
640
|
-
console.log(
|
|
852
|
+
console.log(`[TIMEOUT] (${Date.now() - t0}ms)`);
|
|
641
853
|
failed++;
|
|
642
854
|
break;
|
|
643
855
|
}
|
|
@@ -646,14 +858,14 @@ switch (command) {
|
|
|
646
858
|
const ms = Date.now() - t0;
|
|
647
859
|
if (shot.stale) {
|
|
648
860
|
const threshold = getSnapshotStaleThresholdMs();
|
|
649
|
-
console.log(
|
|
861
|
+
console.log(`[FAIL] Stale (${kb}KB, age=${(shot.ageMs / 1000).toFixed(1)}s > ${(threshold / 1000).toFixed(1)}s) ${shot.width}x${shot.height} seq=${shot.sequence} — phone may not be screen-sharing (${ms}ms)`);
|
|
650
862
|
failed++;
|
|
651
863
|
} else {
|
|
652
|
-
console.log(
|
|
864
|
+
console.log(`[PASS] ${kb}KB, ${shot.width}x${shot.height}, age=${shot.ageMs >= 0 ? `${shot.ageMs}ms` : "?"}, seq=${shot.sequence} (${ms}ms)`);
|
|
653
865
|
passed++;
|
|
654
866
|
}
|
|
655
867
|
} catch (err) {
|
|
656
|
-
console.log(
|
|
868
|
+
console.log(`[FAIL] ${err.message} (${Date.now() - t0}ms)`);
|
|
657
869
|
failed++;
|
|
658
870
|
}
|
|
659
871
|
break;
|
|
@@ -686,14 +898,14 @@ switch (command) {
|
|
|
686
898
|
if (selectedIds) {
|
|
687
899
|
const foundIds = new Set(toRun.map((t) => t.id));
|
|
688
900
|
const missing = [...selectedIds].filter((id) => !foundIds.has(id));
|
|
689
|
-
if (missing.length) console.warn(
|
|
901
|
+
if (missing.length) console.warn(`[!] Unknown test IDs: ${missing.join(", ")}`);
|
|
690
902
|
}
|
|
691
903
|
|
|
692
904
|
let currentPhase = "";
|
|
693
905
|
for (let i = 0; i < toRun.length; i++) {
|
|
694
906
|
const t = toRun[i];
|
|
695
907
|
if (t.phase !== currentPhase) {
|
|
696
|
-
console.log(`
|
|
908
|
+
console.log(` -- ${t.phase} --`);
|
|
697
909
|
currentPhase = t.phase;
|
|
698
910
|
}
|
|
699
911
|
await runSingleTest(t);
|
|
@@ -701,8 +913,8 @@ switch (command) {
|
|
|
701
913
|
}
|
|
702
914
|
|
|
703
915
|
console.log(`\n Result: ${passed}/${totalSteps} passed, ${failed} failed, ${skipped} skipped`);
|
|
704
|
-
if (failed === 0) console.log("
|
|
705
|
-
else console.log(`
|
|
916
|
+
if (failed === 0) console.log(" All tests passed! Device is fully responsive.");
|
|
917
|
+
else console.log(` ${failed} test(s) failed. Check phone connectivity.`);
|
|
706
918
|
process.exit(failed > 0 ? 1 : 0);
|
|
707
919
|
}
|
|
708
920
|
|
package/dist/core/command.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { log } from "./logger.js";
|
|
2
|
+
const dbg = (msg) => log.debug(msg);
|
|
2
3
|
let messageCounter = 0;
|
|
3
4
|
function nextMessageId() {
|
|
4
5
|
messageCounter = (messageCounter + 1) % 1000;
|
|
@@ -157,7 +158,7 @@ export async function enqueueCommand(config, command) {
|
|
|
157
158
|
method: "POST",
|
|
158
159
|
headers: {
|
|
159
160
|
"Content-Type": "application/json",
|
|
160
|
-
"
|
|
161
|
+
"Authorization": `Bearer ${config.controllerToken}`,
|
|
161
162
|
},
|
|
162
163
|
body: JSON.stringify(body),
|
|
163
164
|
});
|
|
@@ -173,7 +174,7 @@ export async function getCommand(config, commandId) {
|
|
|
173
174
|
const url = `${config.controlPlaneEndpoint}/v1/credentials/${encodeURIComponent(config.credentialId)}/commands/${encodeURIComponent(commandId)}`;
|
|
174
175
|
dbg(`[cmd] GET ${url}`);
|
|
175
176
|
const response = await fetch(url, {
|
|
176
|
-
headers: { "
|
|
177
|
+
headers: { "Authorization": `Bearer ${config.controllerToken}` },
|
|
177
178
|
});
|
|
178
179
|
if (!response.ok) {
|
|
179
180
|
dbg(`[cmd] Get failed: ${response.status}`);
|