@zhihand/mcp 0.32.4 → 0.33.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 +63 -37
- package/dist/core/command.d.ts +0 -1
- package/dist/core/command.js +0 -15
- package/dist/core/ws.d.ts +2 -2
- package/dist/core/ws.js +2 -35
- package/dist/daemon/index.js +35 -0
- package/dist/daemon/prompt-listener.d.ts +0 -6
- package/dist/daemon/prompt-listener.js +1 -60
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/bin/zhihand
CHANGED
|
@@ -30,7 +30,7 @@ import { fetchUserCredentials } from "../dist/core/ws.js";
|
|
|
30
30
|
import { configureMCP, displayName } from "../dist/cli/mcp-config.js";
|
|
31
31
|
|
|
32
32
|
const DEFAULT_ENDPOINT = "https://api.zhihand.com";
|
|
33
|
-
const VERSION = "0.
|
|
33
|
+
const VERSION = "0.33.0";
|
|
34
34
|
|
|
35
35
|
const CLI_TOOL_MAP = {
|
|
36
36
|
claude: "claudecode",
|
|
@@ -555,8 +555,7 @@ switch (command) {
|
|
|
555
555
|
}
|
|
556
556
|
|
|
557
557
|
case "test": {
|
|
558
|
-
const { createControlCommand, createSystemCommand
|
|
559
|
-
const { waitForCommandAck } = await import("../dist/core/ws.js");
|
|
558
|
+
const { createControlCommand, createSystemCommand } = await import("../dist/core/command.js");
|
|
560
559
|
const { fetchScreenshot, getSnapshotStaleThresholdMs } = await import("../dist/core/screenshot.js");
|
|
561
560
|
const { fetchDeviceProfileOnce, extractStatic, computeCapabilities, formatDeviceStatus } = await import("../dist/core/device.js");
|
|
562
561
|
|
|
@@ -630,6 +629,34 @@ switch (command) {
|
|
|
630
629
|
process.exit(1);
|
|
631
630
|
}
|
|
632
631
|
|
|
632
|
+
// Require daemon to be running (provides WS connections for command ACK)
|
|
633
|
+
const DAEMON_PORT = parseInt(process.env.ZHIHAND_PORT ?? "", 10) || 18686;
|
|
634
|
+
const DAEMON_BASE = `http://127.0.0.1:${DAEMON_PORT}`;
|
|
635
|
+
let daemonOk = false;
|
|
636
|
+
try {
|
|
637
|
+
const resp = await fetch(`${DAEMON_BASE}/internal/status`, { signal: AbortSignal.timeout(2000) });
|
|
638
|
+
daemonOk = resp.ok;
|
|
639
|
+
} catch { /* daemon not reachable */ }
|
|
640
|
+
if (!daemonOk) {
|
|
641
|
+
console.error("❌ Daemon is not running. Start it first: zhihand start");
|
|
642
|
+
process.exit(1);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Execute command via daemon's /internal/exec endpoint
|
|
646
|
+
async function execViaDaemon(command, timeoutMs = 10_000) {
|
|
647
|
+
const resp = await fetch(`${DAEMON_BASE}/internal/exec`, {
|
|
648
|
+
method: "POST",
|
|
649
|
+
headers: { "Content-Type": "application/json" },
|
|
650
|
+
body: JSON.stringify({ command, credentialId: testConfig.credentialId, timeoutMs }),
|
|
651
|
+
signal: AbortSignal.timeout(timeoutMs + 5000),
|
|
652
|
+
});
|
|
653
|
+
if (!resp.ok) {
|
|
654
|
+
const body = await resp.text();
|
|
655
|
+
throw new Error(`Daemon exec failed: ${resp.status} ${body}`);
|
|
656
|
+
}
|
|
657
|
+
return resp.json();
|
|
658
|
+
}
|
|
659
|
+
|
|
633
660
|
const forceRun = values.force === true;
|
|
634
661
|
let selectedIds = null;
|
|
635
662
|
let includeUnsafe = false;
|
|
@@ -652,7 +679,7 @@ switch (command) {
|
|
|
652
679
|
}
|
|
653
680
|
}
|
|
654
681
|
|
|
655
|
-
console.log("ZhiHand Device Test");
|
|
682
|
+
console.log("🔧 ZhiHand Device Test");
|
|
656
683
|
console.log(` Device: ${testConfig.credentialId}`);
|
|
657
684
|
console.log(` Endpoint: ${testConfig.controlPlaneEndpoint}\n`);
|
|
658
685
|
|
|
@@ -672,18 +699,18 @@ switch (command) {
|
|
|
672
699
|
} catch { /* non-fatal */ }
|
|
673
700
|
const getDevicePlatform = () => currentProfile?.platform ?? "unknown";
|
|
674
701
|
|
|
675
|
-
console.log("
|
|
702
|
+
console.log(" ── Capability readiness ──");
|
|
676
703
|
if (!currentCaps) {
|
|
677
|
-
console.log("
|
|
704
|
+
console.log(" ⚠️ Device profile not loaded — all capability gates will allow tests through.");
|
|
678
705
|
} else {
|
|
679
|
-
const fmt = (name, cap) => ` ${cap.ready ? "
|
|
706
|
+
const fmt = (name, cap) => ` ${cap.ready ? "✅" : "⚠️"} ${name.padEnd(16)} ${cap.ready ? "ready" : "NOT ready"} — ${cap.reason}`;
|
|
680
707
|
console.log(fmt("screen_sharing", currentCaps.screen_sharing));
|
|
681
708
|
console.log(fmt("hid", currentCaps.hid));
|
|
682
709
|
console.log(fmt("live_session", currentCaps.live_session));
|
|
683
710
|
const ageStr = currentCaps.profile.age_ms >= 0 ? `${(currentCaps.profile.age_ms / 1000).toFixed(1)}s` : "unknown";
|
|
684
|
-
console.log(` ${currentCaps.profile.stale ? "
|
|
711
|
+
console.log(` ${currentCaps.profile.stale ? "⚠️" : "✅"} profile age=${ageStr}${currentCaps.profile.stale ? " (STALE)" : ""}`);
|
|
685
712
|
if (forceRun) {
|
|
686
|
-
console.log(" --force passed: capability gates disabled.");
|
|
713
|
+
console.log(" ⚡ --force passed: capability gates disabled.");
|
|
687
714
|
}
|
|
688
715
|
}
|
|
689
716
|
console.log("");
|
|
@@ -712,25 +739,24 @@ switch (command) {
|
|
|
712
739
|
process.stdout.write(` ${String(t.id).padStart(2)}. ${t.label}... `);
|
|
713
740
|
const t0 = Date.now();
|
|
714
741
|
try {
|
|
715
|
-
const
|
|
716
|
-
const ack = await waitForCommandAck(testConfig, { commandId: queued.id, timeoutMs: 10_000 });
|
|
742
|
+
const result = await execViaDaemon(command);
|
|
717
743
|
const ms = Date.now() - t0;
|
|
718
|
-
if (
|
|
719
|
-
const ackStatus =
|
|
720
|
-
const resultInfo =
|
|
744
|
+
if (result.acked) {
|
|
745
|
+
const ackStatus = result.command?.ack_status ?? "ok";
|
|
746
|
+
const resultInfo = result.command?.ack_result ? ` ${JSON.stringify(result.command.ack_result)}` : "";
|
|
721
747
|
if (ackStatus === "ok") {
|
|
722
|
-
console.log(
|
|
748
|
+
console.log(`✅ (${ms}ms)${resultInfo}`);
|
|
723
749
|
passed++;
|
|
724
750
|
} else {
|
|
725
|
-
console.log(
|
|
751
|
+
console.log(`❌ [${ackStatus}] (${ms}ms)${resultInfo}`);
|
|
726
752
|
failed++;
|
|
727
753
|
}
|
|
728
754
|
} else {
|
|
729
|
-
console.log(
|
|
755
|
+
console.log(`⏱️ timeout (${ms}ms)`);
|
|
730
756
|
failed++;
|
|
731
757
|
}
|
|
732
758
|
} catch (err) {
|
|
733
|
-
console.log(
|
|
759
|
+
console.log(`❌ ${err.message} (${Date.now() - t0}ms)`);
|
|
734
760
|
failed++;
|
|
735
761
|
}
|
|
736
762
|
}
|
|
@@ -739,7 +765,7 @@ switch (command) {
|
|
|
739
765
|
const currentPlatform = getDevicePlatform();
|
|
740
766
|
if (t.platform && t.platform !== currentPlatform) {
|
|
741
767
|
totalSteps++; skipped++;
|
|
742
|
-
console.log(` ${String(t.id).padStart(2)}. ${t.label}...
|
|
768
|
+
console.log(` ${String(t.id).padStart(2)}. ${t.label}... ⏭️ (${t.platform}-only, device is ${currentPlatform})`);
|
|
743
769
|
return;
|
|
744
770
|
}
|
|
745
771
|
if (!forceRun && currentCaps) {
|
|
@@ -748,7 +774,7 @@ switch (command) {
|
|
|
748
774
|
const gate = requiredCap === "screen" ? currentCaps.screen_sharing : currentCaps.hid;
|
|
749
775
|
if (!gate.ready) {
|
|
750
776
|
totalSteps++; skipped++;
|
|
751
|
-
console.log(` ${String(t.id).padStart(2)}. ${t.label}...
|
|
777
|
+
console.log(` ${String(t.id).padStart(2)}. ${t.label}... ⏭️ (${requiredCap} not ready: ${gate.reason})`);
|
|
752
778
|
return;
|
|
753
779
|
}
|
|
754
780
|
}
|
|
@@ -768,14 +794,14 @@ switch (command) {
|
|
|
768
794
|
currentProfile = extractStatic(currentRawAttrs);
|
|
769
795
|
currentCaps = computeCapabilities(currentRawAttrs, profileReceivedAtMs);
|
|
770
796
|
const s = currentProfile;
|
|
771
|
-
console.log(
|
|
797
|
+
console.log(`✅ ${s.platform} ${s.model}, ${s.osVersion}, ${s.screenWidthPx}x${s.screenHeightPx} (${ms}ms)`);
|
|
772
798
|
passed++;
|
|
773
799
|
} else {
|
|
774
|
-
console.log(
|
|
800
|
+
console.log(`⚠️ loaded but empty (${ms}ms)`);
|
|
775
801
|
failed++;
|
|
776
802
|
}
|
|
777
803
|
} catch (err) {
|
|
778
|
-
console.log(
|
|
804
|
+
console.log(`❌ ${err.message} (${Date.now() - t0}ms)`);
|
|
779
805
|
failed++;
|
|
780
806
|
}
|
|
781
807
|
break;
|
|
@@ -805,12 +831,12 @@ switch (command) {
|
|
|
805
831
|
const capReadySummary = ["screen_sharing", "hid", "live_session"]
|
|
806
832
|
.map((k) => `${k}=${caps[k]?.ready ? "ready" : "not-ready"}`)
|
|
807
833
|
.join(", ");
|
|
808
|
-
console.log(
|
|
834
|
+
console.log(`✅ ${topLevel.length} curated + ${rawKeys.length} raw attributes; ${capReadySummary}`);
|
|
809
835
|
console.log(` curated: ${topLevel.join(", ")}`);
|
|
810
836
|
console.log(` raw: ${rawKeys.join(", ")}`);
|
|
811
837
|
passed++;
|
|
812
838
|
} catch (err) {
|
|
813
|
-
console.log(
|
|
839
|
+
console.log(`❌ ${err.message}`);
|
|
814
840
|
failed++;
|
|
815
841
|
}
|
|
816
842
|
break;
|
|
@@ -821,10 +847,9 @@ switch (command) {
|
|
|
821
847
|
const t0 = Date.now();
|
|
822
848
|
try {
|
|
823
849
|
const cmd = createControlCommand({ action: "screenshot" });
|
|
824
|
-
const
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
console.log(`[TIMEOUT] (${Date.now() - t0}ms)`);
|
|
850
|
+
const result = await execViaDaemon(cmd);
|
|
851
|
+
if (!result.acked) {
|
|
852
|
+
console.log(`⏱️ timeout (${Date.now() - t0}ms)`);
|
|
828
853
|
failed++;
|
|
829
854
|
break;
|
|
830
855
|
}
|
|
@@ -833,14 +858,14 @@ switch (command) {
|
|
|
833
858
|
const ms = Date.now() - t0;
|
|
834
859
|
if (shot.stale) {
|
|
835
860
|
const threshold = getSnapshotStaleThresholdMs();
|
|
836
|
-
console.log(
|
|
861
|
+
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)`);
|
|
837
862
|
failed++;
|
|
838
863
|
} else {
|
|
839
|
-
console.log(
|
|
864
|
+
console.log(`✅ ${kb}KB, ${shot.width}x${shot.height}, age=${shot.ageMs >= 0 ? `${shot.ageMs}ms` : "?"}, seq=${shot.sequence} (${ms}ms)`);
|
|
840
865
|
passed++;
|
|
841
866
|
}
|
|
842
867
|
} catch (err) {
|
|
843
|
-
console.log(
|
|
868
|
+
console.log(`❌ ${err.message} (${Date.now() - t0}ms)`);
|
|
844
869
|
failed++;
|
|
845
870
|
}
|
|
846
871
|
break;
|
|
@@ -873,23 +898,24 @@ switch (command) {
|
|
|
873
898
|
if (selectedIds) {
|
|
874
899
|
const foundIds = new Set(toRun.map((t) => t.id));
|
|
875
900
|
const missing = [...selectedIds].filter((id) => !foundIds.has(id));
|
|
876
|
-
if (missing.length) console.warn(`
|
|
901
|
+
if (missing.length) console.warn(` ⚠️ Unknown test IDs: ${missing.join(", ")}`);
|
|
877
902
|
}
|
|
878
903
|
|
|
879
904
|
let currentPhase = "";
|
|
880
905
|
for (let i = 0; i < toRun.length; i++) {
|
|
881
906
|
const t = toRun[i];
|
|
882
907
|
if (t.phase !== currentPhase) {
|
|
883
|
-
console.log(
|
|
908
|
+
console.log(`\n ── ${t.phase} ──`);
|
|
884
909
|
currentPhase = t.phase;
|
|
885
910
|
}
|
|
886
911
|
await runSingleTest(t);
|
|
887
912
|
if (i < toRun.length - 1) await pause();
|
|
888
913
|
}
|
|
889
914
|
|
|
890
|
-
console.log(`\n Result
|
|
891
|
-
|
|
892
|
-
|
|
915
|
+
console.log(`\n ── Result ──`);
|
|
916
|
+
console.log(` ${passed}/${totalSteps} passed, ${failed} failed, ${skipped} skipped`);
|
|
917
|
+
if (failed === 0) console.log(" 🎉 All tests passed! Device is fully responsive.");
|
|
918
|
+
else console.log(` ⚠️ ${failed} test(s) failed. Check phone connectivity.`);
|
|
893
919
|
process.exit(failed > 0 ? 1 : 0);
|
|
894
920
|
}
|
|
895
921
|
|
package/dist/core/command.d.ts
CHANGED
|
@@ -43,5 +43,4 @@ export interface SystemParams {
|
|
|
43
43
|
}
|
|
44
44
|
export declare function createSystemCommand(params: SystemParams, platform?: string): QueuedControlCommand;
|
|
45
45
|
export declare function enqueueCommand(config: ZhiHandRuntimeConfig, command: QueuedControlCommand): Promise<QueuedCommandRecord>;
|
|
46
|
-
export declare function getCommand(config: ZhiHandRuntimeConfig, commandId: string): Promise<QueuedCommandRecord>;
|
|
47
46
|
export declare function formatAckSummary(action: string, result: WaitForCommandAckResult): string;
|
package/dist/core/command.js
CHANGED
|
@@ -170,21 +170,6 @@ export async function enqueueCommand(config, command) {
|
|
|
170
170
|
dbg(`[cmd] Enqueued: id=${payload.command.id}, status=${payload.command.status}`);
|
|
171
171
|
return payload.command;
|
|
172
172
|
}
|
|
173
|
-
export async function getCommand(config, commandId) {
|
|
174
|
-
const url = `${config.controlPlaneEndpoint}/v1/credentials/${encodeURIComponent(config.credentialId)}/commands/${encodeURIComponent(commandId)}`;
|
|
175
|
-
dbg(`[cmd] GET ${url}`);
|
|
176
|
-
const response = await fetch(url, {
|
|
177
|
-
headers: { "Authorization": `Bearer ${config.controllerToken}` },
|
|
178
|
-
});
|
|
179
|
-
if (!response.ok) {
|
|
180
|
-
dbg(`[cmd] Get failed: ${response.status}`);
|
|
181
|
-
throw new Error(`Get command failed: ${response.status}`);
|
|
182
|
-
}
|
|
183
|
-
const payload = (await response.json());
|
|
184
|
-
const cmd = payload.command;
|
|
185
|
-
dbg(`[cmd] Got: id=${cmd.id}, status=${cmd.status}, acked=${!!cmd.acked_at}, ack_status=${cmd.ack_status ?? "-"}, ack_result=${JSON.stringify(cmd.ack_result ?? null)}`);
|
|
186
|
-
return payload.command;
|
|
187
|
-
}
|
|
188
173
|
export function formatAckSummary(action, result) {
|
|
189
174
|
if (!result.acked) {
|
|
190
175
|
return `Sent ${action}, waiting for ACK (timed out).`;
|
package/dist/core/ws.d.ts
CHANGED
|
@@ -85,9 +85,9 @@ export interface CredentialResponse {
|
|
|
85
85
|
export declare function fetchUserCredentials(endpoint: string, userId: string, controllerToken: string, onlineFilter?: boolean): Promise<CredentialResponse[]>;
|
|
86
86
|
/**
|
|
87
87
|
* Wait for command ACK via WS push (which should already be connected by the
|
|
88
|
-
* registry).
|
|
88
|
+
* registry). WS-only — no polling fallback.
|
|
89
89
|
*/
|
|
90
|
-
export declare function waitForCommandAck(
|
|
90
|
+
export declare function waitForCommandAck(_config: ZhiHandRuntimeConfig, options: {
|
|
91
91
|
commandId: string;
|
|
92
92
|
timeoutMs?: number;
|
|
93
93
|
signal?: AbortSignal;
|
package/dist/core/ws.js
CHANGED
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
* - fetchUserCredentials: HTTP REST helper (unchanged from sse.ts).
|
|
10
10
|
*/
|
|
11
11
|
import WebSocket from "ws";
|
|
12
|
-
import { getCommand } from "./command.js";
|
|
13
12
|
import { log } from "./logger.js";
|
|
14
13
|
// ── Shared reconnecting base ─────────────────────────────
|
|
15
14
|
const BACKOFF_INITIAL_MS = 1000;
|
|
@@ -279,59 +278,27 @@ export async function fetchUserCredentials(endpoint, userId, controllerToken, on
|
|
|
279
278
|
}
|
|
280
279
|
/**
|
|
281
280
|
* Wait for command ACK via WS push (which should already be connected by the
|
|
282
|
-
* registry).
|
|
281
|
+
* registry). WS-only — no polling fallback.
|
|
283
282
|
*/
|
|
284
|
-
export async function waitForCommandAck(
|
|
283
|
+
export async function waitForCommandAck(_config, options) {
|
|
285
284
|
const timeoutMs = options.timeoutMs ?? 15_000;
|
|
286
285
|
log.debug(`[ws-cmd] Waiting for ACK: commandId=${options.commandId}, timeout=${timeoutMs}ms`);
|
|
287
286
|
return new Promise((resolve, reject) => {
|
|
288
|
-
let resolved = false;
|
|
289
|
-
let pollInterval;
|
|
290
287
|
const timeout = setTimeout(() => {
|
|
291
288
|
cleanup();
|
|
292
289
|
resolve({ acked: false });
|
|
293
290
|
}, timeoutMs);
|
|
294
291
|
const unsubscribe = subscribeToCommandAck(options.commandId, (ackedCommand) => {
|
|
295
|
-
if (resolved)
|
|
296
|
-
return;
|
|
297
|
-
resolved = true;
|
|
298
292
|
cleanup();
|
|
299
293
|
resolve({ acked: true, command: ackedCommand });
|
|
300
294
|
});
|
|
301
|
-
// Delay polling startup by 2s so WS push ACK normally wins in the
|
|
302
|
-
// registry-connected path. CLI (zhihand test) still resolves via polling
|
|
303
|
-
// after the initial delay.
|
|
304
|
-
const POLL_START_DELAY_MS = 2000;
|
|
305
|
-
const POLL_INTERVAL_MS = 500;
|
|
306
|
-
const startPolling = setTimeout(() => {
|
|
307
|
-
if (resolved)
|
|
308
|
-
return;
|
|
309
|
-
pollInterval = setInterval(async () => {
|
|
310
|
-
if (resolved)
|
|
311
|
-
return;
|
|
312
|
-
try {
|
|
313
|
-
const cmd = await getCommand(config, options.commandId);
|
|
314
|
-
if (cmd.acked_at) {
|
|
315
|
-
resolved = true;
|
|
316
|
-
cleanup();
|
|
317
|
-
resolve({ acked: true, command: cmd });
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
catch {
|
|
321
|
-
// non-fatal
|
|
322
|
-
}
|
|
323
|
-
}, POLL_INTERVAL_MS);
|
|
324
|
-
}, POLL_START_DELAY_MS);
|
|
325
295
|
options.signal?.addEventListener("abort", () => {
|
|
326
296
|
cleanup();
|
|
327
297
|
reject(new Error("The operation was aborted"));
|
|
328
298
|
}, { once: true });
|
|
329
299
|
function cleanup() {
|
|
330
300
|
clearTimeout(timeout);
|
|
331
|
-
clearTimeout(startPolling);
|
|
332
301
|
unsubscribe();
|
|
333
|
-
if (pollInterval)
|
|
334
|
-
clearInterval(pollInterval);
|
|
335
302
|
}
|
|
336
303
|
});
|
|
337
304
|
}
|
package/dist/daemon/index.js
CHANGED
|
@@ -12,6 +12,8 @@ import { PromptListener } from "./prompt-listener.js";
|
|
|
12
12
|
import { dispatchToCLI, postReply, killActiveChild } from "./dispatcher.js";
|
|
13
13
|
import { setDebugEnabled, dbg } from "./logger.js";
|
|
14
14
|
import { registry } from "../core/registry.js";
|
|
15
|
+
import { enqueueCommand } from "../core/command.js";
|
|
16
|
+
import { waitForCommandAck } from "../core/ws.js";
|
|
15
17
|
const DEFAULT_PORT = 18686;
|
|
16
18
|
const PID_FILE = "daemon.pid";
|
|
17
19
|
// ── State ────────���─────────────────────────────────────────
|
|
@@ -102,6 +104,39 @@ function handleInternalAPI(req, res) {
|
|
|
102
104
|
});
|
|
103
105
|
return true;
|
|
104
106
|
}
|
|
107
|
+
// Execute command via daemon's WS (used by zhihand test)
|
|
108
|
+
if (url === "/internal/exec" && req.method === "POST") {
|
|
109
|
+
dbg(`[api] POST /internal/exec`);
|
|
110
|
+
let body = "";
|
|
111
|
+
const MAX_BODY = 10 * 1024;
|
|
112
|
+
req.on("data", (chunk) => {
|
|
113
|
+
body += chunk.toString();
|
|
114
|
+
if (body.length > MAX_BODY) {
|
|
115
|
+
res.writeHead(413, { "Content-Type": "application/json" });
|
|
116
|
+
res.end(JSON.stringify({ error: "Payload too large" }));
|
|
117
|
+
req.destroy();
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
req.on("end", async () => {
|
|
121
|
+
try {
|
|
122
|
+
const { command, credentialId, timeoutMs } = JSON.parse(body);
|
|
123
|
+
const cfg = resolveConfig(credentialId);
|
|
124
|
+
const effectiveTimeout = timeoutMs ?? 10_000;
|
|
125
|
+
const queued = await enqueueCommand(cfg, command);
|
|
126
|
+
const ack = await waitForCommandAck(cfg, {
|
|
127
|
+
commandId: queued.id,
|
|
128
|
+
timeoutMs: effectiveTimeout,
|
|
129
|
+
});
|
|
130
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
131
|
+
res.end(JSON.stringify({ id: queued.id, ...ack }));
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
135
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
105
140
|
if (url === "/internal/status" && req.method === "GET") {
|
|
106
141
|
dbg(`[api] GET /internal/status`);
|
|
107
142
|
const effectiveModel = activeBackend ? (activeModel ?? DEFAULT_MODELS[activeBackend]) : null;
|
|
@@ -17,8 +17,6 @@ export declare class PromptListener {
|
|
|
17
17
|
private log;
|
|
18
18
|
private processedIds;
|
|
19
19
|
private rws;
|
|
20
|
-
private pollTimer;
|
|
21
|
-
private wsConnected;
|
|
22
20
|
private stopped;
|
|
23
21
|
constructor(config: ZhiHandConfig, handler: PromptHandler, log: (msg: string) => void);
|
|
24
22
|
start(): void;
|
|
@@ -27,9 +25,5 @@ export declare class PromptListener {
|
|
|
27
25
|
private connectWS;
|
|
28
26
|
private handleWSMessage;
|
|
29
27
|
private handleEvent;
|
|
30
|
-
private startPolling;
|
|
31
|
-
private schedulePoll;
|
|
32
|
-
private stopPolling;
|
|
33
|
-
private poll;
|
|
34
28
|
}
|
|
35
29
|
export {};
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import { ReconnectingWebSocket } from "../core/ws.js";
|
|
2
2
|
import { dbg } from "./logger.js";
|
|
3
|
-
const POLL_INTERVAL = 2_000;
|
|
4
3
|
export class PromptListener {
|
|
5
4
|
config;
|
|
6
5
|
handler;
|
|
7
6
|
log;
|
|
8
7
|
processedIds = new Set();
|
|
9
8
|
rws = null;
|
|
10
|
-
pollTimer = null;
|
|
11
|
-
wsConnected = false;
|
|
12
9
|
stopped = false;
|
|
13
10
|
constructor(config, handler, log) {
|
|
14
11
|
this.config = config;
|
|
@@ -23,8 +20,6 @@ export class PromptListener {
|
|
|
23
20
|
this.stopped = true;
|
|
24
21
|
this.rws?.stop();
|
|
25
22
|
this.rws = null;
|
|
26
|
-
this.wsConnected = false;
|
|
27
|
-
this.stopPolling();
|
|
28
23
|
}
|
|
29
24
|
dispatchPrompt(prompt) {
|
|
30
25
|
if (this.processedIds.has(prompt.id)) {
|
|
@@ -60,11 +55,7 @@ export class PromptListener {
|
|
|
60
55
|
// onConnected deferred until auth_ok is received (see handleWSMessage)
|
|
61
56
|
},
|
|
62
57
|
onClose: (_code, _reason) => {
|
|
63
|
-
|
|
64
|
-
this.wsConnected = false;
|
|
65
|
-
this.log("[ws] Disconnected. Falling back to polling.");
|
|
66
|
-
this.startPolling();
|
|
67
|
-
}
|
|
58
|
+
dbg("[ws] Disconnected. ReconnectingWebSocket will retry.");
|
|
68
59
|
},
|
|
69
60
|
onMessage: (data) => {
|
|
70
61
|
this.handleWSMessage(data);
|
|
@@ -79,8 +70,6 @@ export class PromptListener {
|
|
|
79
70
|
const msg = data;
|
|
80
71
|
// Auth responses
|
|
81
72
|
if (msg.type === "auth_ok") {
|
|
82
|
-
this.wsConnected = true;
|
|
83
|
-
this.stopPolling();
|
|
84
73
|
this.log("[ws] Connected to prompt stream.");
|
|
85
74
|
return;
|
|
86
75
|
}
|
|
@@ -88,8 +77,6 @@ export class PromptListener {
|
|
|
88
77
|
this.log(`[ws] Auth failed: ${msg.error}`);
|
|
89
78
|
this.rws?.stop();
|
|
90
79
|
this.rws = null;
|
|
91
|
-
this.wsConnected = false;
|
|
92
|
-
this.startPolling();
|
|
93
80
|
return;
|
|
94
81
|
}
|
|
95
82
|
// Application-level ping (if server sends these alongside protocol pings)
|
|
@@ -119,50 +106,4 @@ export class PromptListener {
|
|
|
119
106
|
this.log("[device] device_profile.updated event received on prompts stream (ignored; registry handles it)");
|
|
120
107
|
}
|
|
121
108
|
}
|
|
122
|
-
startPolling() {
|
|
123
|
-
if (this.pollTimer || this.stopped)
|
|
124
|
-
return;
|
|
125
|
-
this.schedulePoll();
|
|
126
|
-
}
|
|
127
|
-
schedulePoll() {
|
|
128
|
-
if (this.pollTimer)
|
|
129
|
-
return;
|
|
130
|
-
this.pollTimer = setTimeout(async () => {
|
|
131
|
-
this.pollTimer = null;
|
|
132
|
-
await this.poll();
|
|
133
|
-
if (!this.wsConnected && !this.stopped) {
|
|
134
|
-
this.schedulePoll();
|
|
135
|
-
}
|
|
136
|
-
}, POLL_INTERVAL);
|
|
137
|
-
}
|
|
138
|
-
stopPolling() {
|
|
139
|
-
if (this.pollTimer) {
|
|
140
|
-
clearTimeout(this.pollTimer);
|
|
141
|
-
this.pollTimer = null;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
async poll() {
|
|
145
|
-
try {
|
|
146
|
-
const url = `${this.config.controlPlaneEndpoint}/v1/credentials/${encodeURIComponent(this.config.credentialId)}/prompts?limit=5`;
|
|
147
|
-
dbg(`[poll] GET ${url}`);
|
|
148
|
-
const response = await fetch(url, {
|
|
149
|
-
headers: { "Authorization": `Bearer ${this.config.controllerToken}` },
|
|
150
|
-
signal: AbortSignal.timeout(10_000),
|
|
151
|
-
});
|
|
152
|
-
if (!response.ok) {
|
|
153
|
-
dbg(`[poll] Response: ${response.status}`);
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
const data = (await response.json());
|
|
157
|
-
dbg(`[poll] Got ${data.items?.length ?? 0} prompt(s)`);
|
|
158
|
-
if (this.stopped)
|
|
159
|
-
return; // Guard against late responses after stop()
|
|
160
|
-
for (const prompt of data.items ?? []) {
|
|
161
|
-
this.dispatchPrompt(prompt);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
catch (err) {
|
|
165
|
-
dbg(`[poll] Error: ${err.message}`);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
109
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
export declare const PACKAGE_VERSION = "0.
|
|
2
|
+
export declare const PACKAGE_VERSION = "0.33.0";
|
|
3
3
|
export declare function createServer(): McpServer;
|
|
4
4
|
export declare function startStdioServer(): Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { handlePair } from "./tools/pair.js";
|
|
|
8
8
|
import { resolveTargetDevice } from "./tools/resolve.js";
|
|
9
9
|
import { buildControlToolDescription, buildSystemToolDescription, buildScreenshotToolDescription, formatDeviceStatus, extractDynamic, } from "./core/device.js";
|
|
10
10
|
import { registry } from "./core/registry.js";
|
|
11
|
-
export const PACKAGE_VERSION = "0.
|
|
11
|
+
export const PACKAGE_VERSION = "0.33.0";
|
|
12
12
|
function errorResult(message) {
|
|
13
13
|
return { content: [{ type: "text", text: message }], isError: true };
|
|
14
14
|
}
|