@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 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
- listDeviceRecords,
11
- removeDevice,
12
- renameDevice,
13
- setDefaultDevice,
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 { executePairing } from "../dist/core/pair.js";
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.30.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 setup Interactive setup: pair + configure + start
69
- zhihand pair [--label X] Pair with a phone device
70
- zhihand list List paired devices
71
- zhihand unpair <cred_id> Revoke + remove a device
72
- zhihand rename <cred> <n> Rename a device
73
- zhihand default <cred_id> Set default device
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 edgeId = `mcp-${Date.now().toString(36)}`;
217
+ const arg1 = positionals[1];
206
218
  const label = values.label ?? undefined;
207
- await executePairing(DEFAULT_ENDPOINT, edgeId, label);
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 records = listDeviceRecords();
213
- const cfg = loadConfig();
214
- if (records.length === 0) {
215
- console.log("No paired devices. Run: zhihand pair");
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
- const header = ["credential_id", "label", "platform", "default", "paired_at", "last_seen"];
219
- const rows = records.map((r) => [
220
- r.credential_id,
221
- r.label,
222
- r.platform,
223
- cfg.default_credential_id === r.credential_id ? "*" : "",
224
- r.paired_at,
225
- r.last_seen_at,
226
- ]);
227
- const widths = header.map((h, i) => Math.max(h.length, ...rows.map((row) => row[i].length)));
228
- const fmt = (row) => row.map((c, i) => c.padEnd(widths[i])).join(" ");
229
- console.log(fmt(header));
230
- console.log(widths.map((w) => "-".repeat(w)).join(" "));
231
- for (const r of rows) console.log(fmt(r));
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
- if (!credId) {
238
- console.error("Usage: zhihand unpair <credential_id>");
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 cfg = loadConfig();
242
- const record = cfg.devices[credId];
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
- // Best-effort revoke
370
+ // Server-side PATCH
371
+ const endpoint = resolveDefaultEndpoint();
248
372
  try {
249
- const url = `${record.endpoint}/v1/credentials/${encodeURIComponent(credId)}/revoke`;
250
- const res = await fetch(url, {
251
- method: "POST",
252
- headers: { "x-zhihand-controller-token": record.controller_token },
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 revoke returned ${res.status} (continuing to remove locally)`);
383
+ console.warn(`Warning: server rename returned ${res.status}`);
257
384
  }
258
385
  } catch (err) {
259
- console.warn(`Warning: server revoke failed: ${err.message} (continuing to remove locally)`);
386
+ console.warn(`Warning: server rename failed: ${err.message}`);
260
387
  }
261
- removeDevice(credId);
262
- console.log(`Unpaired: ${credId}`);
388
+ updateDeviceLabel(owner.user.user_id, credId, newLabel);
389
+ console.log(`Renamed ${credId} to '${newLabel}'`);
263
390
  break;
264
391
  }
265
392
 
266
- case "rename": {
267
- const credId = positionals[1];
268
- const newLabel = positionals[2];
269
- if (!credId || !newLabel) {
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
- renameDevice(credId, newLabel);
274
- console.log(`Renamed ${credId} to '${newLabel}'`);
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 "default": {
279
- const credId = positionals[1];
280
- if (!credId) {
281
- console.error("Usage: zhihand default <credential_id>");
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 records = listDeviceRecords();
291
- const cfg = loadConfig();
507
+ const users = listUsers();
292
508
  const backend = loadBackendConfig();
293
509
  const daemonPid = isAlreadyRunning();
294
510
 
295
- if (records.length === 0) {
296
- console.log("No paired devices. Run: zhihand setup");
511
+ if (users.length === 0) {
512
+ console.log("No users configured. Run: zhihand pair");
297
513
  } else {
298
- console.log(`Paired devices: ${records.length}`);
299
- for (const r of records) {
300
- const mark = cfg.default_credential_id === r.credential_id ? " *" : "";
301
- console.log(` ${r.credential_id} (${r.label}, ${r.platform})${mark}`);
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
- const records = listDeviceRecords();
335
- if (records.length === 0) {
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/sse.js");
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 (⚠️ may lock screen)", kind: "system", params: { action: "power" }, unsafe: true },
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("🧪 ZhiHand Device Test");
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(" ── Capability readiness ──");
675
+ console.log(" -- Capability readiness --");
488
676
  if (!currentCaps) {
489
- console.log(" ⚠️ Device profile not loaded — all capability gates will allow tests through.");
677
+ console.log(" [!] Device profile not loaded — all capability gates will allow tests through.");
490
678
  } else {
491
- const fmt = (name, cap) => ` ${cap.ready ? "" : "⚠️"} ${name.padEnd(14)} ${cap.ready ? "ready" : "NOT ready"} — ${cap.reason}`;
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 ? "⚠️" : ""} profile age=${ageStr}${currentCaps.profile.stale ? " (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(`✅ (${ms}ms)${resultInfo}`);
722
+ console.log(`[PASS] (${ms}ms)${resultInfo}`);
535
723
  passed++;
536
724
  } else {
537
- console.log(`❌ [${ackStatus}] (${ms}ms)${resultInfo}`);
725
+ console.log(`[FAIL] [${ackStatus}] (${ms}ms)${resultInfo}`);
538
726
  failed++;
539
727
  }
540
728
  } else {
541
- console.log(`⏱️ Timeout (${ms}ms)`);
729
+ console.log(`[TIMEOUT] (${ms}ms)`);
542
730
  failed++;
543
731
  }
544
732
  } catch (err) {
545
- console.log(`❌ ${err.message} (${Date.now() - t0}ms)`);
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}... ⏭️ Skipped (${t.platform}-only, device is ${currentPlatform})`);
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}... ⏭️ Skipped (${requiredCap} not ready: ${gate.reason})`);
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(`✅ ${s.platform} ${s.model}, ${s.osVersion}, ${s.screenWidthPx}x${s.screenHeightPx} (${ms}ms)`);
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(`⚠️ Loaded but empty (${ms}ms)`);
774
+ console.log(`[!] Loaded but empty (${ms}ms)`);
587
775
  failed++;
588
776
  }
589
777
  } catch (err) {
590
- console.log(`❌ ${err.message} (${Date.now() - t0}ms)`);
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
- sseController: null,
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(`✅ ${topLevel.length} curated + ${rawKeys.length} raw attributes; ${capReadySummary}`);
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(`❌ ${err.message}`);
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(`⏱️ Timeout (${Date.now() - t0}ms)`);
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(`❌ 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)`);
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(`✅ ${kb}KB, ${shot.width}x${shot.height}, age=${shot.ageMs >= 0 ? `${shot.ageMs}ms` : "?"}, seq=${shot.sequence} (${ms}ms)`);
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(`❌ ${err.message} (${Date.now() - t0}ms)`);
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(`⚠️ Unknown test IDs: ${missing.join(", ")}`);
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(` ── ${t.phase} ──`);
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(" All tests passed! Device is fully responsive.");
705
- else console.log(` ⚠️ ${failed} test(s) failed. Check phone connectivity.`);
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