@zhihand/mcp 0.30.0 → 0.32.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 +316 -129
- package/dist/core/command.js +4 -3
- package/dist/core/config.d.ts +35 -20
- package/dist/core/config.js +129 -55
- package/dist/core/device.d.ts +3 -3
- package/dist/core/device.js +22 -14
- package/dist/core/logger.d.ts +17 -0
- package/dist/core/logger.js +32 -0
- package/dist/core/pair.d.ts +39 -31
- package/dist/core/pair.js +188 -84
- package/dist/core/registry.d.ts +23 -30
- package/dist/core/registry.js +321 -194
- package/dist/core/screenshot.js +3 -2
- package/dist/core/sse.d.ts +32 -7
- package/dist/core/sse.js +90 -22
- package/dist/core/ws.d.ts +92 -0
- package/dist/core/ws.js +327 -0
- package/dist/daemon/dispatcher.js +1 -1
- package/dist/daemon/heartbeat.js +1 -1
- package/dist/daemon/index.js +4 -4
- package/dist/daemon/prompt-listener.d.ts +5 -6
- package/dist/daemon/prompt-listener.js +58 -94
- package/dist/index.d.ts +1 -1
- package/dist/index.js +18 -16
- package/dist/tools/control.js +1 -1
- package/dist/tools/pair.d.ts +1 -1
- package/dist/tools/pair.js +22 -25
- package/dist/tools/system.js +1 -1
- package/package.json +3 -1
- package/README.md +0 -359
package/dist/core/command.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { log } from "./logger.js";
|
|
2
|
+
const dbg = (msg) => log.debug(msg);
|
|
2
3
|
let messageCounter = 0;
|
|
3
4
|
function nextMessageId() {
|
|
4
5
|
messageCounter = (messageCounter + 1) % 1000;
|
|
@@ -157,7 +158,7 @@ export async function enqueueCommand(config, command) {
|
|
|
157
158
|
method: "POST",
|
|
158
159
|
headers: {
|
|
159
160
|
"Content-Type": "application/json",
|
|
160
|
-
"
|
|
161
|
+
"Authorization": `Bearer ${config.controllerToken}`,
|
|
161
162
|
},
|
|
162
163
|
body: JSON.stringify(body),
|
|
163
164
|
});
|
|
@@ -173,7 +174,7 @@ export async function getCommand(config, commandId) {
|
|
|
173
174
|
const url = `${config.controlPlaneEndpoint}/v1/credentials/${encodeURIComponent(config.credentialId)}/commands/${encodeURIComponent(commandId)}`;
|
|
174
175
|
dbg(`[cmd] GET ${url}`);
|
|
175
176
|
const response = await fetch(url, {
|
|
176
|
-
headers: { "
|
|
177
|
+
headers: { "Authorization": `Bearer ${config.controllerToken}` },
|
|
177
178
|
});
|
|
178
179
|
if (!response.ok) {
|
|
179
180
|
dbg(`[cmd] Get failed: ${response.status}`);
|
package/dist/core/config.d.ts
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
export type DevicePlatform = "ios" | "android" | "unknown";
|
|
2
2
|
export interface DeviceRecord {
|
|
3
3
|
credential_id: string;
|
|
4
|
-
controller_token: string;
|
|
5
|
-
endpoint: string;
|
|
6
4
|
label: string;
|
|
7
5
|
platform: DevicePlatform;
|
|
8
6
|
paired_at: string;
|
|
9
7
|
last_seen_at: string;
|
|
10
8
|
}
|
|
11
|
-
export interface
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
export interface UserRecord {
|
|
10
|
+
user_id: string;
|
|
11
|
+
controller_token: string;
|
|
12
|
+
label: string;
|
|
13
|
+
created_at: string;
|
|
14
|
+
devices: DeviceRecord[];
|
|
15
|
+
}
|
|
16
|
+
export interface ZhihandConfigV3 {
|
|
17
|
+
schema_version: 3;
|
|
18
|
+
users: Record<string, UserRecord>;
|
|
15
19
|
}
|
|
16
20
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
21
|
+
* Runtime config passed to HTTP callers (command/sse/device endpoints).
|
|
22
|
+
* Derived from a UserRecord + DeviceRecord pair.
|
|
19
23
|
*/
|
|
20
24
|
export interface ZhiHandRuntimeConfig {
|
|
21
25
|
controlPlaneEndpoint: string;
|
|
@@ -32,19 +36,30 @@ export interface BackendConfig {
|
|
|
32
36
|
export declare const DEFAULT_MODELS: Record<Exclude<BackendName, "openclaw">, string>;
|
|
33
37
|
export declare function resolveZhiHandDir(): string;
|
|
34
38
|
export declare function ensureZhiHandDir(): void;
|
|
35
|
-
export declare function
|
|
36
|
-
export declare function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
export declare function
|
|
42
|
-
export declare function
|
|
43
|
-
export declare function
|
|
44
|
-
export declare function
|
|
39
|
+
export declare function getConfigPath(): string;
|
|
40
|
+
export declare function loadConfig(): ZhihandConfigV3;
|
|
41
|
+
/**
|
|
42
|
+
* Atomically write config: write to .tmp, then rename. Prevents corruption
|
|
43
|
+
* when the daemon and CLI write concurrently (Gemini code review v0.31).
|
|
44
|
+
*/
|
|
45
|
+
export declare function saveConfig(cfg: ZhihandConfigV3): void;
|
|
46
|
+
export declare function addUser(user: UserRecord): void;
|
|
47
|
+
export declare function removeUser(userId: string): void;
|
|
48
|
+
export declare function addDeviceToUser(userId: string, device: DeviceRecord): void;
|
|
49
|
+
export declare function removeDeviceFromUser(userId: string, credentialId: string): void;
|
|
50
|
+
export declare function updateDeviceLabel(userId: string, credentialId: string, label: string): void;
|
|
51
|
+
export declare function updateControllerToken(userId: string, newToken: string): void;
|
|
52
|
+
export declare function updateDeviceLastSeen(userId: string, credentialId: string, iso: string): void;
|
|
53
|
+
export declare function getUserRecord(userId: string): UserRecord | null;
|
|
54
|
+
export declare function findDeviceOwner(credentialId: string): {
|
|
55
|
+
user: UserRecord;
|
|
56
|
+
device: DeviceRecord;
|
|
57
|
+
} | null;
|
|
58
|
+
export declare function listUsers(): UserRecord[];
|
|
59
|
+
export declare function resolveDefaultEndpoint(): string;
|
|
45
60
|
/**
|
|
46
|
-
* Resolve a runtime config for HTTP calls.
|
|
47
|
-
*
|
|
61
|
+
* Resolve a runtime config for HTTP calls. Find which user owns the
|
|
62
|
+
* credential and use the user's controller_token.
|
|
48
63
|
*/
|
|
49
64
|
export declare function resolveConfig(credentialId?: string): ZhiHandRuntimeConfig;
|
|
50
65
|
export declare function loadState<T = unknown>(): T | null;
|
package/dist/core/config.js
CHANGED
|
@@ -9,7 +9,6 @@ export const DEFAULT_MODELS = {
|
|
|
9
9
|
// ── Paths ──────────────────────────────────────────────────
|
|
10
10
|
const ZHIHAND_DIR = path.join(os.homedir(), ".zhihand");
|
|
11
11
|
const CONFIG_PATH = path.join(ZHIHAND_DIR, "config.json");
|
|
12
|
-
const LEGACY_CREDENTIALS_PATH = path.join(ZHIHAND_DIR, "credentials.json");
|
|
13
12
|
const STATE_PATH = path.join(ZHIHAND_DIR, "state.json");
|
|
14
13
|
const BACKEND_PATH = path.join(ZHIHAND_DIR, "backend.json");
|
|
15
14
|
export function resolveZhiHandDir() {
|
|
@@ -18,112 +17,187 @@ export function resolveZhiHandDir() {
|
|
|
18
17
|
export function ensureZhiHandDir() {
|
|
19
18
|
fs.mkdirSync(ZHIHAND_DIR, { recursive: true, mode: 0o700 });
|
|
20
19
|
}
|
|
21
|
-
|
|
20
|
+
export function getConfigPath() {
|
|
21
|
+
return CONFIG_PATH;
|
|
22
|
+
}
|
|
23
|
+
// ── v3 config I/O ──────────────────────────────────────────
|
|
22
24
|
let legacyWarningPrinted = false;
|
|
23
25
|
function emptyConfig() {
|
|
24
|
-
return { schema_version:
|
|
26
|
+
return { schema_version: 3, users: {} };
|
|
25
27
|
}
|
|
26
28
|
export function loadConfig() {
|
|
27
29
|
if (!fs.existsSync(CONFIG_PATH)) {
|
|
28
|
-
|
|
30
|
+
// Check for v2 or legacy credentials
|
|
31
|
+
const legacyCredentials = path.join(ZHIHAND_DIR, "credentials.json");
|
|
32
|
+
if (!legacyWarningPrinted && (fs.existsSync(legacyCredentials) || checkForV2Config())) {
|
|
29
33
|
legacyWarningPrinted = true;
|
|
30
|
-
process.stderr.write("[zhihand]
|
|
34
|
+
process.stderr.write("[zhihand] old config detected (v2 or legacy) — run 'zhihand pair' to re-pair on v0.31 schema\n");
|
|
31
35
|
}
|
|
32
36
|
return emptyConfig();
|
|
33
37
|
}
|
|
34
38
|
try {
|
|
35
39
|
const raw = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8"));
|
|
36
|
-
if (raw && raw.schema_version ===
|
|
40
|
+
if (raw && raw.schema_version === 3) {
|
|
37
41
|
return {
|
|
38
|
-
schema_version:
|
|
39
|
-
|
|
40
|
-
devices: raw.devices ?? {},
|
|
42
|
+
schema_version: 3,
|
|
43
|
+
users: raw.users ?? {},
|
|
41
44
|
};
|
|
42
45
|
}
|
|
46
|
+
// Old schema version detected
|
|
47
|
+
if (!legacyWarningPrinted) {
|
|
48
|
+
legacyWarningPrinted = true;
|
|
49
|
+
process.stderr.write("[zhihand] old config detected (schema v" + (raw.schema_version ?? "?") + ") — run 'zhihand pair' to re-pair on v0.31 schema\n");
|
|
50
|
+
}
|
|
43
51
|
}
|
|
44
52
|
catch {
|
|
45
53
|
// fall through
|
|
46
54
|
}
|
|
47
55
|
return emptyConfig();
|
|
48
56
|
}
|
|
57
|
+
function checkForV2Config() {
|
|
58
|
+
if (!fs.existsSync(CONFIG_PATH))
|
|
59
|
+
return false;
|
|
60
|
+
try {
|
|
61
|
+
const raw = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8"));
|
|
62
|
+
return raw && raw.schema_version === 2;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Atomically write config: write to .tmp, then rename. Prevents corruption
|
|
70
|
+
* when the daemon and CLI write concurrently (Gemini code review v0.31).
|
|
71
|
+
*/
|
|
49
72
|
export function saveConfig(cfg) {
|
|
50
73
|
ensureZhiHandDir();
|
|
51
|
-
|
|
74
|
+
const tmpPath = CONFIG_PATH + ".tmp";
|
|
75
|
+
fs.writeFileSync(tmpPath, JSON.stringify(cfg, null, 2), { mode: 0o600 });
|
|
76
|
+
fs.renameSync(tmpPath, CONFIG_PATH);
|
|
52
77
|
}
|
|
53
|
-
|
|
78
|
+
// ── User helpers ──────────────────────────────────────────
|
|
79
|
+
export function addUser(user) {
|
|
54
80
|
const cfg = loadConfig();
|
|
55
|
-
cfg.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
81
|
+
cfg.users[user.user_id] = user;
|
|
82
|
+
saveConfig(cfg);
|
|
83
|
+
}
|
|
84
|
+
export function removeUser(userId) {
|
|
85
|
+
const cfg = loadConfig();
|
|
86
|
+
delete cfg.users[userId];
|
|
59
87
|
saveConfig(cfg);
|
|
60
88
|
}
|
|
61
|
-
export function
|
|
89
|
+
export function addDeviceToUser(userId, device) {
|
|
62
90
|
const cfg = loadConfig();
|
|
63
|
-
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
|
|
91
|
+
const user = cfg.users[userId];
|
|
92
|
+
if (!user)
|
|
93
|
+
throw new Error(`User '${userId}' not found in config`);
|
|
94
|
+
// Replace if exists, else append
|
|
95
|
+
const idx = user.devices.findIndex((d) => d.credential_id === device.credential_id);
|
|
96
|
+
if (idx >= 0) {
|
|
97
|
+
user.devices[idx] = device;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
user.devices.push(device);
|
|
67
101
|
}
|
|
68
102
|
saveConfig(cfg);
|
|
69
103
|
}
|
|
70
|
-
export function
|
|
104
|
+
export function removeDeviceFromUser(userId, credentialId) {
|
|
71
105
|
const cfg = loadConfig();
|
|
72
|
-
const
|
|
73
|
-
if (!
|
|
74
|
-
|
|
75
|
-
|
|
106
|
+
const user = cfg.users[userId];
|
|
107
|
+
if (!user)
|
|
108
|
+
return;
|
|
109
|
+
user.devices = user.devices.filter((d) => d.credential_id !== credentialId);
|
|
76
110
|
saveConfig(cfg);
|
|
77
111
|
}
|
|
78
|
-
export function
|
|
112
|
+
export function updateDeviceLabel(userId, credentialId, label) {
|
|
79
113
|
const cfg = loadConfig();
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
114
|
+
const user = cfg.users[userId];
|
|
115
|
+
if (!user)
|
|
116
|
+
throw new Error(`User '${userId}' not found`);
|
|
117
|
+
const dev = user.devices.find((d) => d.credential_id === credentialId);
|
|
118
|
+
if (!dev)
|
|
119
|
+
throw new Error(`Device '${credentialId}' not found under user '${userId}'`);
|
|
120
|
+
dev.label = label;
|
|
121
|
+
saveConfig(cfg);
|
|
122
|
+
}
|
|
123
|
+
export function updateControllerToken(userId, newToken) {
|
|
124
|
+
const cfg = loadConfig();
|
|
125
|
+
const user = cfg.users[userId];
|
|
126
|
+
if (!user)
|
|
127
|
+
throw new Error(`User '${userId}' not found`);
|
|
128
|
+
user.controller_token = newToken;
|
|
84
129
|
saveConfig(cfg);
|
|
85
130
|
}
|
|
86
|
-
export function
|
|
131
|
+
export function updateDeviceLastSeen(userId, credentialId, iso) {
|
|
87
132
|
const cfg = loadConfig();
|
|
88
|
-
const
|
|
89
|
-
if (!
|
|
133
|
+
const user = cfg.users[userId];
|
|
134
|
+
if (!user)
|
|
90
135
|
return;
|
|
91
|
-
|
|
136
|
+
const dev = user.devices.find((d) => d.credential_id === credentialId);
|
|
137
|
+
if (!dev)
|
|
138
|
+
return;
|
|
139
|
+
dev.last_seen_at = iso;
|
|
92
140
|
saveConfig(cfg);
|
|
93
141
|
}
|
|
94
|
-
export function
|
|
142
|
+
export function getUserRecord(userId) {
|
|
95
143
|
const cfg = loadConfig();
|
|
96
|
-
return cfg.
|
|
144
|
+
return cfg.users[userId] ?? null;
|
|
97
145
|
}
|
|
98
|
-
export function
|
|
146
|
+
export function findDeviceOwner(credentialId) {
|
|
99
147
|
const cfg = loadConfig();
|
|
100
|
-
|
|
148
|
+
for (const user of Object.values(cfg.users)) {
|
|
149
|
+
const device = user.devices.find((d) => d.credential_id === credentialId);
|
|
150
|
+
if (device)
|
|
151
|
+
return { user, device };
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
export function listUsers() {
|
|
156
|
+
const cfg = loadConfig();
|
|
157
|
+
return Object.values(cfg.users);
|
|
101
158
|
}
|
|
102
159
|
// ── Runtime config resolution ─────────────────────────────
|
|
103
|
-
export function
|
|
104
|
-
return
|
|
105
|
-
controlPlaneEndpoint: r.endpoint,
|
|
106
|
-
credentialId: r.credential_id,
|
|
107
|
-
controllerToken: r.controller_token,
|
|
108
|
-
timeoutMs: 10_000,
|
|
109
|
-
};
|
|
160
|
+
export function resolveDefaultEndpoint() {
|
|
161
|
+
return process.env.ZHIHAND_ENDPOINT ?? "https://api.zhihand.com";
|
|
110
162
|
}
|
|
111
163
|
/**
|
|
112
|
-
* Resolve a runtime config for HTTP calls.
|
|
113
|
-
*
|
|
164
|
+
* Resolve a runtime config for HTTP calls. Find which user owns the
|
|
165
|
+
* credential and use the user's controller_token.
|
|
114
166
|
*/
|
|
115
167
|
export function resolveConfig(credentialId) {
|
|
116
168
|
const cfg = loadConfig();
|
|
117
|
-
const
|
|
118
|
-
if (
|
|
119
|
-
|
|
169
|
+
const endpoint = resolveDefaultEndpoint();
|
|
170
|
+
if (credentialId) {
|
|
171
|
+
const owner = findDeviceOwner(credentialId);
|
|
172
|
+
if (!owner) {
|
|
173
|
+
const known = Object.values(cfg.users)
|
|
174
|
+
.flatMap((u) => u.devices.map((d) => d.credential_id))
|
|
175
|
+
.join(", ") || "(none)";
|
|
176
|
+
throw new Error(`Device '${credentialId}' not found. Known: ${known}`);
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
controlPlaneEndpoint: endpoint,
|
|
180
|
+
credentialId,
|
|
181
|
+
controllerToken: owner.user.controller_token,
|
|
182
|
+
timeoutMs: 10_000,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
// No explicit credential — pick first device of first user
|
|
186
|
+
const users = Object.values(cfg.users);
|
|
187
|
+
if (users.length === 0) {
|
|
188
|
+
throw new Error("No users configured — run zhihand pair");
|
|
120
189
|
}
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
190
|
+
for (const user of users) {
|
|
191
|
+
if (user.devices.length > 0) {
|
|
192
|
+
return {
|
|
193
|
+
controlPlaneEndpoint: endpoint,
|
|
194
|
+
credentialId: user.devices[0].credential_id,
|
|
195
|
+
controllerToken: user.controller_token,
|
|
196
|
+
timeoutMs: 10_000,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
125
199
|
}
|
|
126
|
-
|
|
200
|
+
throw new Error("No devices configured — run zhihand pair");
|
|
127
201
|
}
|
|
128
202
|
// ── State / backend ───────────────────────────────────────
|
|
129
203
|
export function loadState() {
|
package/dist/core/device.d.ts
CHANGED
|
@@ -67,7 +67,7 @@ export declare function normalizeProfilePayload(raw: Record<string, unknown>): R
|
|
|
67
67
|
export declare function pickAllowlistedRawAttributes(rawAttributes: Record<string, unknown>): Record<string, unknown>;
|
|
68
68
|
export { DEFAULT_STATIC, DEFAULT_DYNAMIC };
|
|
69
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;
|
|
70
|
+
export declare function buildControlToolDescription(state: DeviceState | null, onlineStates?: DeviceState[], multiUser?: boolean): string;
|
|
71
|
+
export declare function buildSystemToolDescription(state: DeviceState | null, onlineStates?: DeviceState[], multiUser?: boolean): string;
|
|
72
|
+
export declare function buildScreenshotToolDescription(state: DeviceState | null, onlineStates?: DeviceState[], multiUser?: boolean): string;
|
|
73
73
|
export declare function formatDeviceStatus(state: DeviceState): Record<string, unknown>;
|
package/dist/core/device.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* to extract, classify, and format device data so the same logic can be
|
|
7
7
|
* applied to any number of devices.
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
9
|
+
import { log } from "./logger.js";
|
|
10
10
|
const DEFAULT_STATIC = {
|
|
11
11
|
platform: "unknown",
|
|
12
12
|
model: "unknown",
|
|
@@ -136,14 +136,14 @@ export function extractDynamic(profile) {
|
|
|
136
136
|
*/
|
|
137
137
|
export async function fetchDeviceProfileOnce(config) {
|
|
138
138
|
const url = `${config.controlPlaneEndpoint}/v1/credentials/${encodeURIComponent(config.credentialId)}/device-profile`;
|
|
139
|
-
|
|
139
|
+
log.debug(`[device] Fetching profile: GET ${url}`);
|
|
140
140
|
try {
|
|
141
141
|
const response = await fetch(url, {
|
|
142
|
-
headers: { "
|
|
142
|
+
headers: { "Authorization": `Bearer ${config.controllerToken}` },
|
|
143
143
|
signal: AbortSignal.timeout(10_000),
|
|
144
144
|
});
|
|
145
145
|
if (!response.ok) {
|
|
146
|
-
|
|
146
|
+
log.debug(`[device] Profile fetch failed: ${response.status} ${response.statusText}`);
|
|
147
147
|
return null;
|
|
148
148
|
}
|
|
149
149
|
const data = (await response.json());
|
|
@@ -157,7 +157,7 @@ export async function fetchDeviceProfileOnce(config) {
|
|
|
157
157
|
return { rawAttrs, receivedAtMs: Date.now() };
|
|
158
158
|
}
|
|
159
159
|
catch (err) {
|
|
160
|
-
|
|
160
|
+
log.debug(`[device] Profile fetch error: ${err.message}`);
|
|
161
161
|
return null;
|
|
162
162
|
}
|
|
163
163
|
}
|
|
@@ -200,6 +200,9 @@ export function pickAllowlistedRawAttributes(rawAttributes) {
|
|
|
200
200
|
}
|
|
201
201
|
// ── Default static/dynamic export for empty-state rendering ──
|
|
202
202
|
export { DEFAULT_STATIC, DEFAULT_DYNAMIC };
|
|
203
|
+
function formatDeviceLabel(d, multiUser) {
|
|
204
|
+
return multiUser ? `[${d.userLabel}] ${d.label}` : d.label;
|
|
205
|
+
}
|
|
203
206
|
function singleDeviceOpenAppGuidance(platform) {
|
|
204
207
|
if (platform === "android") {
|
|
205
208
|
return " For open_app, use appPackage (e.g. 'com.tencent.mm'). Do NOT send bundleId or urlScheme.";
|
|
@@ -209,7 +212,7 @@ function singleDeviceOpenAppGuidance(platform) {
|
|
|
209
212
|
}
|
|
210
213
|
return "";
|
|
211
214
|
}
|
|
212
|
-
export function buildControlToolDescription(state, onlineStates) {
|
|
215
|
+
export function buildControlToolDescription(state, onlineStates, multiUser) {
|
|
213
216
|
const baseGeneric = "Control the connected mobile device. Supports click, swipe, type, scroll, open_app, back, home, and more. All coordinates use normalized ratios [0,1]. Call zhihand_list_devices to see online devices, then pass device_id.";
|
|
214
217
|
if (onlineStates) {
|
|
215
218
|
if (onlineStates.length === 0) {
|
|
@@ -218,8 +221,9 @@ export function buildControlToolDescription(state, onlineStates) {
|
|
|
218
221
|
if (onlineStates.length === 1) {
|
|
219
222
|
const s = onlineStates[0];
|
|
220
223
|
const ctx = s.profile;
|
|
224
|
+
const label = formatDeviceLabel(s, multiUser ?? false);
|
|
221
225
|
if (!ctx || ctx.platform === "unknown") {
|
|
222
|
-
return `Control the connected mobile device (${
|
|
226
|
+
return `Control the connected mobile device (${label}). device_id is optional (single device online). All coordinates use normalized ratios [0,1].`;
|
|
223
227
|
}
|
|
224
228
|
const parts = [
|
|
225
229
|
`Control a ${ctx.platform} device`,
|
|
@@ -233,7 +237,7 @@ export function buildControlToolDescription(state, onlineStates) {
|
|
|
233
237
|
return desc;
|
|
234
238
|
}
|
|
235
239
|
// 2+ devices
|
|
236
|
-
const ids = onlineStates.map((d) => `${d.credentialId} (${d
|
|
240
|
+
const ids = onlineStates.map((d) => `${d.credentialId} (${formatDeviceLabel(d, multiUser ?? false)}, ${d.platform})`).join("; ");
|
|
237
241
|
return `Control a mobile device. device_id is REQUIRED (multiple online). Online devices: ${ids}. Call zhihand_list_devices first. All coordinates use normalized ratios [0,1].`;
|
|
238
242
|
}
|
|
239
243
|
// No explicit onlineStates: describe single state or generic
|
|
@@ -252,7 +256,7 @@ export function buildControlToolDescription(state, onlineStates) {
|
|
|
252
256
|
desc += singleDeviceOpenAppGuidance(ctx.platform);
|
|
253
257
|
return desc;
|
|
254
258
|
}
|
|
255
|
-
export function buildSystemToolDescription(state, onlineStates) {
|
|
259
|
+
export function buildSystemToolDescription(state, onlineStates, multiUser) {
|
|
256
260
|
const genericBase = "System navigation and media controls. Actions: notification, recent, search, switch_input, siri (iOS), control_center (iOS), open_browser (Android), shortcut_help (Android), volume_up/down, mute, play_pause, stop, next/prev_track, fast_forward, rewind, brightness_up/down, power.";
|
|
257
261
|
if (onlineStates) {
|
|
258
262
|
if (onlineStates.length === 0) {
|
|
@@ -261,8 +265,9 @@ export function buildSystemToolDescription(state, onlineStates) {
|
|
|
261
265
|
if (onlineStates.length === 1) {
|
|
262
266
|
const s = onlineStates[0];
|
|
263
267
|
const platform = s.profile?.platform ?? s.platform;
|
|
268
|
+
const label = formatDeviceLabel(s, multiUser ?? false);
|
|
264
269
|
const parts = [
|
|
265
|
-
`System navigation and media controls for ${platform} device (${s.profile?.model ??
|
|
270
|
+
`System navigation and media controls for ${platform} device (${s.profile?.model ?? label}). device_id is optional (single device online).`,
|
|
266
271
|
];
|
|
267
272
|
parts.push("Navigation: notification, recent, search (optional text query), switch_input.");
|
|
268
273
|
if (platform === "ios")
|
|
@@ -273,7 +278,7 @@ export function buildSystemToolDescription(state, onlineStates) {
|
|
|
273
278
|
parts.push("Hardware: brightness_up, brightness_down, power.");
|
|
274
279
|
return parts.join(" ");
|
|
275
280
|
}
|
|
276
|
-
const ids = onlineStates.map((d) => `${d.credentialId} (${d
|
|
281
|
+
const ids = onlineStates.map((d) => `${d.credentialId} (${formatDeviceLabel(d, multiUser ?? false)}, ${d.platform})`).join("; ");
|
|
277
282
|
return `System navigation and media controls for mobile device. device_id is REQUIRED (multiple online). Online: ${ids}. ` + genericBase;
|
|
278
283
|
}
|
|
279
284
|
if (!state || !state.profile || state.profile.platform === "unknown") {
|
|
@@ -292,7 +297,7 @@ export function buildSystemToolDescription(state, onlineStates) {
|
|
|
292
297
|
parts.push("Hardware: brightness_up, brightness_down, power.");
|
|
293
298
|
return parts.join(" ");
|
|
294
299
|
}
|
|
295
|
-
export function buildScreenshotToolDescription(state, onlineStates) {
|
|
300
|
+
export function buildScreenshotToolDescription(state, onlineStates, multiUser) {
|
|
296
301
|
if (onlineStates) {
|
|
297
302
|
if (onlineStates.length === 0) {
|
|
298
303
|
return "Take a screenshot of the phone screen. No devices online — ask user to open the ZhiHand app.";
|
|
@@ -300,12 +305,13 @@ export function buildScreenshotToolDescription(state, onlineStates) {
|
|
|
300
305
|
if (onlineStates.length === 1) {
|
|
301
306
|
const s = onlineStates[0];
|
|
302
307
|
const ctx = s.profile;
|
|
308
|
+
const label = formatDeviceLabel(s, multiUser ?? false);
|
|
303
309
|
if (!ctx || ctx.platform === "unknown") {
|
|
304
|
-
return `Take a screenshot of the phone screen (${
|
|
310
|
+
return `Take a screenshot of the phone screen (${label}). device_id is optional (single device online).`;
|
|
305
311
|
}
|
|
306
312
|
return `Take a screenshot of the ${ctx.platform} device (${ctx.model}, ${ctx.screenWidthPx}x${ctx.screenHeightPx}). device_id is optional (single device online).`;
|
|
307
313
|
}
|
|
308
|
-
const ids = onlineStates.map((d) => `${d.credentialId} (${d
|
|
314
|
+
const ids = onlineStates.map((d) => `${d.credentialId} (${formatDeviceLabel(d, multiUser ?? false)})`).join("; ");
|
|
309
315
|
return `Take a screenshot of a mobile device. device_id is REQUIRED (multiple online). Online: ${ids}.`;
|
|
310
316
|
}
|
|
311
317
|
if (!state || !state.profile || state.profile.platform === "unknown") {
|
|
@@ -324,6 +330,8 @@ export function formatDeviceStatus(state) {
|
|
|
324
330
|
return {
|
|
325
331
|
credential_id: state.credentialId,
|
|
326
332
|
label: state.label,
|
|
333
|
+
user_id: state.userId,
|
|
334
|
+
user_label: state.userLabel,
|
|
327
335
|
online: state.online,
|
|
328
336
|
platform: staticCtx.platform,
|
|
329
337
|
model: staticCtx.model,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified logger — all log output goes to stderr so stdout stays clean
|
|
3
|
+
* for MCP JSON-RPC. Replaces ad-hoc process.stderr.write and dbg() calls
|
|
4
|
+
* in core/ and tools/ code.
|
|
5
|
+
*
|
|
6
|
+
* The daemon has its own stdout-based log() in daemon/index.ts — that is
|
|
7
|
+
* intentional (it writes to daemon.log). The daemon's debug logger
|
|
8
|
+
* (daemon/logger.ts) remains for daemon-specific verbose output.
|
|
9
|
+
*/
|
|
10
|
+
export declare const log: {
|
|
11
|
+
info: (...args: unknown[]) => void;
|
|
12
|
+
warn: (...args: unknown[]) => void;
|
|
13
|
+
error: (...args: unknown[]) => void;
|
|
14
|
+
debug: (...args: unknown[]) => void;
|
|
15
|
+
};
|
|
16
|
+
export declare function setDebugEnabled(v: boolean): void;
|
|
17
|
+
export declare function isDebugEnabled(): boolean;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified logger — all log output goes to stderr so stdout stays clean
|
|
3
|
+
* for MCP JSON-RPC. Replaces ad-hoc process.stderr.write and dbg() calls
|
|
4
|
+
* in core/ and tools/ code.
|
|
5
|
+
*
|
|
6
|
+
* The daemon has its own stdout-based log() in daemon/index.ts — that is
|
|
7
|
+
* intentional (it writes to daemon.log). The daemon's debug logger
|
|
8
|
+
* (daemon/logger.ts) remains for daemon-specific verbose output.
|
|
9
|
+
*/
|
|
10
|
+
let debugEnabled = false;
|
|
11
|
+
export const log = {
|
|
12
|
+
info: (...args) => {
|
|
13
|
+
process.stderr.write(`[info] ${args.map(String).join(" ")}\n`);
|
|
14
|
+
},
|
|
15
|
+
warn: (...args) => {
|
|
16
|
+
process.stderr.write(`[warn] ${args.map(String).join(" ")}\n`);
|
|
17
|
+
},
|
|
18
|
+
error: (...args) => {
|
|
19
|
+
process.stderr.write(`[error] ${args.map(String).join(" ")}\n`);
|
|
20
|
+
},
|
|
21
|
+
debug: (...args) => {
|
|
22
|
+
if (debugEnabled) {
|
|
23
|
+
process.stderr.write(`[debug] ${args.map(String).join(" ")}\n`);
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
export function setDebugEnabled(v) {
|
|
28
|
+
debugEnabled = v;
|
|
29
|
+
}
|
|
30
|
+
export function isDebugEnabled() {
|
|
31
|
+
return debugEnabled;
|
|
32
|
+
}
|
package/dist/core/pair.d.ts
CHANGED
|
@@ -1,45 +1,53 @@
|
|
|
1
|
-
import type { DeviceRecord } from "./config.ts";
|
|
2
|
-
export interface PluginRecord {
|
|
3
|
-
id: string;
|
|
4
|
-
edge_id: string;
|
|
5
|
-
adapter_kind: string;
|
|
6
|
-
display_name?: string;
|
|
7
|
-
stable_identity?: string;
|
|
8
|
-
status: string;
|
|
9
|
-
created_at: string;
|
|
10
|
-
}
|
|
1
|
+
import type { DeviceRecord, UserRecord } from "./config.ts";
|
|
11
2
|
export interface PairingSession {
|
|
12
|
-
|
|
3
|
+
session_id: string;
|
|
13
4
|
pair_url: string;
|
|
14
5
|
qr_payload: string;
|
|
15
|
-
controller_token?: string;
|
|
16
|
-
edge_id: string;
|
|
17
|
-
status: "pending" | "claimed" | "expired" | string;
|
|
18
|
-
credential_id?: string;
|
|
19
6
|
expires_at: string;
|
|
20
|
-
requested_scopes?: string[];
|
|
21
7
|
}
|
|
22
|
-
export interface
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
8
|
+
export interface CreateUserResponse {
|
|
9
|
+
user_id: string;
|
|
10
|
+
controller_token: string;
|
|
11
|
+
label: string;
|
|
12
|
+
created_at: string;
|
|
26
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Create a new user on the server.
|
|
16
|
+
* POST /v1/users { label } → { user_id, controller_token, label, created_at }
|
|
17
|
+
*/
|
|
18
|
+
export declare function createUser(endpoint: string, label: string): Promise<CreateUserResponse>;
|
|
19
|
+
/**
|
|
20
|
+
* Create a pairing session for a user.
|
|
21
|
+
* POST /v1/users/{id}/pairing/sessions { edge_id, ttl_seconds } → PairingSession
|
|
22
|
+
*/
|
|
23
|
+
export declare function createPairingSession(endpoint: string, userId: string, controllerToken: string, edgeId: string, ttlSeconds?: number): Promise<PairingSession>;
|
|
24
|
+
/**
|
|
25
|
+
* Register a plugin (edge). Kept for backward compat with edge registration.
|
|
26
|
+
*/
|
|
27
27
|
export declare function registerPlugin(endpoint: string, options: {
|
|
28
28
|
stableIdentity: string;
|
|
29
29
|
displayName?: string;
|
|
30
30
|
adapterKind?: string;
|
|
31
|
-
}): Promise<
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
}): Promise<{
|
|
32
|
+
edge_id: string;
|
|
33
|
+
}>;
|
|
34
|
+
/**
|
|
35
|
+
* Poll pairing session until claimed or expired.
|
|
36
|
+
*/
|
|
37
|
+
export declare function waitForPairingClaim(endpoint: string, userId: string, controllerToken: string, sessionId: string, timeoutMs?: number): Promise<void>;
|
|
35
38
|
export declare function renderPairingQRCode(url: string): Promise<string>;
|
|
36
39
|
/**
|
|
37
|
-
*
|
|
38
|
-
* v2 config on success. Label defaults to the device model (fetched post-claim)
|
|
39
|
-
* and falls back to the supplied preferredLabel or timestamp.
|
|
40
|
+
* New user pairing: create user → create pairing session → wait → fetch credentials → save config.
|
|
40
41
|
*/
|
|
41
|
-
export declare function
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
export declare function executePairingNewUser(preferredLabel?: string): Promise<{
|
|
43
|
+
userRecord: UserRecord;
|
|
44
|
+
deviceRecord: DeviceRecord;
|
|
44
45
|
}>;
|
|
45
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Add device to existing user: create pairing session → wait → fetch new credential → save.
|
|
48
|
+
*/
|
|
49
|
+
export declare function executePairingAddDevice(userId: string, preferredLabel?: string): Promise<DeviceRecord>;
|
|
50
|
+
/**
|
|
51
|
+
* Legacy: format pairing status (kept for backward compat).
|
|
52
|
+
*/
|
|
53
|
+
export declare function formatPairingStatus(userId: string | null): string;
|