@zhihand/mcp 0.28.0 → 0.30.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 +254 -158
- package/dist/core/command.d.ts +5 -5
- package/dist/core/command.js +2 -5
- package/dist/core/config.d.ts +32 -20
- package/dist/core/config.js +102 -40
- package/dist/core/device.d.ts +41 -16
- package/dist/core/device.js +199 -79
- package/dist/core/pair.d.ts +9 -9
- package/dist/core/pair.js +54 -30
- package/dist/core/registry.d.ts +67 -0
- package/dist/core/registry.js +288 -0
- package/dist/core/screenshot.d.ts +13 -2
- package/dist/core/screenshot.js +43 -3
- package/dist/core/sse.d.ts +13 -16
- package/dist/core/sse.js +46 -54
- package/dist/daemon/dispatcher.d.ts +3 -1
- package/dist/daemon/dispatcher.js +3 -2
- package/dist/daemon/heartbeat.d.ts +4 -4
- package/dist/daemon/index.js +8 -6
- package/dist/daemon/prompt-listener.d.ts +3 -1
- package/dist/daemon/prompt-listener.js +2 -6
- package/dist/index.d.ts +3 -3
- package/dist/index.js +102 -40
- package/dist/openclaw.adapter.js +10 -2
- package/dist/tools/control.d.ts +10 -3
- package/dist/tools/control.js +58 -29
- package/dist/tools/pair.js +15 -18
- package/dist/tools/resolve.d.ts +7 -0
- package/dist/tools/resolve.js +22 -0
- package/dist/tools/schemas.d.ts +9 -1
- package/dist/tools/schemas.js +10 -8
- package/dist/tools/screenshot.d.ts +3 -2
- package/dist/tools/screenshot.js +2 -2
- package/dist/tools/system.d.ts +3 -5
- package/dist/tools/system.js +18 -5
- package/package.json +1 -1
package/dist/core/command.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ZhiHandRuntimeConfig } from "./config.ts";
|
|
2
2
|
export type ScrollDirection = "up" | "down" | "left" | "right";
|
|
3
3
|
export interface ControlParams {
|
|
4
4
|
action: string;
|
|
@@ -36,12 +36,12 @@ export interface WaitForCommandAckResult {
|
|
|
36
36
|
acked: boolean;
|
|
37
37
|
command?: QueuedCommandRecord;
|
|
38
38
|
}
|
|
39
|
-
export declare function createControlCommand(params: ControlParams): QueuedControlCommand;
|
|
39
|
+
export declare function createControlCommand(params: ControlParams, platform?: string): QueuedControlCommand;
|
|
40
40
|
export interface SystemParams {
|
|
41
41
|
action: string;
|
|
42
42
|
text?: string;
|
|
43
43
|
}
|
|
44
|
-
export declare function createSystemCommand(params: SystemParams): QueuedControlCommand;
|
|
45
|
-
export declare function enqueueCommand(config:
|
|
46
|
-
export declare function getCommand(config:
|
|
44
|
+
export declare function createSystemCommand(params: SystemParams, platform?: string): QueuedControlCommand;
|
|
45
|
+
export declare function enqueueCommand(config: ZhiHandRuntimeConfig, command: QueuedControlCommand): Promise<QueuedCommandRecord>;
|
|
46
|
+
export declare function getCommand(config: ZhiHandRuntimeConfig, commandId: string): Promise<QueuedCommandRecord>;
|
|
47
47
|
export declare function formatAckSummary(action: string, result: WaitForCommandAckResult): string;
|
package/dist/core/command.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { getStaticContext, isDeviceProfileLoaded } from "./device.js";
|
|
2
1
|
import { dbg } from "../daemon/logger.js";
|
|
3
2
|
let messageCounter = 0;
|
|
4
3
|
function nextMessageId() {
|
|
5
4
|
messageCounter = (messageCounter + 1) % 1000;
|
|
6
5
|
return (Date.now() * 1000) + messageCounter;
|
|
7
6
|
}
|
|
8
|
-
export function createControlCommand(params) {
|
|
7
|
+
export function createControlCommand(params, platform = "unknown") {
|
|
9
8
|
switch (params.action) {
|
|
10
9
|
case "click":
|
|
11
10
|
return { type: "receive_click", payload: { x: params.xRatio, y: params.yRatio } };
|
|
@@ -57,7 +56,6 @@ export function createControlCommand(params) {
|
|
|
57
56
|
};
|
|
58
57
|
case "open_app": {
|
|
59
58
|
const appPayload = {};
|
|
60
|
-
const platform = isDeviceProfileLoaded() ? getStaticContext().platform : "unknown";
|
|
61
59
|
// Only send platform-appropriate fields — Android strict JSON rejects unknown keys
|
|
62
60
|
if (platform === "android") {
|
|
63
61
|
// Android: only app_package
|
|
@@ -95,8 +93,7 @@ export function createControlCommand(params) {
|
|
|
95
93
|
}
|
|
96
94
|
const IOS_ONLY_ACTIONS = new Set(["siri", "control_center"]);
|
|
97
95
|
const ANDROID_ONLY_ACTIONS = new Set(["open_browser", "shortcut_help"]);
|
|
98
|
-
export function createSystemCommand(params) {
|
|
99
|
-
const platform = isDeviceProfileLoaded() ? getStaticContext().platform : "unknown";
|
|
96
|
+
export function createSystemCommand(params, platform = "unknown") {
|
|
100
97
|
// Platform validation — block mismatched platform-specific actions
|
|
101
98
|
if (platform === "android" && IOS_ONLY_ACTIONS.has(params.action)) {
|
|
102
99
|
throw new Error(`Action '${params.action}' is not supported on Android.`);
|
package/dist/core/config.d.ts
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
export type DevicePlatform = "ios" | "android" | "unknown";
|
|
2
|
+
export interface DeviceRecord {
|
|
3
|
+
credential_id: string;
|
|
4
|
+
controller_token: string;
|
|
4
5
|
endpoint: string;
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
label: string;
|
|
7
|
+
platform: DevicePlatform;
|
|
8
|
+
paired_at: string;
|
|
9
|
+
last_seen_at: string;
|
|
7
10
|
}
|
|
8
|
-
export interface
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
export interface ZhihandConfig {
|
|
12
|
+
schema_version: 2;
|
|
13
|
+
default_credential_id: string | null;
|
|
14
|
+
devices: Record<string, DeviceRecord>;
|
|
11
15
|
}
|
|
12
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Legacy-shaped config passed to HTTP callers (command/sse/device endpoints).
|
|
18
|
+
* Corresponds to what the old single-device code called ZhiHandConfig.
|
|
19
|
+
*/
|
|
20
|
+
export interface ZhiHandRuntimeConfig {
|
|
13
21
|
controlPlaneEndpoint: string;
|
|
14
22
|
credentialId: string;
|
|
15
23
|
controllerToken: string;
|
|
@@ -21,20 +29,24 @@ export interface BackendConfig {
|
|
|
21
29
|
activeBackend: BackendName | null;
|
|
22
30
|
model?: string | null;
|
|
23
31
|
}
|
|
24
|
-
/**
|
|
25
|
-
* Default model aliases per backend.
|
|
26
|
-
* These are generic aliases that the respective CLIs resolve to the latest version:
|
|
27
|
-
* - Gemini CLI: "flash" → latest flash model (e.g. gemini-2.5-flash)
|
|
28
|
-
* - Claude Code: "sonnet" → latest sonnet (e.g. claude-sonnet-4-20250514)
|
|
29
|
-
* - Codex CLI: requires full model name, no alias support
|
|
30
|
-
*/
|
|
31
32
|
export declare const DEFAULT_MODELS: Record<Exclude<BackendName, "openclaw">, string>;
|
|
32
33
|
export declare function resolveZhiHandDir(): string;
|
|
33
34
|
export declare function ensureZhiHandDir(): void;
|
|
34
|
-
export declare function
|
|
35
|
-
export declare function
|
|
36
|
-
export declare function
|
|
37
|
-
export declare function
|
|
35
|
+
export declare function loadConfig(): ZhihandConfig;
|
|
36
|
+
export declare function saveConfig(cfg: ZhihandConfig): void;
|
|
37
|
+
export declare function addDevice(record: DeviceRecord, makeDefault?: boolean): void;
|
|
38
|
+
export declare function removeDevice(credentialId: string): void;
|
|
39
|
+
export declare function renameDevice(credentialId: string, label: string): void;
|
|
40
|
+
export declare function setDefaultDevice(credentialId: string): void;
|
|
41
|
+
export declare function updateLastSeen(credentialId: string, iso: string): void;
|
|
42
|
+
export declare function getDeviceRecord(credentialId: string): DeviceRecord | null;
|
|
43
|
+
export declare function listDeviceRecords(): DeviceRecord[];
|
|
44
|
+
export declare function recordToRuntimeConfig(r: DeviceRecord): ZhiHandRuntimeConfig;
|
|
45
|
+
/**
|
|
46
|
+
* Resolve a runtime config for HTTP calls. If credentialId provided, look it up;
|
|
47
|
+
* else use default_credential_id; else throw.
|
|
48
|
+
*/
|
|
49
|
+
export declare function resolveConfig(credentialId?: string): ZhiHandRuntimeConfig;
|
|
38
50
|
export declare function loadState<T = unknown>(): T | null;
|
|
39
51
|
export declare function saveState(state: unknown): void;
|
|
40
52
|
export declare function loadBackendConfig(): BackendConfig;
|
package/dist/core/config.js
CHANGED
|
@@ -1,20 +1,15 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import os from "node:os";
|
|
4
|
-
/**
|
|
5
|
-
* Default model aliases per backend.
|
|
6
|
-
* These are generic aliases that the respective CLIs resolve to the latest version:
|
|
7
|
-
* - Gemini CLI: "flash" → latest flash model (e.g. gemini-2.5-flash)
|
|
8
|
-
* - Claude Code: "sonnet" → latest sonnet (e.g. claude-sonnet-4-20250514)
|
|
9
|
-
* - Codex CLI: requires full model name, no alias support
|
|
10
|
-
*/
|
|
11
4
|
export const DEFAULT_MODELS = {
|
|
12
|
-
gemini: "flash",
|
|
13
|
-
claudecode: "sonnet",
|
|
14
|
-
codex: "gpt-5.4-mini",
|
|
5
|
+
gemini: "flash",
|
|
6
|
+
claudecode: "sonnet",
|
|
7
|
+
codex: "gpt-5.4-mini",
|
|
15
8
|
};
|
|
9
|
+
// ── Paths ──────────────────────────────────────────────────
|
|
16
10
|
const ZHIHAND_DIR = path.join(os.homedir(), ".zhihand");
|
|
17
|
-
const
|
|
11
|
+
const CONFIG_PATH = path.join(ZHIHAND_DIR, "config.json");
|
|
12
|
+
const LEGACY_CREDENTIALS_PATH = path.join(ZHIHAND_DIR, "credentials.json");
|
|
18
13
|
const STATE_PATH = path.join(ZHIHAND_DIR, "state.json");
|
|
19
14
|
const BACKEND_PATH = path.join(ZHIHAND_DIR, "backend.json");
|
|
20
15
|
export function resolveZhiHandDir() {
|
|
@@ -23,47 +18,114 @@ export function resolveZhiHandDir() {
|
|
|
23
18
|
export function ensureZhiHandDir() {
|
|
24
19
|
fs.mkdirSync(ZHIHAND_DIR, { recursive: true, mode: 0o700 });
|
|
25
20
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
// ── v2 config I/O ──────────────────────────────────────────
|
|
22
|
+
let legacyWarningPrinted = false;
|
|
23
|
+
function emptyConfig() {
|
|
24
|
+
return { schema_version: 2, default_credential_id: null, devices: {} };
|
|
25
|
+
}
|
|
26
|
+
export function loadConfig() {
|
|
27
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
28
|
+
if (!legacyWarningPrinted && fs.existsSync(LEGACY_CREDENTIALS_PATH)) {
|
|
29
|
+
legacyWarningPrinted = true;
|
|
30
|
+
process.stderr.write("[zhihand] legacy credentials.json detected — run 'zhihand pair' to re-pair on v0.30 schema\n");
|
|
31
|
+
}
|
|
32
|
+
return emptyConfig();
|
|
33
|
+
}
|
|
29
34
|
try {
|
|
30
|
-
|
|
35
|
+
const raw = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8"));
|
|
36
|
+
if (raw && raw.schema_version === 2) {
|
|
37
|
+
return {
|
|
38
|
+
schema_version: 2,
|
|
39
|
+
default_credential_id: raw.default_credential_id ?? null,
|
|
40
|
+
devices: raw.devices ?? {},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
31
43
|
}
|
|
32
44
|
catch {
|
|
33
|
-
|
|
45
|
+
// fall through
|
|
34
46
|
}
|
|
47
|
+
return emptyConfig();
|
|
35
48
|
}
|
|
36
|
-
export function
|
|
37
|
-
const store = loadCredentialStore();
|
|
38
|
-
if (!store)
|
|
39
|
-
return null;
|
|
40
|
-
return store.devices[store.default] ?? null;
|
|
41
|
-
}
|
|
42
|
-
export function saveCredential(name, cred, setDefault = true) {
|
|
49
|
+
export function saveConfig(cfg) {
|
|
43
50
|
ensureZhiHandDir();
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2), { mode: 0o600 });
|
|
52
|
+
}
|
|
53
|
+
export function addDevice(record, makeDefault) {
|
|
54
|
+
const cfg = loadConfig();
|
|
55
|
+
cfg.devices[record.credential_id] = record;
|
|
56
|
+
if (makeDefault || cfg.default_credential_id === null) {
|
|
57
|
+
cfg.default_credential_id = record.credential_id;
|
|
58
|
+
}
|
|
59
|
+
saveConfig(cfg);
|
|
60
|
+
}
|
|
61
|
+
export function removeDevice(credentialId) {
|
|
62
|
+
const cfg = loadConfig();
|
|
63
|
+
delete cfg.devices[credentialId];
|
|
64
|
+
if (cfg.default_credential_id === credentialId) {
|
|
65
|
+
const remaining = Object.keys(cfg.devices);
|
|
66
|
+
cfg.default_credential_id = remaining[0] ?? null;
|
|
54
67
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
68
|
+
saveConfig(cfg);
|
|
69
|
+
}
|
|
70
|
+
export function renameDevice(credentialId, label) {
|
|
71
|
+
const cfg = loadConfig();
|
|
72
|
+
const r = cfg.devices[credentialId];
|
|
73
|
+
if (!r)
|
|
74
|
+
throw new Error(`Device '${credentialId}' not found`);
|
|
75
|
+
r.label = label;
|
|
76
|
+
saveConfig(cfg);
|
|
77
|
+
}
|
|
78
|
+
export function setDefaultDevice(credentialId) {
|
|
79
|
+
const cfg = loadConfig();
|
|
80
|
+
if (!cfg.devices[credentialId]) {
|
|
81
|
+
throw new Error(`Device '${credentialId}' not found`);
|
|
59
82
|
}
|
|
83
|
+
cfg.default_credential_id = credentialId;
|
|
84
|
+
saveConfig(cfg);
|
|
85
|
+
}
|
|
86
|
+
export function updateLastSeen(credentialId, iso) {
|
|
87
|
+
const cfg = loadConfig();
|
|
88
|
+
const r = cfg.devices[credentialId];
|
|
89
|
+
if (!r)
|
|
90
|
+
return;
|
|
91
|
+
r.last_seen_at = iso;
|
|
92
|
+
saveConfig(cfg);
|
|
93
|
+
}
|
|
94
|
+
export function getDeviceRecord(credentialId) {
|
|
95
|
+
const cfg = loadConfig();
|
|
96
|
+
return cfg.devices[credentialId] ?? null;
|
|
97
|
+
}
|
|
98
|
+
export function listDeviceRecords() {
|
|
99
|
+
const cfg = loadConfig();
|
|
100
|
+
return Object.values(cfg.devices);
|
|
101
|
+
}
|
|
102
|
+
// ── Runtime config resolution ─────────────────────────────
|
|
103
|
+
export function recordToRuntimeConfig(r) {
|
|
60
104
|
return {
|
|
61
|
-
controlPlaneEndpoint:
|
|
62
|
-
credentialId:
|
|
63
|
-
controllerToken:
|
|
105
|
+
controlPlaneEndpoint: r.endpoint,
|
|
106
|
+
credentialId: r.credential_id,
|
|
107
|
+
controllerToken: r.controller_token,
|
|
64
108
|
timeoutMs: 10_000,
|
|
65
109
|
};
|
|
66
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Resolve a runtime config for HTTP calls. If credentialId provided, look it up;
|
|
113
|
+
* else use default_credential_id; else throw.
|
|
114
|
+
*/
|
|
115
|
+
export function resolveConfig(credentialId) {
|
|
116
|
+
const cfg = loadConfig();
|
|
117
|
+
const id = credentialId ?? cfg.default_credential_id;
|
|
118
|
+
if (!id) {
|
|
119
|
+
throw new Error("No default device — run zhihand pair");
|
|
120
|
+
}
|
|
121
|
+
const r = cfg.devices[id];
|
|
122
|
+
if (!r) {
|
|
123
|
+
const known = Object.keys(cfg.devices).join(", ") || "(none)";
|
|
124
|
+
throw new Error(`Device '${id}' not found. Known: ${known}`);
|
|
125
|
+
}
|
|
126
|
+
return recordToRuntimeConfig(r);
|
|
127
|
+
}
|
|
128
|
+
// ── State / backend ───────────────────────────────────────
|
|
67
129
|
export function loadState() {
|
|
68
130
|
if (!fs.existsSync(STATE_PATH))
|
|
69
131
|
return null;
|
package/dist/core/device.d.ts
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Device
|
|
2
|
+
* Device profile extraction & formatting — stateless.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* through the zhihand_status tool and device://profile resource.
|
|
4
|
+
* Per-device state (profile, raw attributes, timestamps) lives in the
|
|
5
|
+
* device registry (see ./registry.ts). This module exposes pure helpers
|
|
6
|
+
* to extract, classify, and format device data so the same logic can be
|
|
7
|
+
* applied to any number of devices.
|
|
9
8
|
*/
|
|
10
|
-
import type {
|
|
9
|
+
import type { ZhiHandRuntimeConfig } from "./config.ts";
|
|
11
10
|
export interface StaticContext {
|
|
12
11
|
platform: string;
|
|
13
12
|
model: string;
|
|
@@ -35,14 +34,40 @@ export interface DynamicContext {
|
|
|
35
34
|
thermalState?: string;
|
|
36
35
|
fontScale: number;
|
|
37
36
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
export
|
|
37
|
+
declare const DEFAULT_STATIC: StaticContext;
|
|
38
|
+
declare const DEFAULT_DYNAMIC: DynamicContext;
|
|
39
|
+
export interface Capability {
|
|
40
|
+
ready: boolean;
|
|
41
|
+
reason: string;
|
|
42
|
+
}
|
|
43
|
+
export interface Capabilities {
|
|
44
|
+
screen_sharing: Capability;
|
|
45
|
+
hid: Capability;
|
|
46
|
+
live_session: Capability;
|
|
47
|
+
profile: {
|
|
48
|
+
age_ms: number;
|
|
49
|
+
stale: boolean;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export declare function computeCapabilities(rawAttributes: Record<string, unknown>, profileReceivedAtMs: number): Capabilities;
|
|
41
53
|
export declare function extractStatic(profile: Record<string, unknown>): StaticContext;
|
|
42
54
|
export declare function extractDynamic(profile: Record<string, unknown>): DynamicContext;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
export declare function
|
|
48
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Fetch and normalize the device profile from the control plane once.
|
|
57
|
+
* Returns null on failure (HTTP or network).
|
|
58
|
+
*/
|
|
59
|
+
export declare function fetchDeviceProfileOnce(config: ZhiHandRuntimeConfig): Promise<{
|
|
60
|
+
rawAttrs: Record<string, unknown>;
|
|
61
|
+
receivedAtMs: number;
|
|
62
|
+
} | null>;
|
|
63
|
+
/**
|
|
64
|
+
* Normalize an SSE device_profile.updated payload into rawAttrs shape.
|
|
65
|
+
*/
|
|
66
|
+
export declare function normalizeProfilePayload(raw: Record<string, unknown>): Record<string, unknown>;
|
|
67
|
+
export declare function pickAllowlistedRawAttributes(rawAttributes: Record<string, unknown>): Record<string, unknown>;
|
|
68
|
+
export { DEFAULT_STATIC, DEFAULT_DYNAMIC };
|
|
69
|
+
import type { DeviceState } from "./registry.ts";
|
|
70
|
+
export declare function buildControlToolDescription(state: DeviceState | null, onlineStates?: DeviceState[]): string;
|
|
71
|
+
export declare function buildSystemToolDescription(state: DeviceState | null, onlineStates?: DeviceState[]): string;
|
|
72
|
+
export declare function buildScreenshotToolDescription(state: DeviceState | null, onlineStates?: DeviceState[]): string;
|
|
73
|
+
export declare function formatDeviceStatus(state: DeviceState): Record<string, unknown>;
|