@zhihand/mcp 0.33.1 → 0.34.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 +20 -0
- package/dist/core/config.d.ts +11 -0
- package/dist/core/config.js +31 -0
- package/dist/core/pair.d.ts +8 -1
- package/dist/core/pair.js +26 -10
- package/dist/daemon/heartbeat.d.ts +9 -4
- package/dist/daemon/heartbeat.js +12 -12
- package/dist/daemon/index.js +26 -6
- package/dist/daemon/prompt-listener.d.ts +8 -4
- package/dist/daemon/prompt-listener.js +19 -24
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/bin/zhihand
CHANGED
|
@@ -21,6 +21,8 @@ import {
|
|
|
21
21
|
DEFAULT_MODELS,
|
|
22
22
|
resolveConfig,
|
|
23
23
|
resolveDefaultEndpoint,
|
|
24
|
+
loadPluginIdentity,
|
|
25
|
+
clearPluginIdentity,
|
|
24
26
|
} from "../dist/core/config.js";
|
|
25
27
|
import {
|
|
26
28
|
executePairingNewUser,
|
|
@@ -207,6 +209,24 @@ switch (command) {
|
|
|
207
209
|
break;
|
|
208
210
|
}
|
|
209
211
|
|
|
212
|
+
case "identity": {
|
|
213
|
+
const subCmd = positionals[1];
|
|
214
|
+
if (subCmd === "reset") {
|
|
215
|
+
clearPluginIdentity();
|
|
216
|
+
console.log("Plugin identity cleared. Next 'zhihand pair' will register a new identity.");
|
|
217
|
+
} else {
|
|
218
|
+
const id = loadPluginIdentity();
|
|
219
|
+
if (id) {
|
|
220
|
+
console.log(`Edge ID: ${id.edge_id}`);
|
|
221
|
+
console.log(`Stable Identity: ${id.stable_identity}`);
|
|
222
|
+
console.log(`Plugin Secret: ${id.plugin_secret.slice(0, 6)}...(redacted)`);
|
|
223
|
+
} else {
|
|
224
|
+
console.log("No plugin identity found. Run 'zhihand pair' to create one.");
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
|
|
210
230
|
case "mcp":
|
|
211
231
|
case "serve": {
|
|
212
232
|
await startStdioServer();
|
package/dist/core/config.d.ts
CHANGED
|
@@ -34,9 +34,20 @@ export interface BackendConfig {
|
|
|
34
34
|
model?: string | null;
|
|
35
35
|
}
|
|
36
36
|
export declare const DEFAULT_MODELS: Record<Exclude<BackendName, "openclaw">, string>;
|
|
37
|
+
export interface PluginIdentity {
|
|
38
|
+
stable_identity: string;
|
|
39
|
+
edge_id: string;
|
|
40
|
+
plugin_secret: string;
|
|
41
|
+
}
|
|
37
42
|
export declare function resolveZhiHandDir(): string;
|
|
38
43
|
export declare function ensureZhiHandDir(): void;
|
|
39
44
|
export declare function getConfigPath(): string;
|
|
45
|
+
/** Read persisted Plugin identity. Returns null if missing or malformed. */
|
|
46
|
+
export declare function loadPluginIdentity(): PluginIdentity | null;
|
|
47
|
+
/** Atomically persist Plugin identity (write-to-tmp + rename, mode 0o600). */
|
|
48
|
+
export declare function savePluginIdentity(identity: PluginIdentity): void;
|
|
49
|
+
/** Delete identity.json (used by `zhihand identity reset`). */
|
|
50
|
+
export declare function clearPluginIdentity(): void;
|
|
40
51
|
export declare function loadConfig(): ZhihandConfigV3;
|
|
41
52
|
/**
|
|
42
53
|
* Atomically write config: write to .tmp, then rename. Prevents corruption
|
package/dist/core/config.js
CHANGED
|
@@ -11,6 +11,7 @@ const ZHIHAND_DIR = path.join(os.homedir(), ".zhihand");
|
|
|
11
11
|
const CONFIG_PATH = path.join(ZHIHAND_DIR, "config.json");
|
|
12
12
|
const STATE_PATH = path.join(ZHIHAND_DIR, "state.json");
|
|
13
13
|
const BACKEND_PATH = path.join(ZHIHAND_DIR, "backend.json");
|
|
14
|
+
const IDENTITY_PATH = path.join(ZHIHAND_DIR, "identity.json");
|
|
14
15
|
export function resolveZhiHandDir() {
|
|
15
16
|
return ZHIHAND_DIR;
|
|
16
17
|
}
|
|
@@ -20,6 +21,36 @@ export function ensureZhiHandDir() {
|
|
|
20
21
|
export function getConfigPath() {
|
|
21
22
|
return CONFIG_PATH;
|
|
22
23
|
}
|
|
24
|
+
// ── Plugin identity I/O ──────────────────────────────────
|
|
25
|
+
/** Read persisted Plugin identity. Returns null if missing or malformed. */
|
|
26
|
+
export function loadPluginIdentity() {
|
|
27
|
+
try {
|
|
28
|
+
const raw = JSON.parse(fs.readFileSync(IDENTITY_PATH, "utf-8"));
|
|
29
|
+
if (raw.stable_identity && raw.edge_id && raw.plugin_secret) {
|
|
30
|
+
return raw;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Atomically persist Plugin identity (write-to-tmp + rename, mode 0o600). */
|
|
39
|
+
export function savePluginIdentity(identity) {
|
|
40
|
+
ensureZhiHandDir();
|
|
41
|
+
const tmp = IDENTITY_PATH + ".tmp";
|
|
42
|
+
fs.writeFileSync(tmp, JSON.stringify(identity, null, 2), { mode: 0o600 });
|
|
43
|
+
fs.renameSync(tmp, IDENTITY_PATH);
|
|
44
|
+
}
|
|
45
|
+
/** Delete identity.json (used by `zhihand identity reset`). */
|
|
46
|
+
export function clearPluginIdentity() {
|
|
47
|
+
try {
|
|
48
|
+
fs.unlinkSync(IDENTITY_PATH);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// already gone
|
|
52
|
+
}
|
|
53
|
+
}
|
|
23
54
|
// ── v3 config I/O ──────────────────────────────────────────
|
|
24
55
|
let legacyWarningPrinted = false;
|
|
25
56
|
function emptyConfig() {
|
package/dist/core/pair.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DeviceRecord, UserRecord } from "./config.ts";
|
|
1
|
+
import type { DeviceRecord, UserRecord, PluginIdentity } from "./config.ts";
|
|
2
2
|
export interface PairingSession {
|
|
3
3
|
session_id: string;
|
|
4
4
|
pair_url: string;
|
|
@@ -30,7 +30,14 @@ export declare function registerPlugin(endpoint: string, options: {
|
|
|
30
30
|
adapterKind?: string;
|
|
31
31
|
}): Promise<{
|
|
32
32
|
edge_id: string;
|
|
33
|
+
plugin_secret: string;
|
|
33
34
|
}>;
|
|
35
|
+
/**
|
|
36
|
+
* Ensure a persistent Plugin identity exists. Reuses existing identity
|
|
37
|
+
* if present; otherwise registers a new plugin and persists the result.
|
|
38
|
+
* All pair operations share the same identity → same EdgeID.
|
|
39
|
+
*/
|
|
40
|
+
export declare function ensurePluginIdentity(endpoint: string): Promise<PluginIdentity>;
|
|
34
41
|
/**
|
|
35
42
|
* Poll pairing session until claimed or expired.
|
|
36
43
|
*/
|
package/dist/core/pair.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import QRCode from "qrcode";
|
|
2
|
-
import
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import { addUser, addDeviceToUser, ensureZhiHandDir, saveState, resolveDefaultEndpoint, getUserRecord, cleanupLegacyConfig, loadPluginIdentity, savePluginIdentity, } from "./config.js";
|
|
3
4
|
import { fetchDeviceProfileOnce, extractStatic } from "./device.js";
|
|
4
5
|
import { fetchUserCredentials } from "./ws.js";
|
|
5
6
|
// ── Server API helpers ─────────────────────────────────────
|
|
@@ -58,7 +59,24 @@ export async function registerPlugin(endpoint, options) {
|
|
|
58
59
|
throw new Error(`Register plugin failed: ${response.status} ${await response.text()}`);
|
|
59
60
|
}
|
|
60
61
|
const payload = (await response.json());
|
|
61
|
-
return { edge_id: payload.plugin.edge_id };
|
|
62
|
+
return { edge_id: payload.plugin.edge_id, plugin_secret: payload.plugin.plugin_secret };
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Ensure a persistent Plugin identity exists. Reuses existing identity
|
|
66
|
+
* if present; otherwise registers a new plugin and persists the result.
|
|
67
|
+
* All pair operations share the same identity → same EdgeID.
|
|
68
|
+
*/
|
|
69
|
+
export async function ensurePluginIdentity(endpoint) {
|
|
70
|
+
const existing = loadPluginIdentity();
|
|
71
|
+
const stableId = existing?.stable_identity ?? `mcp-${os.hostname()}-${Date.now().toString(36)}`;
|
|
72
|
+
const plugin = await registerPlugin(endpoint, { stableIdentity: stableId });
|
|
73
|
+
const identity = {
|
|
74
|
+
stable_identity: stableId,
|
|
75
|
+
edge_id: plugin.edge_id,
|
|
76
|
+
plugin_secret: plugin.plugin_secret,
|
|
77
|
+
};
|
|
78
|
+
savePluginIdentity(identity);
|
|
79
|
+
return identity;
|
|
62
80
|
}
|
|
63
81
|
/**
|
|
64
82
|
* Poll pairing session until claimed or expired.
|
|
@@ -97,10 +115,9 @@ export async function executePairingNewUser(preferredLabel) {
|
|
|
97
115
|
cleanupLegacyConfig();
|
|
98
116
|
const userId = userResp.user_id;
|
|
99
117
|
const controllerToken = userResp.controller_token;
|
|
100
|
-
// 2.
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
const edgeId = plugin.edge_id;
|
|
118
|
+
// 2. Ensure persistent plugin identity (same EdgeID across all pairs)
|
|
119
|
+
const identity = await ensurePluginIdentity(endpoint);
|
|
120
|
+
const edgeId = identity.edge_id;
|
|
104
121
|
// 3. Create pairing session
|
|
105
122
|
const session = await createPairingSession(endpoint, userId, controllerToken, edgeId, 300);
|
|
106
123
|
saveState({
|
|
@@ -186,10 +203,9 @@ export async function executePairingAddDevice(userId, preferredLabel) {
|
|
|
186
203
|
if (!user)
|
|
187
204
|
throw new Error(`User '${userId}' not found in config`);
|
|
188
205
|
const controllerToken = user.controller_token;
|
|
189
|
-
//
|
|
190
|
-
const
|
|
191
|
-
const
|
|
192
|
-
const edgeId = plugin.edge_id;
|
|
206
|
+
// Ensure persistent plugin identity (same EdgeID across all pairs)
|
|
207
|
+
const identity = await ensurePluginIdentity(endpoint);
|
|
208
|
+
const edgeId = identity.edge_id;
|
|
193
209
|
// Get existing credential IDs before pairing
|
|
194
210
|
const existingCreds = await fetchUserCredentials(endpoint, userId, controllerToken);
|
|
195
211
|
const existingIds = new Set(existingCreds.map((c) => c.credential_id));
|
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import type { ZhiHandRuntimeConfig } from "../core/config.ts";
|
|
2
1
|
/** Brain metadata included in every heartbeat, so the app always knows the current backend/model. */
|
|
3
2
|
export interface BrainMeta {
|
|
4
3
|
backend?: string | null;
|
|
5
4
|
model?: string | null;
|
|
6
5
|
}
|
|
6
|
+
/** Plugin-level heartbeat target. Uses edgeId + pluginSecret instead of per-credential auth. */
|
|
7
|
+
export interface HeartbeatTarget {
|
|
8
|
+
controlPlaneEndpoint: string;
|
|
9
|
+
edgeId: string;
|
|
10
|
+
pluginSecret: string;
|
|
11
|
+
}
|
|
7
12
|
/** Update the backend/model metadata that will be sent with the next heartbeat. */
|
|
8
13
|
export declare function setBrainMeta(meta: BrainMeta): void;
|
|
9
|
-
export declare function sendBrainOnline(
|
|
10
|
-
export declare function sendBrainOffline(
|
|
11
|
-
export declare function startHeartbeatLoop(
|
|
14
|
+
export declare function sendBrainOnline(target: HeartbeatTarget): Promise<boolean>;
|
|
15
|
+
export declare function sendBrainOffline(target: HeartbeatTarget): Promise<boolean>;
|
|
16
|
+
export declare function startHeartbeatLoop(target: HeartbeatTarget, log: (msg: string) => void): void;
|
|
12
17
|
export declare function stopHeartbeatLoop(): void;
|
package/dist/daemon/heartbeat.js
CHANGED
|
@@ -9,23 +9,23 @@ let currentMeta = {};
|
|
|
9
9
|
export function setBrainMeta(meta) {
|
|
10
10
|
currentMeta = meta;
|
|
11
11
|
}
|
|
12
|
-
function buildUrl(
|
|
13
|
-
return `${
|
|
12
|
+
function buildUrl(target) {
|
|
13
|
+
return `${target.controlPlaneEndpoint}/v1/plugins/${encodeURIComponent(target.edgeId)}/brain-status`;
|
|
14
14
|
}
|
|
15
|
-
async function sendHeartbeat(
|
|
15
|
+
async function sendHeartbeat(target, online) {
|
|
16
16
|
try {
|
|
17
17
|
const body = { plugin_online: online };
|
|
18
18
|
if (currentMeta.backend)
|
|
19
19
|
body.backend = currentMeta.backend;
|
|
20
20
|
if (currentMeta.model)
|
|
21
21
|
body.model = currentMeta.model;
|
|
22
|
-
const url = buildUrl(
|
|
22
|
+
const url = buildUrl(target);
|
|
23
23
|
dbg(`[heartbeat] POST ${url} body=${JSON.stringify(body)}`);
|
|
24
24
|
const response = await fetch(url, {
|
|
25
25
|
method: "POST",
|
|
26
26
|
headers: {
|
|
27
27
|
"Content-Type": "application/json",
|
|
28
|
-
"Authorization": `Bearer ${
|
|
28
|
+
"Authorization": `Bearer ${target.pluginSecret}`,
|
|
29
29
|
},
|
|
30
30
|
body: JSON.stringify(body),
|
|
31
31
|
signal: AbortSignal.timeout(10_000),
|
|
@@ -38,20 +38,20 @@ async function sendHeartbeat(config, online) {
|
|
|
38
38
|
return false;
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
-
export async function sendBrainOnline(
|
|
42
|
-
return sendHeartbeat(
|
|
41
|
+
export async function sendBrainOnline(target) {
|
|
42
|
+
return sendHeartbeat(target, true);
|
|
43
43
|
}
|
|
44
|
-
export async function sendBrainOffline(
|
|
45
|
-
return sendHeartbeat(
|
|
44
|
+
export async function sendBrainOffline(target) {
|
|
45
|
+
return sendHeartbeat(target, false);
|
|
46
46
|
}
|
|
47
|
-
export function startHeartbeatLoop(
|
|
47
|
+
export function startHeartbeatLoop(target, log) {
|
|
48
48
|
let retrying = false;
|
|
49
49
|
stopped = false;
|
|
50
50
|
async function beat() {
|
|
51
51
|
// Skip main-timer beats while retry loop is active (avoids overlap & flapping)
|
|
52
52
|
if (retrying || stopped)
|
|
53
53
|
return;
|
|
54
|
-
const ok = await sendBrainOnline(
|
|
54
|
+
const ok = await sendBrainOnline(target);
|
|
55
55
|
if (stopped)
|
|
56
56
|
return; // check after await — stopHeartbeatLoop() may have been called
|
|
57
57
|
if (ok) {
|
|
@@ -70,7 +70,7 @@ export function startHeartbeatLoop(config, log) {
|
|
|
70
70
|
retryTimer = setTimeout(async () => {
|
|
71
71
|
if (!retrying || stopped)
|
|
72
72
|
return;
|
|
73
|
-
const recovered = await sendBrainOnline(
|
|
73
|
+
const recovered = await sendBrainOnline(target);
|
|
74
74
|
if (stopped)
|
|
75
75
|
return; // check after await
|
|
76
76
|
if (recovered) {
|
package/dist/daemon/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
6
6
|
// Transport type used only for cleanup interface
|
|
7
7
|
import { createServer as createMcpServer } from "../index.js";
|
|
8
|
-
import { resolveConfig, loadBackendConfig, saveBackendConfig, resolveZhiHandDir, ensureZhiHandDir, DEFAULT_MODELS, } from "../core/config.js";
|
|
8
|
+
import { resolveConfig, loadBackendConfig, saveBackendConfig, resolveZhiHandDir, resolveDefaultEndpoint, ensureZhiHandDir, loadPluginIdentity, DEFAULT_MODELS, } from "../core/config.js";
|
|
9
9
|
import { PACKAGE_VERSION } from "../index.js";
|
|
10
10
|
import { startHeartbeatLoop, stopHeartbeatLoop, sendBrainOffline, setBrainMeta } from "./heartbeat.js";
|
|
11
11
|
import { PromptListener } from "./prompt-listener.js";
|
|
@@ -359,15 +359,35 @@ export async function startDaemon(options) {
|
|
|
359
359
|
httpServer.listen(port, "127.0.0.1", () => resolve());
|
|
360
360
|
});
|
|
361
361
|
writePid();
|
|
362
|
-
//
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
362
|
+
// Load Plugin identity for edge-level heartbeat + prompt WS
|
|
363
|
+
const identity = loadPluginIdentity();
|
|
364
|
+
if (!identity) {
|
|
365
|
+
log("[identity] No plugin identity found. Run 'zhihand pair' first.");
|
|
366
|
+
process.exit(1);
|
|
367
|
+
}
|
|
368
|
+
log(`[identity] Loaded: edge_id=${identity.edge_id}, stable_identity=${identity.stable_identity}`);
|
|
369
|
+
const heartbeatTarget = {
|
|
370
|
+
controlPlaneEndpoint: resolveDefaultEndpoint(),
|
|
371
|
+
edgeId: identity.edge_id,
|
|
372
|
+
pluginSecret: identity.plugin_secret,
|
|
373
|
+
};
|
|
374
|
+
// Start heartbeat (edge-level, pluginSecret auth)
|
|
375
|
+
startHeartbeatLoop(heartbeatTarget, log);
|
|
376
|
+
// Start prompt listener (edge-level single WS)
|
|
377
|
+
const promptListener = new PromptListener({
|
|
378
|
+
controlPlaneEndpoint: resolveDefaultEndpoint(),
|
|
379
|
+
edgeId: identity.edge_id,
|
|
380
|
+
pluginSecret: identity.plugin_secret,
|
|
381
|
+
}, (prompt) => onPromptReceived(config, prompt), log, (reason) => {
|
|
382
|
+
log(`[fatal] ${reason}`);
|
|
383
|
+
process.exit(1);
|
|
384
|
+
});
|
|
366
385
|
promptListener.start();
|
|
367
386
|
log(`ZhiHand daemon started.`);
|
|
368
387
|
log(` PID: ${process.pid}`);
|
|
369
388
|
log(` MCP: http://127.0.0.1:${port}/mcp`);
|
|
370
389
|
log(` Backend: ${activeBackend ?? "(none)"}`);
|
|
390
|
+
log(` Edge: ${identity.edge_id}`);
|
|
371
391
|
log(` Device: ${config.credentialId}`);
|
|
372
392
|
log(`Listening for prompts...`);
|
|
373
393
|
// Graceful shutdown
|
|
@@ -377,7 +397,7 @@ export async function startDaemon(options) {
|
|
|
377
397
|
stopHeartbeatLoop();
|
|
378
398
|
clearInterval(sessionCleanupTimer);
|
|
379
399
|
await killActiveChild();
|
|
380
|
-
await sendBrainOffline(
|
|
400
|
+
await sendBrainOffline(heartbeatTarget);
|
|
381
401
|
// Close all MCP sessions
|
|
382
402
|
for (const session of mcpSessions.values()) {
|
|
383
403
|
try {
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { ZhiHandRuntimeConfig } from "../core/config.ts";
|
|
2
|
-
type ZhiHandConfig = ZhiHandRuntimeConfig;
|
|
3
1
|
export interface MobilePrompt {
|
|
4
2
|
id: string;
|
|
5
3
|
credential_id: string;
|
|
@@ -11,14 +9,21 @@ export interface MobilePrompt {
|
|
|
11
9
|
attachments?: unknown[];
|
|
12
10
|
}
|
|
13
11
|
export type PromptHandler = (prompt: MobilePrompt) => void;
|
|
12
|
+
/** Edge-level prompt listener config. Uses pluginSecret instead of per-user controllerToken. */
|
|
13
|
+
export interface PromptListenerConfig {
|
|
14
|
+
controlPlaneEndpoint: string;
|
|
15
|
+
edgeId: string;
|
|
16
|
+
pluginSecret: string;
|
|
17
|
+
}
|
|
14
18
|
export declare class PromptListener {
|
|
15
19
|
private config;
|
|
16
20
|
private handler;
|
|
17
21
|
private log;
|
|
22
|
+
private onFatalError?;
|
|
18
23
|
private processedIds;
|
|
19
24
|
private rws;
|
|
20
25
|
private stopped;
|
|
21
|
-
constructor(config:
|
|
26
|
+
constructor(config: PromptListenerConfig, handler: PromptHandler, log: (msg: string) => void, onFatalError?: (reason: string) => void);
|
|
22
27
|
start(): void;
|
|
23
28
|
stop(): void;
|
|
24
29
|
private dispatchPrompt;
|
|
@@ -26,4 +31,3 @@ export declare class PromptListener {
|
|
|
26
31
|
private handleWSMessage;
|
|
27
32
|
private handleEvent;
|
|
28
33
|
}
|
|
29
|
-
export {};
|
|
@@ -4,13 +4,15 @@ export class PromptListener {
|
|
|
4
4
|
config;
|
|
5
5
|
handler;
|
|
6
6
|
log;
|
|
7
|
+
onFatalError;
|
|
7
8
|
processedIds = new Set();
|
|
8
9
|
rws = null;
|
|
9
10
|
stopped = false;
|
|
10
|
-
constructor(config, handler, log) {
|
|
11
|
+
constructor(config, handler, log, onFatalError) {
|
|
11
12
|
this.config = config;
|
|
12
13
|
this.handler = handler;
|
|
13
14
|
this.log = log;
|
|
15
|
+
this.onFatalError = onFatalError;
|
|
14
16
|
}
|
|
15
17
|
start() {
|
|
16
18
|
this.stopped = false;
|
|
@@ -27,7 +29,7 @@ export class PromptListener {
|
|
|
27
29
|
return;
|
|
28
30
|
}
|
|
29
31
|
this.processedIds.add(prompt.id);
|
|
30
|
-
dbg(`[prompt] Dispatching prompt: id=${prompt.id}, status=${prompt.status}, text="${prompt.text.slice(0, 100)}${prompt.text.length > 100 ? "..." : ""}"`);
|
|
32
|
+
dbg(`[prompt] Dispatching prompt: id=${prompt.id}, cred=${prompt.credential_id}, status=${prompt.status}, text="${prompt.text.slice(0, 100)}${prompt.text.length > 100 ? "..." : ""}"`);
|
|
31
33
|
// Prevent unbounded growth
|
|
32
34
|
if (this.processedIds.size > 500) {
|
|
33
35
|
const arr = [...this.processedIds];
|
|
@@ -38,21 +40,18 @@ export class PromptListener {
|
|
|
38
40
|
connectWS() {
|
|
39
41
|
if (this.stopped)
|
|
40
42
|
return;
|
|
41
|
-
|
|
43
|
+
// Edge-level WS: single connection for all credentials under this edge
|
|
44
|
+
const wsUrl = `${this.config.controlPlaneEndpoint.replace(/^http/, "ws")}/v1/plugins/${encodeURIComponent(this.config.edgeId)}/ws?topic=prompts`;
|
|
42
45
|
dbg(`[ws] Connecting to ${wsUrl}`);
|
|
43
46
|
this.rws = new ReconnectingWebSocket({
|
|
44
47
|
url: wsUrl,
|
|
45
|
-
headers: {
|
|
46
|
-
"Authorization": `Bearer ${this.config.controllerToken}`,
|
|
47
|
-
},
|
|
48
|
+
headers: {},
|
|
48
49
|
onOpen: () => {
|
|
49
|
-
// Send auth
|
|
50
|
+
// Send auth frame with pluginSecret (server verifies before streaming)
|
|
50
51
|
this.rws.send(JSON.stringify({
|
|
51
52
|
type: "auth",
|
|
52
|
-
|
|
53
|
-
topics: ["prompts"],
|
|
53
|
+
plugin_secret: this.config.pluginSecret,
|
|
54
54
|
}));
|
|
55
|
-
// onConnected deferred until auth_ok is received (see handleWSMessage)
|
|
56
55
|
},
|
|
57
56
|
onClose: (_code, _reason) => {
|
|
58
57
|
dbg("[ws] Disconnected. ReconnectingWebSocket will retry.");
|
|
@@ -68,42 +67,38 @@ export class PromptListener {
|
|
|
68
67
|
}
|
|
69
68
|
handleWSMessage(data) {
|
|
70
69
|
const msg = data;
|
|
71
|
-
// Auth responses
|
|
72
70
|
if (msg.type === "auth_ok") {
|
|
73
|
-
this.log("[ws] Connected to prompt stream.");
|
|
71
|
+
this.log("[ws] Connected to Edge prompt stream.");
|
|
74
72
|
return;
|
|
75
73
|
}
|
|
76
74
|
if (msg.type === "auth_error") {
|
|
77
|
-
|
|
75
|
+
const reason = `Plugin auth failed: ${msg.error}. Run 'zhihand pair' to re-register.`;
|
|
76
|
+
this.log(`[ws] ${reason}`);
|
|
78
77
|
this.rws?.stop();
|
|
79
78
|
this.rws = null;
|
|
79
|
+
this.onFatalError?.(reason);
|
|
80
80
|
return;
|
|
81
81
|
}
|
|
82
|
-
// Application-level ping (if server sends these alongside protocol pings)
|
|
83
82
|
if (msg.type === "ping") {
|
|
84
83
|
this.rws?.send(JSON.stringify({ type: "pong" }));
|
|
85
84
|
return;
|
|
86
85
|
}
|
|
87
|
-
// Event dispatch
|
|
88
86
|
if (msg.type === "event" || msg.kind) {
|
|
89
87
|
this.handleEvent(msg);
|
|
90
88
|
}
|
|
91
89
|
}
|
|
92
90
|
handleEvent(event) {
|
|
93
91
|
const kind = event.kind;
|
|
94
|
-
dbg(`[ws] Event: kind=${kind}, prompt=${event.prompt?.id ?? "-"}
|
|
92
|
+
dbg(`[ws] Event: kind=${kind}, prompt=${event.prompt?.id ?? "-"}`);
|
|
95
93
|
if (kind === "prompt.queued" && event.prompt) {
|
|
96
94
|
this.dispatchPrompt(event.prompt);
|
|
97
95
|
}
|
|
98
|
-
else if (kind === "prompt.snapshot" && event.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
96
|
+
else if (kind === "prompt.snapshot" && event.prompt) {
|
|
97
|
+
// Edge WS sends individual snapshot events (one per pending prompt)
|
|
98
|
+
const p = event.prompt;
|
|
99
|
+
if (p.status === "pending" || p.status === "processing") {
|
|
100
|
+
this.dispatchPrompt(p);
|
|
103
101
|
}
|
|
104
102
|
}
|
|
105
|
-
else if (kind === "device_profile.updated") {
|
|
106
|
-
this.log("[device] device_profile.updated event received on prompts stream (ignored; registry handles it)");
|
|
107
|
-
}
|
|
108
103
|
}
|
|
109
104
|
}
|
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.34.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.34.0";
|
|
12
12
|
function errorResult(message) {
|
|
13
13
|
return { content: [{ type: "text", text: message }], isError: true };
|
|
14
14
|
}
|