@zhihand/mcp 0.30.0 → 0.32.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/zhihand +316 -129
- 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 +4 -4
- 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/README.md +0 -359
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.1";
|
|
24
34
|
|
|
25
35
|
const CLI_TOOL_MAP = {
|
|
26
36
|
claude: "claudecode",
|
|
@@ -65,12 +75,14 @@ Usage:
|
|
|
65
75
|
zhihand claude Switch backend to Claude Code
|
|
66
76
|
zhihand codex Switch backend to Codex CLI
|
|
67
77
|
|
|
68
|
-
zhihand
|
|
69
|
-
zhihand pair
|
|
70
|
-
zhihand list
|
|
71
|
-
zhihand unpair <
|
|
72
|
-
zhihand rename <cred> <n> Rename a device
|
|
73
|
-
zhihand
|
|
78
|
+
zhihand pair [--label X] Pair new user + first device + auto-configure MCP
|
|
79
|
+
zhihand pair <user_id> Add device to existing user
|
|
80
|
+
zhihand list [<user_id>] List users/devices with real-time online status
|
|
81
|
+
zhihand unpair <id> Remove user (usr_*) or device (credential)
|
|
82
|
+
zhihand rename <cred> <n> Rename a device (server-side + local)
|
|
83
|
+
zhihand export <user_id> Export user credentials as JSON to stdout
|
|
84
|
+
zhihand import <file> Import user credentials from JSON file
|
|
85
|
+
zhihand rotate <user_id> Rotate controller token
|
|
74
86
|
zhihand detect Detect available CLI tools
|
|
75
87
|
|
|
76
88
|
zhihand test [cred] [ids] Run device tests (all, specific ids, or for a credential)
|
|
@@ -202,103 +214,309 @@ switch (command) {
|
|
|
202
214
|
}
|
|
203
215
|
|
|
204
216
|
case "pair": {
|
|
205
|
-
const
|
|
217
|
+
const arg1 = positionals[1];
|
|
206
218
|
const label = values.label ?? undefined;
|
|
207
|
-
|
|
219
|
+
|
|
220
|
+
if (arg1 && arg1.startsWith("usr_")) {
|
|
221
|
+
// Add device to existing user
|
|
222
|
+
const deviceRecord = await executePairingAddDevice(arg1, label);
|
|
223
|
+
console.log(`\nDevice paired: ${deviceRecord.label} (${deviceRecord.credential_id})`);
|
|
224
|
+
} else {
|
|
225
|
+
// New user + first device
|
|
226
|
+
const { userRecord, deviceRecord } = await executePairingNewUser(label);
|
|
227
|
+
console.log(`\nUser created: ${userRecord.label} (${userRecord.user_id})`);
|
|
228
|
+
console.log(`Device paired: ${deviceRecord.label} (${deviceRecord.credential_id})`);
|
|
229
|
+
console.log(`\nAdd another device: zhihand pair ${userRecord.user_id}`);
|
|
230
|
+
|
|
231
|
+
// Auto-configure MCP hosts
|
|
232
|
+
const tools = await detectCLITools();
|
|
233
|
+
if (tools.length > 0) {
|
|
234
|
+
const best = tools.find((t) => t.loggedIn) ?? tools[0];
|
|
235
|
+
console.log(`\nAuto-configuring MCP for ${displayName(best.name)}...`);
|
|
236
|
+
const backendCfg = loadBackendConfig();
|
|
237
|
+
configureMCP(best.name, backendCfg.activeBackend);
|
|
238
|
+
saveBackendConfig({ activeBackend: best.name });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
208
241
|
break;
|
|
209
242
|
}
|
|
210
243
|
|
|
211
244
|
case "list": {
|
|
212
|
-
const
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
245
|
+
const userIdFilter = positionals[1];
|
|
246
|
+
const users = listUsers();
|
|
247
|
+
const endpoint = resolveDefaultEndpoint();
|
|
248
|
+
|
|
249
|
+
if (users.length === 0) {
|
|
250
|
+
console.log("No users configured. Run: zhihand pair");
|
|
216
251
|
break;
|
|
217
252
|
}
|
|
218
|
-
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
253
|
+
|
|
254
|
+
const filteredUsers = userIdFilter
|
|
255
|
+
? users.filter((u) => u.user_id === userIdFilter)
|
|
256
|
+
: users;
|
|
257
|
+
|
|
258
|
+
if (filteredUsers.length === 0) {
|
|
259
|
+
console.error(`User '${userIdFilter}' not found.`);
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
for (const user of filteredUsers) {
|
|
264
|
+
console.log(`\nUSER: ${user.label} (${user.user_id})`);
|
|
265
|
+
|
|
266
|
+
// Fetch real-time online status from server
|
|
267
|
+
let onlineMap = new Map();
|
|
268
|
+
try {
|
|
269
|
+
const creds = await fetchUserCredentials(endpoint, user.user_id, user.controller_token);
|
|
270
|
+
for (const c of creds) {
|
|
271
|
+
onlineMap.set(c.credential_id, c.online ?? false);
|
|
272
|
+
}
|
|
273
|
+
} catch {
|
|
274
|
+
// Fallback: no online status
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (user.devices.length === 0) {
|
|
278
|
+
console.log(" (no devices)");
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const header = ["DEVICE_ID", "LABEL", "PLATFORM", "ONLINE", "PAIRED"];
|
|
283
|
+
const rows = user.devices.map((d) => [
|
|
284
|
+
d.credential_id,
|
|
285
|
+
d.label,
|
|
286
|
+
d.platform,
|
|
287
|
+
onlineMap.has(d.credential_id)
|
|
288
|
+
? (onlineMap.get(d.credential_id) ? "yes" : "no")
|
|
289
|
+
: "?",
|
|
290
|
+
d.paired_at,
|
|
291
|
+
]);
|
|
292
|
+
const widths = header.map((h, i) => Math.max(h.length, ...rows.map((row) => row[i].length)));
|
|
293
|
+
const fmt = (row) => row.map((c, i) => c.padEnd(widths[i])).join(" ");
|
|
294
|
+
console.log(" " + fmt(header));
|
|
295
|
+
console.log(" " + widths.map((w) => "-".repeat(w)).join(" "));
|
|
296
|
+
for (const r of rows) console.log(" " + fmt(r));
|
|
297
|
+
}
|
|
232
298
|
break;
|
|
233
299
|
}
|
|
234
300
|
|
|
235
301
|
case "unpair": {
|
|
302
|
+
const id = positionals[1];
|
|
303
|
+
if (!id) {
|
|
304
|
+
console.error("Usage: zhihand unpair <user_id | credential_id>");
|
|
305
|
+
process.exit(1);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const endpoint = resolveDefaultEndpoint();
|
|
309
|
+
|
|
310
|
+
if (id.startsWith("usr_")) {
|
|
311
|
+
// Delete user (cascade)
|
|
312
|
+
const user = getUserRecord(id);
|
|
313
|
+
if (!user) {
|
|
314
|
+
console.error(`User '${id}' not found.`);
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
317
|
+
// Best-effort server-side delete
|
|
318
|
+
try {
|
|
319
|
+
const res = await fetch(`${endpoint}/v1/users/${encodeURIComponent(id)}`, {
|
|
320
|
+
method: "DELETE",
|
|
321
|
+
headers: { "Authorization": `Bearer ${user.controller_token}` },
|
|
322
|
+
signal: AbortSignal.timeout(5000),
|
|
323
|
+
});
|
|
324
|
+
if (!res.ok) {
|
|
325
|
+
console.warn(`Warning: server delete returned ${res.status} (continuing to remove locally)`);
|
|
326
|
+
}
|
|
327
|
+
} catch (err) {
|
|
328
|
+
console.warn(`Warning: server delete failed: ${err.message} (continuing to remove locally)`);
|
|
329
|
+
}
|
|
330
|
+
removeUser(id);
|
|
331
|
+
console.log(`Removed user: ${id} (${user.label})`);
|
|
332
|
+
} else {
|
|
333
|
+
// Delete single credential
|
|
334
|
+
const owner = findDeviceOwner(id);
|
|
335
|
+
if (!owner) {
|
|
336
|
+
console.error(`Device '${id}' not found.`);
|
|
337
|
+
process.exit(1);
|
|
338
|
+
}
|
|
339
|
+
// Best-effort server-side delete
|
|
340
|
+
try {
|
|
341
|
+
const res = await fetch(`${endpoint}/v1/credentials/${encodeURIComponent(id)}`, {
|
|
342
|
+
method: "DELETE",
|
|
343
|
+
headers: { "Authorization": `Bearer ${owner.user.controller_token}` },
|
|
344
|
+
signal: AbortSignal.timeout(5000),
|
|
345
|
+
});
|
|
346
|
+
if (!res.ok) {
|
|
347
|
+
console.warn(`Warning: server delete returned ${res.status} (continuing to remove locally)`);
|
|
348
|
+
}
|
|
349
|
+
} catch (err) {
|
|
350
|
+
console.warn(`Warning: server delete failed: ${err.message} (continuing to remove locally)`);
|
|
351
|
+
}
|
|
352
|
+
removeDeviceFromUser(owner.user.user_id, id);
|
|
353
|
+
console.log(`Removed device: ${id}`);
|
|
354
|
+
}
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
case "rename": {
|
|
236
359
|
const credId = positionals[1];
|
|
237
|
-
|
|
238
|
-
|
|
360
|
+
const newLabel = positionals[2];
|
|
361
|
+
if (!credId || !newLabel) {
|
|
362
|
+
console.error("Usage: zhihand rename <credential_id> <new_label>");
|
|
239
363
|
process.exit(1);
|
|
240
364
|
}
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
if (!record) {
|
|
365
|
+
const owner = findDeviceOwner(credId);
|
|
366
|
+
if (!owner) {
|
|
244
367
|
console.error(`Device '${credId}' not found.`);
|
|
245
368
|
process.exit(1);
|
|
246
369
|
}
|
|
247
|
-
//
|
|
370
|
+
// Server-side PATCH
|
|
371
|
+
const endpoint = resolveDefaultEndpoint();
|
|
248
372
|
try {
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
373
|
+
const res = await fetch(`${endpoint}/v1/credentials/${encodeURIComponent(credId)}`, {
|
|
374
|
+
method: "PATCH",
|
|
375
|
+
headers: {
|
|
376
|
+
"Content-Type": "application/json",
|
|
377
|
+
"Authorization": `Bearer ${owner.user.controller_token}`,
|
|
378
|
+
},
|
|
379
|
+
body: JSON.stringify({ device_label: newLabel }),
|
|
253
380
|
signal: AbortSignal.timeout(5000),
|
|
254
381
|
});
|
|
255
382
|
if (!res.ok) {
|
|
256
|
-
console.warn(`Warning: server
|
|
383
|
+
console.warn(`Warning: server rename returned ${res.status}`);
|
|
257
384
|
}
|
|
258
385
|
} catch (err) {
|
|
259
|
-
console.warn(`Warning: server
|
|
386
|
+
console.warn(`Warning: server rename failed: ${err.message}`);
|
|
260
387
|
}
|
|
261
|
-
|
|
262
|
-
console.log(`
|
|
388
|
+
updateDeviceLabel(owner.user.user_id, credId, newLabel);
|
|
389
|
+
console.log(`Renamed ${credId} to '${newLabel}'`);
|
|
263
390
|
break;
|
|
264
391
|
}
|
|
265
392
|
|
|
266
|
-
case "
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
console.error("Usage: zhihand rename <credential_id> <new_label>");
|
|
393
|
+
case "export": {
|
|
394
|
+
const userId = positionals[1];
|
|
395
|
+
if (!userId) {
|
|
396
|
+
console.error("Usage: zhihand export <user_id>");
|
|
271
397
|
process.exit(1);
|
|
272
398
|
}
|
|
273
|
-
|
|
274
|
-
|
|
399
|
+
const user = getUserRecord(userId);
|
|
400
|
+
if (!user) {
|
|
401
|
+
console.error(`User '${userId}' not found.`);
|
|
402
|
+
process.exit(1);
|
|
403
|
+
}
|
|
404
|
+
// Plain JSON to stdout
|
|
405
|
+
console.log(JSON.stringify({ user_id: user.user_id, controller_token: user.controller_token }, null, 2));
|
|
275
406
|
break;
|
|
276
407
|
}
|
|
277
408
|
|
|
278
|
-
case "
|
|
279
|
-
const
|
|
280
|
-
if (!
|
|
281
|
-
console.error("Usage: zhihand
|
|
409
|
+
case "import": {
|
|
410
|
+
const filePath = positionals[1];
|
|
411
|
+
if (!filePath) {
|
|
412
|
+
console.error("Usage: zhihand import <file>");
|
|
413
|
+
process.exit(1);
|
|
414
|
+
}
|
|
415
|
+
let data;
|
|
416
|
+
try {
|
|
417
|
+
data = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
418
|
+
} catch (err) {
|
|
419
|
+
console.error(`Error reading file: ${err.message}`);
|
|
420
|
+
process.exit(1);
|
|
421
|
+
}
|
|
422
|
+
if (!data.user_id || !data.controller_token) {
|
|
423
|
+
console.error("Invalid import file: must contain user_id and controller_token");
|
|
424
|
+
process.exit(1);
|
|
425
|
+
}
|
|
426
|
+
// Validate by fetching user info from server
|
|
427
|
+
const endpoint = resolveDefaultEndpoint();
|
|
428
|
+
let serverUser;
|
|
429
|
+
try {
|
|
430
|
+
const res = await fetch(`${endpoint}/v1/users/${encodeURIComponent(data.user_id)}`, {
|
|
431
|
+
headers: { "Authorization": `Bearer ${data.controller_token}` },
|
|
432
|
+
signal: AbortSignal.timeout(10000),
|
|
433
|
+
});
|
|
434
|
+
if (!res.ok) {
|
|
435
|
+
console.error(`Server validation failed: ${res.status}`);
|
|
436
|
+
process.exit(1);
|
|
437
|
+
}
|
|
438
|
+
serverUser = await res.json();
|
|
439
|
+
} catch (err) {
|
|
440
|
+
console.error(`Server validation failed: ${err.message}`);
|
|
441
|
+
process.exit(1);
|
|
442
|
+
}
|
|
443
|
+
// Fetch credentials
|
|
444
|
+
let creds = [];
|
|
445
|
+
try {
|
|
446
|
+
creds = await fetchUserCredentials(endpoint, data.user_id, data.controller_token);
|
|
447
|
+
} catch {
|
|
448
|
+
// Non-fatal
|
|
449
|
+
}
|
|
450
|
+
const devices = creds.map((c) => ({
|
|
451
|
+
credential_id: c.credential_id,
|
|
452
|
+
label: c.label ?? c.credential_id,
|
|
453
|
+
platform: c.platform ?? "unknown",
|
|
454
|
+
paired_at: c.paired_at ?? new Date().toISOString(),
|
|
455
|
+
last_seen_at: c.last_seen_at ?? new Date().toISOString(),
|
|
456
|
+
}));
|
|
457
|
+
const userRecord = {
|
|
458
|
+
user_id: data.user_id,
|
|
459
|
+
controller_token: data.controller_token,
|
|
460
|
+
label: serverUser.label ?? data.user_id,
|
|
461
|
+
created_at: serverUser.created_at ?? new Date().toISOString(),
|
|
462
|
+
devices,
|
|
463
|
+
};
|
|
464
|
+
addUser(userRecord);
|
|
465
|
+
console.log(`Imported user: ${userRecord.label} (${userRecord.user_id}) with ${devices.length} device(s)`);
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
case "rotate": {
|
|
470
|
+
const userId = positionals[1];
|
|
471
|
+
if (!userId) {
|
|
472
|
+
console.error("Usage: zhihand rotate <user_id>");
|
|
473
|
+
process.exit(1);
|
|
474
|
+
}
|
|
475
|
+
const user = getUserRecord(userId);
|
|
476
|
+
if (!user) {
|
|
477
|
+
console.error(`User '${userId}' not found.`);
|
|
478
|
+
process.exit(1);
|
|
479
|
+
}
|
|
480
|
+
const endpoint = resolveDefaultEndpoint();
|
|
481
|
+
// Find current token ID (we need to pass it in the URL)
|
|
482
|
+
// The API is POST /v1/users/{id}/controller-tokens/{token}/rotate
|
|
483
|
+
try {
|
|
484
|
+
const res = await fetch(
|
|
485
|
+
`${endpoint}/v1/users/${encodeURIComponent(userId)}/controller-tokens/${encodeURIComponent(user.controller_token)}/rotate`,
|
|
486
|
+
{
|
|
487
|
+
method: "POST",
|
|
488
|
+
headers: { "Authorization": `Bearer ${user.controller_token}` },
|
|
489
|
+
signal: AbortSignal.timeout(10000),
|
|
490
|
+
},
|
|
491
|
+
);
|
|
492
|
+
if (!res.ok) {
|
|
493
|
+
console.error(`Rotate failed: ${res.status}`);
|
|
494
|
+
process.exit(1);
|
|
495
|
+
}
|
|
496
|
+
const result = await res.json();
|
|
497
|
+
updateControllerToken(userId, result.new_token);
|
|
498
|
+
console.log(`Token rotated for ${userId}. New token saved.`);
|
|
499
|
+
} catch (err) {
|
|
500
|
+
console.error(`Rotate failed: ${err.message}`);
|
|
282
501
|
process.exit(1);
|
|
283
502
|
}
|
|
284
|
-
setDefaultDevice(credId);
|
|
285
|
-
console.log(`Default device set to ${credId}`);
|
|
286
503
|
break;
|
|
287
504
|
}
|
|
288
505
|
|
|
289
506
|
case "status": {
|
|
290
|
-
const
|
|
291
|
-
const cfg = loadConfig();
|
|
507
|
+
const users = listUsers();
|
|
292
508
|
const backend = loadBackendConfig();
|
|
293
509
|
const daemonPid = isAlreadyRunning();
|
|
294
510
|
|
|
295
|
-
if (
|
|
296
|
-
console.log("No
|
|
511
|
+
if (users.length === 0) {
|
|
512
|
+
console.log("No users configured. Run: zhihand pair");
|
|
297
513
|
} else {
|
|
298
|
-
console.log(`
|
|
299
|
-
for (const
|
|
300
|
-
|
|
301
|
-
|
|
514
|
+
console.log(`Users: ${users.length}`);
|
|
515
|
+
for (const u of users) {
|
|
516
|
+
console.log(` ${u.user_id} (${u.label}) — ${u.devices.length} device(s)`);
|
|
517
|
+
for (const d of u.devices) {
|
|
518
|
+
console.log(` ${d.credential_id} (${d.label}, ${d.platform})`);
|
|
519
|
+
}
|
|
302
520
|
}
|
|
303
521
|
}
|
|
304
522
|
const backendLabel = backend.activeBackend ? displayName(backend.activeBackend) : "(none)";
|
|
@@ -331,44 +549,14 @@ switch (command) {
|
|
|
331
549
|
}
|
|
332
550
|
|
|
333
551
|
case "setup": {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
console.log("No paired device found. Starting pairing...\n");
|
|
337
|
-
const edgeId = `mcp-${Date.now().toString(36)}`;
|
|
338
|
-
await executePairing(DEFAULT_ENDPOINT, edgeId, values.label);
|
|
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`);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
const tools = await detectCLITools();
|
|
348
|
-
console.log(formatDetectedTools(tools));
|
|
349
|
-
|
|
350
|
-
if (tools.length === 0) {
|
|
351
|
-
console.log("\nNo CLI tools detected. Install one of: Claude Code, Codex CLI, Gemini CLI.");
|
|
352
|
-
break;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
const best = tools.find((t) => t.loggedIn) ?? tools[0];
|
|
356
|
-
const config = loadBackendConfig();
|
|
357
|
-
|
|
358
|
-
console.log(`\nAuto-selecting backend: ${displayName(best.name)}...`);
|
|
359
|
-
if (values.port) process.env.ZHIHAND_PORT = values.port;
|
|
360
|
-
configureMCP(best.name, config.activeBackend);
|
|
361
|
-
saveBackendConfig({ activeBackend: best.name });
|
|
362
|
-
|
|
363
|
-
console.log(`\nStarting daemon...\n`);
|
|
364
|
-
const port = values.port ? parseInt(values.port, 10) : undefined;
|
|
365
|
-
await startDaemon({ port });
|
|
552
|
+
console.log("'zhihand setup' has been merged into 'zhihand pair'.");
|
|
553
|
+
console.log("Run: zhihand pair");
|
|
366
554
|
break;
|
|
367
555
|
}
|
|
368
556
|
|
|
369
557
|
case "test": {
|
|
370
558
|
const { createControlCommand, createSystemCommand, enqueueCommand } = await import("../dist/core/command.js");
|
|
371
|
-
const { waitForCommandAck } = await import("../dist/core/
|
|
559
|
+
const { waitForCommandAck } = await import("../dist/core/ws.js");
|
|
372
560
|
const { fetchScreenshot, getSnapshotStaleThresholdMs } = await import("../dist/core/screenshot.js");
|
|
373
561
|
const { fetchDeviceProfileOnce, extractStatic, computeCapabilities, formatDeviceStatus } = await import("../dist/core/device.js");
|
|
374
562
|
|
|
@@ -416,7 +604,7 @@ switch (command) {
|
|
|
416
604
|
{ id: 38, phase: "Media", label: "Stop", kind: "system", params: { action: "stop" } },
|
|
417
605
|
{ id: 39, phase: "Hardware", label: "Brightness up", kind: "system", params: { action: "brightness_up" } },
|
|
418
606
|
{ id: 40, phase: "Hardware", label: "Brightness down", kind: "system", params: { action: "brightness_down" } },
|
|
419
|
-
{ id: 41, phase: "Hardware", label: "Power button (
|
|
607
|
+
{ id: 41, phase: "Hardware", label: "Power button (may lock screen)", kind: "system", params: { action: "power" }, unsafe: true },
|
|
420
608
|
];
|
|
421
609
|
|
|
422
610
|
// Parse first positional: credential_id (crd_*) or test ids
|
|
@@ -464,7 +652,7 @@ switch (command) {
|
|
|
464
652
|
}
|
|
465
653
|
}
|
|
466
654
|
|
|
467
|
-
console.log("
|
|
655
|
+
console.log("ZhiHand Device Test");
|
|
468
656
|
console.log(` Device: ${testConfig.credentialId}`);
|
|
469
657
|
console.log(` Endpoint: ${testConfig.controlPlaneEndpoint}\n`);
|
|
470
658
|
|
|
@@ -484,16 +672,16 @@ switch (command) {
|
|
|
484
672
|
} catch { /* non-fatal */ }
|
|
485
673
|
const getDevicePlatform = () => currentProfile?.platform ?? "unknown";
|
|
486
674
|
|
|
487
|
-
console.log("
|
|
675
|
+
console.log(" -- Capability readiness --");
|
|
488
676
|
if (!currentCaps) {
|
|
489
|
-
console.log("
|
|
677
|
+
console.log(" [!] Device profile not loaded — all capability gates will allow tests through.");
|
|
490
678
|
} else {
|
|
491
|
-
const fmt = (name, cap) => ` ${cap.ready ? "
|
|
679
|
+
const fmt = (name, cap) => ` ${cap.ready ? "[ok]" : "[!]"} ${name.padEnd(14)} ${cap.ready ? "ready" : "NOT ready"} — ${cap.reason}`;
|
|
492
680
|
console.log(fmt("screen_sharing", currentCaps.screen_sharing));
|
|
493
681
|
console.log(fmt("hid", currentCaps.hid));
|
|
494
682
|
console.log(fmt("live_session", currentCaps.live_session));
|
|
495
683
|
const ageStr = currentCaps.profile.age_ms >= 0 ? `${(currentCaps.profile.age_ms / 1000).toFixed(1)}s` : "unknown";
|
|
496
|
-
console.log(` ${currentCaps.profile.stale ? "
|
|
684
|
+
console.log(` ${currentCaps.profile.stale ? "[!]" : "[ok]"} profile age=${ageStr}${currentCaps.profile.stale ? " (STALE)" : ""}`);
|
|
497
685
|
if (forceRun) {
|
|
498
686
|
console.log(" --force passed: capability gates disabled.");
|
|
499
687
|
}
|
|
@@ -531,18 +719,18 @@ switch (command) {
|
|
|
531
719
|
const ackStatus = ack.command?.ack_status ?? "ok";
|
|
532
720
|
const resultInfo = ack.command?.ack_result ? ` ${JSON.stringify(ack.command.ack_result)}` : "";
|
|
533
721
|
if (ackStatus === "ok") {
|
|
534
|
-
console.log(
|
|
722
|
+
console.log(`[PASS] (${ms}ms)${resultInfo}`);
|
|
535
723
|
passed++;
|
|
536
724
|
} else {
|
|
537
|
-
console.log(
|
|
725
|
+
console.log(`[FAIL] [${ackStatus}] (${ms}ms)${resultInfo}`);
|
|
538
726
|
failed++;
|
|
539
727
|
}
|
|
540
728
|
} else {
|
|
541
|
-
console.log(
|
|
729
|
+
console.log(`[TIMEOUT] (${ms}ms)`);
|
|
542
730
|
failed++;
|
|
543
731
|
}
|
|
544
732
|
} catch (err) {
|
|
545
|
-
console.log(
|
|
733
|
+
console.log(`[FAIL] ${err.message} (${Date.now() - t0}ms)`);
|
|
546
734
|
failed++;
|
|
547
735
|
}
|
|
548
736
|
}
|
|
@@ -551,7 +739,7 @@ switch (command) {
|
|
|
551
739
|
const currentPlatform = getDevicePlatform();
|
|
552
740
|
if (t.platform && t.platform !== currentPlatform) {
|
|
553
741
|
totalSteps++; skipped++;
|
|
554
|
-
console.log(` ${String(t.id).padStart(2)}. ${t.label}...
|
|
742
|
+
console.log(` ${String(t.id).padStart(2)}. ${t.label}... [SKIP] (${t.platform}-only, device is ${currentPlatform})`);
|
|
555
743
|
return;
|
|
556
744
|
}
|
|
557
745
|
if (!forceRun && currentCaps) {
|
|
@@ -560,7 +748,7 @@ switch (command) {
|
|
|
560
748
|
const gate = requiredCap === "screen" ? currentCaps.screen_sharing : currentCaps.hid;
|
|
561
749
|
if (!gate.ready) {
|
|
562
750
|
totalSteps++; skipped++;
|
|
563
|
-
console.log(` ${String(t.id).padStart(2)}. ${t.label}...
|
|
751
|
+
console.log(` ${String(t.id).padStart(2)}. ${t.label}... [SKIP] (${requiredCap} not ready: ${gate.reason})`);
|
|
564
752
|
return;
|
|
565
753
|
}
|
|
566
754
|
}
|
|
@@ -580,14 +768,14 @@ switch (command) {
|
|
|
580
768
|
currentProfile = extractStatic(currentRawAttrs);
|
|
581
769
|
currentCaps = computeCapabilities(currentRawAttrs, profileReceivedAtMs);
|
|
582
770
|
const s = currentProfile;
|
|
583
|
-
console.log(
|
|
771
|
+
console.log(`[PASS] ${s.platform} ${s.model}, ${s.osVersion}, ${s.screenWidthPx}x${s.screenHeightPx} (${ms}ms)`);
|
|
584
772
|
passed++;
|
|
585
773
|
} else {
|
|
586
|
-
console.log(
|
|
774
|
+
console.log(`[!] Loaded but empty (${ms}ms)`);
|
|
587
775
|
failed++;
|
|
588
776
|
}
|
|
589
777
|
} catch (err) {
|
|
590
|
-
console.log(
|
|
778
|
+
console.log(`[FAIL] ${err.message} (${Date.now() - t0}ms)`);
|
|
591
779
|
failed++;
|
|
592
780
|
}
|
|
593
781
|
break;
|
|
@@ -598,6 +786,8 @@ switch (command) {
|
|
|
598
786
|
try {
|
|
599
787
|
const state = {
|
|
600
788
|
credentialId: testConfig.credentialId,
|
|
789
|
+
userId: "",
|
|
790
|
+
userLabel: "",
|
|
601
791
|
label: "(test)",
|
|
602
792
|
platform: getDevicePlatform(),
|
|
603
793
|
online: true,
|
|
@@ -606,10 +796,7 @@ switch (command) {
|
|
|
606
796
|
capabilities: currentCaps,
|
|
607
797
|
profileReceivedAtMs,
|
|
608
798
|
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: "" },
|
|
799
|
+
record: { credential_id: testConfig.credentialId, label: "", platform: "unknown", paired_at: "", last_seen_at: "" },
|
|
613
800
|
};
|
|
614
801
|
const status = formatDeviceStatus(state);
|
|
615
802
|
const topLevel = Object.keys(status).filter((k) => k !== "raw" && k !== "capabilities");
|
|
@@ -618,12 +805,12 @@ switch (command) {
|
|
|
618
805
|
const capReadySummary = ["screen_sharing", "hid", "live_session"]
|
|
619
806
|
.map((k) => `${k}=${caps[k]?.ready ? "ready" : "not-ready"}`)
|
|
620
807
|
.join(", ");
|
|
621
|
-
console.log(
|
|
808
|
+
console.log(`[PASS] ${topLevel.length} curated + ${rawKeys.length} raw attributes; ${capReadySummary}`);
|
|
622
809
|
console.log(` curated: ${topLevel.join(", ")}`);
|
|
623
810
|
console.log(` raw: ${rawKeys.join(", ")}`);
|
|
624
811
|
passed++;
|
|
625
812
|
} catch (err) {
|
|
626
|
-
console.log(
|
|
813
|
+
console.log(`[FAIL] ${err.message}`);
|
|
627
814
|
failed++;
|
|
628
815
|
}
|
|
629
816
|
break;
|
|
@@ -637,7 +824,7 @@ switch (command) {
|
|
|
637
824
|
const queued = await enqueueCommand(testConfig, cmd);
|
|
638
825
|
const ack = await waitForCommandAck(testConfig, { commandId: queued.id, timeoutMs: 10_000 });
|
|
639
826
|
if (!ack.acked) {
|
|
640
|
-
console.log(
|
|
827
|
+
console.log(`[TIMEOUT] (${Date.now() - t0}ms)`);
|
|
641
828
|
failed++;
|
|
642
829
|
break;
|
|
643
830
|
}
|
|
@@ -646,14 +833,14 @@ switch (command) {
|
|
|
646
833
|
const ms = Date.now() - t0;
|
|
647
834
|
if (shot.stale) {
|
|
648
835
|
const threshold = getSnapshotStaleThresholdMs();
|
|
649
|
-
console.log(
|
|
836
|
+
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
837
|
failed++;
|
|
651
838
|
} else {
|
|
652
|
-
console.log(
|
|
839
|
+
console.log(`[PASS] ${kb}KB, ${shot.width}x${shot.height}, age=${shot.ageMs >= 0 ? `${shot.ageMs}ms` : "?"}, seq=${shot.sequence} (${ms}ms)`);
|
|
653
840
|
passed++;
|
|
654
841
|
}
|
|
655
842
|
} catch (err) {
|
|
656
|
-
console.log(
|
|
843
|
+
console.log(`[FAIL] ${err.message} (${Date.now() - t0}ms)`);
|
|
657
844
|
failed++;
|
|
658
845
|
}
|
|
659
846
|
break;
|
|
@@ -686,14 +873,14 @@ switch (command) {
|
|
|
686
873
|
if (selectedIds) {
|
|
687
874
|
const foundIds = new Set(toRun.map((t) => t.id));
|
|
688
875
|
const missing = [...selectedIds].filter((id) => !foundIds.has(id));
|
|
689
|
-
if (missing.length) console.warn(
|
|
876
|
+
if (missing.length) console.warn(`[!] Unknown test IDs: ${missing.join(", ")}`);
|
|
690
877
|
}
|
|
691
878
|
|
|
692
879
|
let currentPhase = "";
|
|
693
880
|
for (let i = 0; i < toRun.length; i++) {
|
|
694
881
|
const t = toRun[i];
|
|
695
882
|
if (t.phase !== currentPhase) {
|
|
696
|
-
console.log(`
|
|
883
|
+
console.log(` -- ${t.phase} --`);
|
|
697
884
|
currentPhase = t.phase;
|
|
698
885
|
}
|
|
699
886
|
await runSingleTest(t);
|
|
@@ -701,8 +888,8 @@ switch (command) {
|
|
|
701
888
|
}
|
|
702
889
|
|
|
703
890
|
console.log(`\n Result: ${passed}/${totalSteps} passed, ${failed} failed, ${skipped} skipped`);
|
|
704
|
-
if (failed === 0) console.log("
|
|
705
|
-
else console.log(`
|
|
891
|
+
if (failed === 0) console.log(" All tests passed! Device is fully responsive.");
|
|
892
|
+
else console.log(` ${failed} test(s) failed. Check phone connectivity.`);
|
|
706
893
|
process.exit(failed > 0 ? 1 : 0);
|
|
707
894
|
}
|
|
708
895
|
|