@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 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.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 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
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 edgeId = `mcp-${Date.now().toString(36)}`;
218
+ const arg1 = positionals[1];
206
219
  const label = values.label ?? undefined;
207
- await executePairing(DEFAULT_ENDPOINT, edgeId, label);
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 records = listDeviceRecords();
213
- const cfg = loadConfig();
214
- if (records.length === 0) {
215
- console.log("No paired devices. Run: zhihand pair");
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
- 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));
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
- if (!credId) {
238
- console.error("Usage: zhihand unpair <credential_id>");
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 cfg = loadConfig();
242
- const record = cfg.devices[credId];
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
- // Best-effort revoke
371
+ // Server-side PATCH
372
+ const endpoint = resolveDefaultEndpoint();
248
373
  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 },
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 revoke returned ${res.status} (continuing to remove locally)`);
384
+ console.warn(`Warning: server rename returned ${res.status}`);
257
385
  }
258
386
  } catch (err) {
259
- console.warn(`Warning: server revoke failed: ${err.message} (continuing to remove locally)`);
387
+ console.warn(`Warning: server rename failed: ${err.message}`);
260
388
  }
261
- removeDevice(credId);
262
- console.log(`Unpaired: ${credId}`);
389
+ updateDeviceLabel(owner.user.user_id, credId, newLabel);
390
+ console.log(`Renamed ${credId} to '${newLabel}'`);
263
391
  break;
264
392
  }
265
393
 
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>");
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
- renameDevice(credId, newLabel);
274
- console.log(`Renamed ${credId} to '${newLabel}'`);
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 "default": {
279
- const credId = positionals[1];
280
- if (!credId) {
281
- console.error("Usage: zhihand default <credential_id>");
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 records = listDeviceRecords();
291
- const cfg = loadConfig();
508
+ const users = listUsers();
292
509
  const backend = loadBackendConfig();
293
510
  const daemonPid = isAlreadyRunning();
294
511
 
295
- if (records.length === 0) {
296
- console.log("No paired devices. Run: zhihand setup");
512
+ if (users.length === 0) {
513
+ console.log("No users configured. Run: zhihand setup");
297
514
  } 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}`);
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 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`);
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 (⚠️ may lock screen)", kind: "system", params: { action: "power" }, unsafe: true },
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("🧪 ZhiHand Device Test");
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(" ── Capability readiness ──");
700
+ console.log(" -- Capability readiness --");
488
701
  if (!currentCaps) {
489
- console.log(" ⚠️ Device profile not loaded — all capability gates will allow tests through.");
702
+ console.log(" [!] Device profile not loaded — all capability gates will allow tests through.");
490
703
  } else {
491
- const fmt = (name, cap) => ` ${cap.ready ? "" : "⚠️"} ${name.padEnd(14)} ${cap.ready ? "ready" : "NOT ready"} — ${cap.reason}`;
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 ? "⚠️" : ""} profile age=${ageStr}${currentCaps.profile.stale ? " (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(`✅ (${ms}ms)${resultInfo}`);
747
+ console.log(`[PASS] (${ms}ms)${resultInfo}`);
535
748
  passed++;
536
749
  } else {
537
- console.log(`❌ [${ackStatus}] (${ms}ms)${resultInfo}`);
750
+ console.log(`[FAIL] [${ackStatus}] (${ms}ms)${resultInfo}`);
538
751
  failed++;
539
752
  }
540
753
  } else {
541
- console.log(`⏱️ Timeout (${ms}ms)`);
754
+ console.log(`[TIMEOUT] (${ms}ms)`);
542
755
  failed++;
543
756
  }
544
757
  } catch (err) {
545
- console.log(`❌ ${err.message} (${Date.now() - t0}ms)`);
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}... ⏭️ Skipped (${t.platform}-only, device is ${currentPlatform})`);
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}... ⏭️ Skipped (${requiredCap} not ready: ${gate.reason})`);
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(`✅ ${s.platform} ${s.model}, ${s.osVersion}, ${s.screenWidthPx}x${s.screenHeightPx} (${ms}ms)`);
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(`⚠️ Loaded but empty (${ms}ms)`);
799
+ console.log(`[!] Loaded but empty (${ms}ms)`);
587
800
  failed++;
588
801
  }
589
802
  } catch (err) {
590
- console.log(`❌ ${err.message} (${Date.now() - t0}ms)`);
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
- 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: "" },
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(`✅ ${topLevel.length} curated + ${rawKeys.length} raw attributes; ${capReadySummary}`);
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(`❌ ${err.message}`);
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(`⏱️ Timeout (${Date.now() - t0}ms)`);
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(`❌ 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)`);
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(`✅ ${kb}KB, ${shot.width}x${shot.height}, age=${shot.ageMs >= 0 ? `${shot.ageMs}ms` : "?"}, seq=${shot.sequence} (${ms}ms)`);
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(`❌ ${err.message} (${Date.now() - t0}ms)`);
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(`⚠️ Unknown test IDs: ${missing.join(", ")}`);
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(` ── ${t.phase} ──`);
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(" All tests passed! Device is fully responsive.");
705
- else console.log(` ⚠️ ${failed} test(s) failed. Check phone connectivity.`);
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
 
@@ -1,4 +1,5 @@
1
- import { dbg } from "../daemon/logger.js";
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
- "x-zhihand-controller-token": config.controllerToken,
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: { "x-zhihand-controller-token": config.controllerToken },
177
+ headers: { "Authorization": `Bearer ${config.controllerToken}` },
177
178
  });
178
179
  if (!response.ok) {
179
180
  dbg(`[cmd] Get failed: ${response.status}`);