@zhihand/mcp 0.32.5 → 0.33.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 +56 -12
- package/dist/core/command.d.ts +0 -1
- package/dist/core/command.js +0 -15
- package/dist/core/logger.d.ts +10 -5
- package/dist/core/logger.js +37 -9
- package/dist/core/ws.d.ts +2 -2
- package/dist/core/ws.js +5 -35
- package/dist/daemon/index.js +46 -2
- package/dist/daemon/logger.d.ts +5 -3
- package/dist/daemon/logger.js +10 -11
- 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.1";
|
|
34
34
|
|
|
35
35
|
const CLI_TOOL_MAP = {
|
|
36
36
|
claude: "claudecode",
|
|
@@ -555,10 +555,12 @@ 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");
|
|
561
|
+
const { setDebugEnabled: setCoreDebug, setTimestampEnabled, log: coreLog } = await import("../dist/core/logger.js");
|
|
562
|
+
if (values.debug) setCoreDebug(true);
|
|
563
|
+
setTimestampEnabled(true);
|
|
562
564
|
|
|
563
565
|
const KIND_CAPABILITY = {
|
|
564
566
|
profile: "none", status: "none", screenshot: "screen", hid: "hid", system: "none",
|
|
@@ -630,6 +632,46 @@ switch (command) {
|
|
|
630
632
|
process.exit(1);
|
|
631
633
|
}
|
|
632
634
|
|
|
635
|
+
// Require daemon to be running (provides WS connections for command ACK)
|
|
636
|
+
const DAEMON_PORT = parseInt(process.env.ZHIHAND_PORT ?? "", 10) || 18686;
|
|
637
|
+
const DAEMON_BASE = `http://127.0.0.1:${DAEMON_PORT}`;
|
|
638
|
+
let daemonOk = false;
|
|
639
|
+
let daemonStatus = null;
|
|
640
|
+
try {
|
|
641
|
+
const resp = await fetch(`${DAEMON_BASE}/internal/status`, { signal: AbortSignal.timeout(2000) });
|
|
642
|
+
daemonOk = resp.ok;
|
|
643
|
+
if (resp.ok) daemonStatus = await resp.json();
|
|
644
|
+
} catch { /* daemon not reachable */ }
|
|
645
|
+
if (!daemonOk) {
|
|
646
|
+
console.error("❌ Daemon is not running. Start it first: zhihand start");
|
|
647
|
+
process.exit(1);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const testDbg = values.debug
|
|
651
|
+
? (msg) => coreLog.debug(`[test] ${msg}`)
|
|
652
|
+
: () => {};
|
|
653
|
+
|
|
654
|
+
// Execute command via daemon's /internal/exec endpoint
|
|
655
|
+
async function execViaDaemon(command, timeoutMs = 10_000) {
|
|
656
|
+
const action = command?.payload?.action ?? command?.type ?? "?";
|
|
657
|
+
testDbg(`exec action=${action} timeout=${timeoutMs}ms`);
|
|
658
|
+
const t0 = Date.now();
|
|
659
|
+
const resp = await fetch(`${DAEMON_BASE}/internal/exec`, {
|
|
660
|
+
method: "POST",
|
|
661
|
+
headers: { "Content-Type": "application/json" },
|
|
662
|
+
body: JSON.stringify({ command, credentialId: testConfig.credentialId, timeoutMs }),
|
|
663
|
+
signal: AbortSignal.timeout(timeoutMs + 5000),
|
|
664
|
+
});
|
|
665
|
+
if (!resp.ok) {
|
|
666
|
+
const body = await resp.text();
|
|
667
|
+
testDbg(`exec FAILED ${resp.status} ${Date.now() - t0}ms`);
|
|
668
|
+
throw new Error(`Daemon exec failed: ${resp.status} ${body}`);
|
|
669
|
+
}
|
|
670
|
+
const result = await resp.json();
|
|
671
|
+
testDbg(`exec done action=${action} acked=${result.acked} id=${result.id ?? "-"} ${Date.now() - t0}ms`);
|
|
672
|
+
return result;
|
|
673
|
+
}
|
|
674
|
+
|
|
633
675
|
const forceRun = values.force === true;
|
|
634
676
|
let selectedIds = null;
|
|
635
677
|
let includeUnsafe = false;
|
|
@@ -654,7 +696,11 @@ switch (command) {
|
|
|
654
696
|
|
|
655
697
|
console.log("🔧 ZhiHand Device Test");
|
|
656
698
|
console.log(` Device: ${testConfig.credentialId}`);
|
|
657
|
-
console.log(` Endpoint: ${testConfig.controlPlaneEndpoint}
|
|
699
|
+
console.log(` Endpoint: ${testConfig.controlPlaneEndpoint}`);
|
|
700
|
+
if (daemonStatus) {
|
|
701
|
+
console.log(` Daemon: v${daemonStatus.version} pid=${daemonStatus.pid} backend=${daemonStatus.backend ?? "none"}`);
|
|
702
|
+
}
|
|
703
|
+
console.log("");
|
|
658
704
|
|
|
659
705
|
// Pre-fetch device profile
|
|
660
706
|
let currentRawAttrs = {};
|
|
@@ -712,12 +758,11 @@ switch (command) {
|
|
|
712
758
|
process.stdout.write(` ${String(t.id).padStart(2)}. ${t.label}... `);
|
|
713
759
|
const t0 = Date.now();
|
|
714
760
|
try {
|
|
715
|
-
const
|
|
716
|
-
const ack = await waitForCommandAck(testConfig, { commandId: queued.id, timeoutMs: 10_000 });
|
|
761
|
+
const result = await execViaDaemon(command);
|
|
717
762
|
const ms = Date.now() - t0;
|
|
718
|
-
if (
|
|
719
|
-
const ackStatus =
|
|
720
|
-
const resultInfo =
|
|
763
|
+
if (result.acked) {
|
|
764
|
+
const ackStatus = result.command?.ack_status ?? "ok";
|
|
765
|
+
const resultInfo = result.command?.ack_result ? ` ${JSON.stringify(result.command.ack_result)}` : "";
|
|
721
766
|
if (ackStatus === "ok") {
|
|
722
767
|
console.log(`✅ (${ms}ms)${resultInfo}`);
|
|
723
768
|
passed++;
|
|
@@ -821,9 +866,8 @@ switch (command) {
|
|
|
821
866
|
const t0 = Date.now();
|
|
822
867
|
try {
|
|
823
868
|
const cmd = createControlCommand({ action: "screenshot" });
|
|
824
|
-
const
|
|
825
|
-
|
|
826
|
-
if (!ack.acked) {
|
|
869
|
+
const result = await execViaDaemon(cmd);
|
|
870
|
+
if (!result.acked) {
|
|
827
871
|
console.log(`⏱️ timeout (${Date.now() - t0}ms)`);
|
|
828
872
|
failed++;
|
|
829
873
|
break;
|
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/logger.d.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Unified logger — all log output goes to stderr so stdout stays clean
|
|
3
|
-
* for MCP JSON-RPC.
|
|
4
|
-
* in core/ and tools/ code.
|
|
3
|
+
* for MCP JSON-RPC.
|
|
5
4
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* (daemon/logger.ts) remains for daemon-specific verbose output.
|
|
5
|
+
* All modules (core/, daemon/, tools/) should use this logger.
|
|
6
|
+
* The daemon's dbg() in daemon/logger.ts delegates here for the debug flag.
|
|
9
7
|
*/
|
|
8
|
+
/**
|
|
9
|
+
* Redact sensitive tokens from log messages.
|
|
10
|
+
* Replaces Bearer tokens and controller_token values with <REDACTED>.
|
|
11
|
+
*/
|
|
12
|
+
export declare function redact(msg: string): string;
|
|
10
13
|
export declare const log: {
|
|
11
14
|
info: (...args: unknown[]) => void;
|
|
12
15
|
warn: (...args: unknown[]) => void;
|
|
@@ -15,3 +18,5 @@ export declare const log: {
|
|
|
15
18
|
};
|
|
16
19
|
export declare function setDebugEnabled(v: boolean): void;
|
|
17
20
|
export declare function isDebugEnabled(): boolean;
|
|
21
|
+
/** Enable timestamps in log output (for daemon / CLI long-running processes). */
|
|
22
|
+
export declare function setTimestampEnabled(v: boolean): void;
|
package/dist/core/logger.js
CHANGED
|
@@ -1,26 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Unified logger — all log output goes to stderr so stdout stays clean
|
|
3
|
-
* for MCP JSON-RPC.
|
|
4
|
-
* in core/ and tools/ code.
|
|
3
|
+
* for MCP JSON-RPC.
|
|
5
4
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* (daemon/logger.ts) remains for daemon-specific verbose output.
|
|
5
|
+
* All modules (core/, daemon/, tools/) should use this logger.
|
|
6
|
+
* The daemon's dbg() in daemon/logger.ts delegates here for the debug flag.
|
|
9
7
|
*/
|
|
10
8
|
let debugEnabled = false;
|
|
9
|
+
let timestampEnabled = false;
|
|
10
|
+
// ── Token redaction ──────────────────────────────────────
|
|
11
|
+
const REDACT_PATTERNS = [
|
|
12
|
+
// Bearer tokens in headers / JSON
|
|
13
|
+
/(Bearer\s+)[^\s"',}]+/gi,
|
|
14
|
+
// controller_token in JSON / key=value
|
|
15
|
+
/(controller_token["']?\s*[:=]\s*["']?)[^\s"',}]+/gi,
|
|
16
|
+
];
|
|
17
|
+
/**
|
|
18
|
+
* Redact sensitive tokens from log messages.
|
|
19
|
+
* Replaces Bearer tokens and controller_token values with <REDACTED>.
|
|
20
|
+
*/
|
|
21
|
+
export function redact(msg) {
|
|
22
|
+
let result = msg;
|
|
23
|
+
for (const pattern of REDACT_PATTERNS) {
|
|
24
|
+
result = result.replace(pattern, "$1<REDACTED>");
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
// ── Logger ───────────────────────────────────────────────
|
|
29
|
+
function prefix(level) {
|
|
30
|
+
if (timestampEnabled) {
|
|
31
|
+
return `[${new Date().toLocaleTimeString()}] [${level}] `;
|
|
32
|
+
}
|
|
33
|
+
return `[${level.padEnd(5)}] `;
|
|
34
|
+
}
|
|
11
35
|
export const log = {
|
|
12
36
|
info: (...args) => {
|
|
13
|
-
process.stderr.write(
|
|
37
|
+
process.stderr.write(`${prefix("info")}${redact(args.map(String).join(" "))}\n`);
|
|
14
38
|
},
|
|
15
39
|
warn: (...args) => {
|
|
16
|
-
process.stderr.write(
|
|
40
|
+
process.stderr.write(`${prefix("warn")}${redact(args.map(String).join(" "))}\n`);
|
|
17
41
|
},
|
|
18
42
|
error: (...args) => {
|
|
19
|
-
process.stderr.write(
|
|
43
|
+
process.stderr.write(`${prefix("error")}${redact(args.map(String).join(" "))}\n`);
|
|
20
44
|
},
|
|
21
45
|
debug: (...args) => {
|
|
22
46
|
if (debugEnabled) {
|
|
23
|
-
process.stderr.write(
|
|
47
|
+
process.stderr.write(`${prefix("debug")}${redact(args.map(String).join(" "))}\n`);
|
|
24
48
|
}
|
|
25
49
|
},
|
|
26
50
|
};
|
|
@@ -30,3 +54,7 @@ export function setDebugEnabled(v) {
|
|
|
30
54
|
export function isDebugEnabled() {
|
|
31
55
|
return debugEnabled;
|
|
32
56
|
}
|
|
57
|
+
/** Enable timestamps in log output (for daemon / CLI long-running processes). */
|
|
58
|
+
export function setTimestampEnabled(v) {
|
|
59
|
+
timestampEnabled = v;
|
|
60
|
+
}
|
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,30 @@ 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`);
|
|
286
|
+
const t0 = Date.now();
|
|
287
287
|
return new Promise((resolve, reject) => {
|
|
288
|
-
let resolved = false;
|
|
289
|
-
let pollInterval;
|
|
290
288
|
const timeout = setTimeout(() => {
|
|
289
|
+
log.debug(`[ws-cmd] ACK timeout: commandId=${options.commandId} after ${Date.now() - t0}ms`);
|
|
291
290
|
cleanup();
|
|
292
291
|
resolve({ acked: false });
|
|
293
292
|
}, timeoutMs);
|
|
294
293
|
const unsubscribe = subscribeToCommandAck(options.commandId, (ackedCommand) => {
|
|
295
|
-
|
|
296
|
-
return;
|
|
297
|
-
resolved = true;
|
|
294
|
+
log.debug(`[ws-cmd] ACK received: commandId=${options.commandId} status=${ackedCommand.ack_status ?? "ok"} ${Date.now() - t0}ms`);
|
|
298
295
|
cleanup();
|
|
299
296
|
resolve({ acked: true, command: ackedCommand });
|
|
300
297
|
});
|
|
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
298
|
options.signal?.addEventListener("abort", () => {
|
|
326
299
|
cleanup();
|
|
327
300
|
reject(new Error("The operation was aborted"));
|
|
328
301
|
}, { once: true });
|
|
329
302
|
function cleanup() {
|
|
330
303
|
clearTimeout(timeout);
|
|
331
|
-
clearTimeout(startPolling);
|
|
332
304
|
unsubscribe();
|
|
333
|
-
if (pollInterval)
|
|
334
|
-
clearInterval(pollInterval);
|
|
335
305
|
}
|
|
336
306
|
});
|
|
337
307
|
}
|
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 ────────���─────────────────────────────────────────
|
|
@@ -19,9 +21,9 @@ let activeBackend = null;
|
|
|
19
21
|
let activeModel = null; // user-selected model alias, null = use default
|
|
20
22
|
let isProcessing = false;
|
|
21
23
|
const promptQueue = [];
|
|
24
|
+
import { log as coreLog, setTimestampEnabled } from "../core/logger.js";
|
|
22
25
|
function log(msg) {
|
|
23
|
-
|
|
24
|
-
process.stdout.write(`[${ts}] ${msg}\n`);
|
|
26
|
+
coreLog.info(msg);
|
|
25
27
|
}
|
|
26
28
|
// ── Prompt Processing ──────────────────────────────────────
|
|
27
29
|
async function processPrompt(config, prompt) {
|
|
@@ -102,6 +104,47 @@ 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
|
+
let body = "";
|
|
110
|
+
const MAX_BODY = 10 * 1024;
|
|
111
|
+
req.on("data", (chunk) => {
|
|
112
|
+
body += chunk.toString();
|
|
113
|
+
if (body.length > MAX_BODY) {
|
|
114
|
+
res.writeHead(413, { "Content-Type": "application/json" });
|
|
115
|
+
res.end(JSON.stringify({ error: "Payload too large" }));
|
|
116
|
+
req.destroy();
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
req.on("end", async () => {
|
|
120
|
+
const t0 = Date.now();
|
|
121
|
+
try {
|
|
122
|
+
const { command, credentialId, timeoutMs } = JSON.parse(body);
|
|
123
|
+
const cmdType = command.type ?? "unknown";
|
|
124
|
+
const cmdAction = command.payload?.action ?? "-";
|
|
125
|
+
dbg(`[api] POST /internal/exec cred=${credentialId} type=${cmdType} action=${cmdAction} timeout=${timeoutMs ?? 10_000}ms`);
|
|
126
|
+
const cfg = resolveConfig(credentialId);
|
|
127
|
+
const effectiveTimeout = timeoutMs ?? 10_000;
|
|
128
|
+
const queued = await enqueueCommand(cfg, command);
|
|
129
|
+
dbg(`[api] /internal/exec enqueued id=${queued.id}`);
|
|
130
|
+
const ack = await waitForCommandAck(cfg, {
|
|
131
|
+
commandId: queued.id,
|
|
132
|
+
timeoutMs: effectiveTimeout,
|
|
133
|
+
});
|
|
134
|
+
const ms = Date.now() - t0;
|
|
135
|
+
const ackStatus = ack.acked ? (ack.command?.ack_status ?? "ok") : "timeout";
|
|
136
|
+
dbg(`[api] /internal/exec done id=${queued.id} acked=${ack.acked} status=${ackStatus} ${ms}ms`);
|
|
137
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
138
|
+
res.end(JSON.stringify({ id: queued.id, ...ack }));
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
dbg(`[api] /internal/exec error: ${err.message} ${Date.now() - t0}ms`);
|
|
142
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
143
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
105
148
|
if (url === "/internal/status" && req.method === "GET") {
|
|
106
149
|
dbg(`[api] GET /internal/status`);
|
|
107
150
|
const effectiveModel = activeBackend ? (activeModel ?? DEFAULT_MODELS[activeBackend]) : null;
|
|
@@ -155,6 +198,7 @@ export function isAlreadyRunning() {
|
|
|
155
198
|
}
|
|
156
199
|
// ── Main Daemon Entry ──────���───────────────────────────────
|
|
157
200
|
export async function startDaemon(options) {
|
|
201
|
+
setTimestampEnabled(true);
|
|
158
202
|
if (options?.debug)
|
|
159
203
|
setDebugEnabled(true);
|
|
160
204
|
const port = options?.port ?? (parseInt(process.env.ZHIHAND_PORT ?? "", 10) || DEFAULT_PORT);
|
package/dist/daemon/logger.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Debug logger for ZhiHand daemon.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Delegates to core/logger.ts for the debug flag, redaction, and output.
|
|
5
|
+
* All output goes to stderr to keep stdout clean for MCP JSON-RPC.
|
|
6
|
+
*
|
|
7
|
+
* Enable with `zhihand start --debug`.
|
|
6
8
|
*/
|
|
7
9
|
export declare function setDebugEnabled(enabled: boolean): void;
|
|
8
10
|
export declare function isDebugEnabled(): boolean;
|
|
9
|
-
/** Debug log — only outputs when --debug is active. */
|
|
11
|
+
/** Debug log — only outputs when --debug is active. Writes to stderr with redaction. */
|
|
10
12
|
export declare function dbg(msg: string): void;
|
package/dist/daemon/logger.js
CHANGED
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Debug logger for ZhiHand daemon.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Delegates to core/logger.ts for the debug flag, redaction, and output.
|
|
5
|
+
* All output goes to stderr to keep stdout clean for MCP JSON-RPC.
|
|
6
|
+
*
|
|
7
|
+
* Enable with `zhihand start --debug`.
|
|
6
8
|
*/
|
|
7
|
-
|
|
9
|
+
import { log, setDebugEnabled as coreSetDebug, isDebugEnabled as coreIsDebug, } from "../core/logger.js";
|
|
8
10
|
export function setDebugEnabled(enabled) {
|
|
9
|
-
|
|
11
|
+
coreSetDebug(enabled);
|
|
10
12
|
}
|
|
11
13
|
export function isDebugEnabled() {
|
|
12
|
-
return
|
|
13
|
-
}
|
|
14
|
-
function ts() {
|
|
15
|
-
return new Date().toLocaleTimeString();
|
|
14
|
+
return coreIsDebug();
|
|
16
15
|
}
|
|
17
|
-
/** Debug log — only outputs when --debug is active. */
|
|
16
|
+
/** Debug log — only outputs when --debug is active. Writes to stderr with redaction. */
|
|
18
17
|
export function dbg(msg) {
|
|
19
|
-
if (!
|
|
18
|
+
if (!coreIsDebug())
|
|
20
19
|
return;
|
|
21
|
-
|
|
20
|
+
log.debug(msg);
|
|
22
21
|
}
|
|
@@ -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.1";
|
|
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.1";
|
|
12
12
|
function errorResult(message) {
|
|
13
13
|
return { content: [{ type: "text", text: message }], isError: true };
|
|
14
14
|
}
|