@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/pair.js
CHANGED
|
@@ -1,62 +1,78 @@
|
|
|
1
1
|
import QRCode from "qrcode";
|
|
2
|
-
import {
|
|
2
|
+
import { addUser, addDeviceToUser, ensureZhiHandDir, saveState, resolveDefaultEndpoint, getUserRecord, } from "./config.js";
|
|
3
3
|
import { fetchDeviceProfileOnce, extractStatic } from "./device.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const response = await fetch(`${endpoint}/v1/plugins`, {
|
|
4
|
+
import { fetchUserCredentials } from "./ws.js";
|
|
5
|
+
// ── Server API helpers ─────────────────────────────────────
|
|
6
|
+
/**
|
|
7
|
+
* Create a new user on the server.
|
|
8
|
+
* POST /v1/users { label } → { user_id, controller_token, label, created_at }
|
|
9
|
+
*/
|
|
10
|
+
export async function createUser(endpoint, label) {
|
|
11
|
+
const response = await fetch(`${endpoint}/v1/users`, {
|
|
13
12
|
method: "POST",
|
|
14
13
|
headers: { "Content-Type": "application/json" },
|
|
15
|
-
body: JSON.stringify({
|
|
16
|
-
adapter_kind: options.adapterKind ?? "mcp",
|
|
17
|
-
display_name: options.displayName ?? "ZhiHand MCP Server",
|
|
18
|
-
stable_identity: options.stableIdentity,
|
|
19
|
-
}),
|
|
14
|
+
body: JSON.stringify({ label }),
|
|
20
15
|
});
|
|
21
16
|
if (!response.ok) {
|
|
22
|
-
throw new Error(`
|
|
17
|
+
throw new Error(`Create user failed: ${response.status} ${await response.text()}`);
|
|
23
18
|
}
|
|
24
|
-
|
|
25
|
-
return payload.plugin;
|
|
19
|
+
return (await response.json());
|
|
26
20
|
}
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Create a pairing session for a user.
|
|
23
|
+
* POST /v1/users/{id}/pairing/sessions { edge_id, ttl_seconds } → PairingSession
|
|
24
|
+
*/
|
|
25
|
+
export async function createPairingSession(endpoint, userId, controllerToken, edgeId, ttlSeconds = 300) {
|
|
26
|
+
const response = await fetch(`${endpoint}/v1/users/${encodeURIComponent(userId)}/pairing/sessions`, {
|
|
29
27
|
method: "POST",
|
|
30
|
-
headers: {
|
|
28
|
+
headers: {
|
|
29
|
+
"Content-Type": "application/json",
|
|
30
|
+
"Authorization": `Bearer ${controllerToken}`,
|
|
31
|
+
},
|
|
31
32
|
body: JSON.stringify({
|
|
32
|
-
edge_id:
|
|
33
|
-
ttl_seconds:
|
|
34
|
-
requested_scopes:
|
|
33
|
+
edge_id: edgeId,
|
|
34
|
+
ttl_seconds: ttlSeconds,
|
|
35
|
+
requested_scopes: ["observe", "session.control", "screen.read", "screen.capture", "ble.control"],
|
|
35
36
|
}),
|
|
36
37
|
});
|
|
37
38
|
if (!response.ok) {
|
|
38
39
|
throw new Error(`Create pairing session failed: ${response.status}`);
|
|
39
40
|
}
|
|
40
41
|
const payload = (await response.json());
|
|
41
|
-
return
|
|
42
|
-
...payload.session,
|
|
43
|
-
controller_token: payload.controller_token ?? payload.session.controller_token,
|
|
44
|
-
};
|
|
42
|
+
return payload;
|
|
45
43
|
}
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Register a plugin (edge). Kept for backward compat with edge registration.
|
|
46
|
+
*/
|
|
47
|
+
export async function registerPlugin(endpoint, options) {
|
|
48
|
+
const response = await fetch(`${endpoint}/v1/plugins`, {
|
|
49
|
+
method: "POST",
|
|
50
|
+
headers: { "Content-Type": "application/json" },
|
|
51
|
+
body: JSON.stringify({
|
|
52
|
+
adapter_kind: options.adapterKind ?? "mcp",
|
|
53
|
+
display_name: options.displayName ?? "ZhiHand MCP Server",
|
|
54
|
+
stable_identity: options.stableIdentity,
|
|
55
|
+
}),
|
|
56
|
+
});
|
|
48
57
|
if (!response.ok) {
|
|
49
|
-
throw new Error(`
|
|
58
|
+
throw new Error(`Register plugin failed: ${response.status} ${await response.text()}`);
|
|
50
59
|
}
|
|
51
60
|
const payload = (await response.json());
|
|
52
|
-
return payload.
|
|
61
|
+
return { edge_id: payload.plugin.edge_id };
|
|
53
62
|
}
|
|
54
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Poll pairing session until claimed or expired.
|
|
65
|
+
*/
|
|
66
|
+
export async function waitForPairingClaim(endpoint, userId, controllerToken, sessionId, timeoutMs = 600_000) {
|
|
55
67
|
const deadline = Date.now() + timeoutMs;
|
|
56
68
|
while (Date.now() < deadline) {
|
|
57
|
-
const
|
|
58
|
-
if (
|
|
59
|
-
|
|
69
|
+
const response = await fetch(`${endpoint}/v1/users/${encodeURIComponent(userId)}/pairing/sessions/${encodeURIComponent(sessionId)}`, { headers: { "Authorization": `Bearer ${controllerToken}` } });
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
throw new Error(`Get pairing session failed: ${response.status}`);
|
|
72
|
+
}
|
|
73
|
+
const session = (await response.json());
|
|
74
|
+
if (session.status === "claimed") {
|
|
75
|
+
return;
|
|
60
76
|
}
|
|
61
77
|
if (session.status === "expired") {
|
|
62
78
|
throw new Error("Pairing session expired.");
|
|
@@ -68,49 +84,60 @@ export async function waitForPairingClaim(endpoint, sessionId, timeoutMs = 600_0
|
|
|
68
84
|
export async function renderPairingQRCode(url) {
|
|
69
85
|
return QRCode.toString(url, { type: "utf8", margin: 2 });
|
|
70
86
|
}
|
|
87
|
+
// ── Pairing flows ──────────────────────────────────────────
|
|
71
88
|
/**
|
|
72
|
-
*
|
|
73
|
-
* v2 config on success. Label defaults to the device model (fetched post-claim)
|
|
74
|
-
* and falls back to the supplied preferredLabel or timestamp.
|
|
89
|
+
* New user pairing: create user → create pairing session → wait → fetch credentials → save config.
|
|
75
90
|
*/
|
|
76
|
-
export async function
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
const
|
|
91
|
+
export async function executePairingNewUser(preferredLabel) {
|
|
92
|
+
const endpoint = resolveDefaultEndpoint();
|
|
93
|
+
const label = preferredLabel ?? `User-${Date.now().toString(36)}`;
|
|
94
|
+
// 1. Create user
|
|
95
|
+
const userResp = await createUser(endpoint, label);
|
|
96
|
+
const userId = userResp.user_id;
|
|
97
|
+
const controllerToken = userResp.controller_token;
|
|
98
|
+
// 2. Register plugin (get edge_id)
|
|
99
|
+
const stableIdentity = `mcp-${Date.now().toString(36)}`;
|
|
100
|
+
const plugin = await registerPlugin(endpoint, { stableIdentity });
|
|
101
|
+
const edgeId = plugin.edge_id;
|
|
102
|
+
// 3. Create pairing session
|
|
103
|
+
const session = await createPairingSession(endpoint, userId, controllerToken, edgeId, 300);
|
|
83
104
|
saveState({
|
|
84
|
-
sessionId: session.
|
|
85
|
-
|
|
86
|
-
|
|
105
|
+
sessionId: session.session_id,
|
|
106
|
+
userId,
|
|
107
|
+
controllerToken,
|
|
108
|
+
edgeId,
|
|
87
109
|
pairUrl: session.pair_url,
|
|
88
110
|
status: "pending",
|
|
89
111
|
expiresAt: session.expires_at,
|
|
90
112
|
});
|
|
113
|
+
// 4. Show QR + wait
|
|
91
114
|
const qr = await renderPairingQRCode(session.pair_url);
|
|
92
115
|
console.log(qr);
|
|
93
116
|
console.log(`Open this URL on your phone to pair:\n ${session.pair_url}\n`);
|
|
94
117
|
console.log(`Expires at: ${session.expires_at}`);
|
|
95
118
|
console.log("Waiting for phone to scan...\n");
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
// Try to fetch profile to infer label/platform
|
|
106
|
-
let label = preferredLabel ?? "";
|
|
107
|
-
let platform = "unknown";
|
|
119
|
+
await waitForPairingClaim(endpoint, userId, controllerToken, session.session_id);
|
|
120
|
+
// 5. Fetch credentials to get device info
|
|
121
|
+
const creds = await fetchUserCredentials(endpoint, userId, controllerToken);
|
|
122
|
+
const cred = creds[0]; // Just-paired device should be the first/only
|
|
123
|
+
if (!cred)
|
|
124
|
+
throw new Error("Pairing claimed but no credentials found");
|
|
125
|
+
// 6. Try to get label/platform from profile
|
|
126
|
+
let deviceLabel = cred.label ?? "";
|
|
127
|
+
let platform = cred.platform ?? "unknown";
|
|
108
128
|
try {
|
|
129
|
+
const runtimeCfg = {
|
|
130
|
+
controlPlaneEndpoint: endpoint,
|
|
131
|
+
credentialId: cred.credential_id,
|
|
132
|
+
controllerToken,
|
|
133
|
+
timeoutMs: 10_000,
|
|
134
|
+
};
|
|
109
135
|
const fetched = await fetchDeviceProfileOnce(runtimeCfg);
|
|
110
136
|
if (fetched) {
|
|
111
137
|
const st = extractStatic(fetched.rawAttrs);
|
|
112
|
-
if (!
|
|
113
|
-
|
|
138
|
+
if (!deviceLabel || deviceLabel === cred.credential_id) {
|
|
139
|
+
deviceLabel = st.model && st.model !== "unknown" ? st.model : "";
|
|
140
|
+
}
|
|
114
141
|
if (st.platform === "ios" || st.platform === "android")
|
|
115
142
|
platform = st.platform;
|
|
116
143
|
}
|
|
@@ -118,37 +145,114 @@ export async function executePairing(endpoint, edgeId, preferredLabel) {
|
|
|
118
145
|
catch {
|
|
119
146
|
// fall through
|
|
120
147
|
}
|
|
121
|
-
if (!
|
|
122
|
-
|
|
148
|
+
if (!deviceLabel)
|
|
149
|
+
deviceLabel = `device-${Date.now().toString(36)}`;
|
|
123
150
|
const now = new Date().toISOString();
|
|
124
|
-
const
|
|
125
|
-
credential_id:
|
|
126
|
-
|
|
127
|
-
endpoint,
|
|
128
|
-
label,
|
|
151
|
+
const deviceRecord = {
|
|
152
|
+
credential_id: cred.credential_id,
|
|
153
|
+
label: deviceLabel,
|
|
129
154
|
platform,
|
|
130
|
-
paired_at: now,
|
|
155
|
+
paired_at: cred.paired_at ?? now,
|
|
131
156
|
last_seen_at: now,
|
|
132
157
|
};
|
|
133
|
-
|
|
158
|
+
const userRecord = {
|
|
159
|
+
user_id: userId,
|
|
160
|
+
controller_token: controllerToken,
|
|
161
|
+
label,
|
|
162
|
+
created_at: userResp.created_at ?? now,
|
|
163
|
+
devices: [deviceRecord],
|
|
164
|
+
};
|
|
165
|
+
addUser(userRecord);
|
|
134
166
|
ensureZhiHandDir();
|
|
135
167
|
saveState({
|
|
136
|
-
sessionId: session.
|
|
168
|
+
sessionId: session.session_id,
|
|
169
|
+
userId,
|
|
137
170
|
controllerToken,
|
|
138
|
-
edgeId
|
|
139
|
-
credentialId,
|
|
171
|
+
edgeId,
|
|
172
|
+
credentialId: cred.credential_id,
|
|
140
173
|
pairUrl: session.pair_url,
|
|
141
174
|
status: "claimed",
|
|
142
175
|
});
|
|
143
|
-
return {
|
|
176
|
+
return { userRecord, deviceRecord };
|
|
144
177
|
}
|
|
145
|
-
|
|
146
|
-
|
|
178
|
+
/**
|
|
179
|
+
* Add device to existing user: create pairing session → wait → fetch new credential → save.
|
|
180
|
+
*/
|
|
181
|
+
export async function executePairingAddDevice(userId, preferredLabel) {
|
|
182
|
+
const endpoint = resolveDefaultEndpoint();
|
|
183
|
+
const user = getUserRecord(userId);
|
|
184
|
+
if (!user)
|
|
185
|
+
throw new Error(`User '${userId}' not found in config`);
|
|
186
|
+
const controllerToken = user.controller_token;
|
|
187
|
+
// Register plugin (get edge_id)
|
|
188
|
+
const stableIdentity = `mcp-${Date.now().toString(36)}`;
|
|
189
|
+
const plugin = await registerPlugin(endpoint, { stableIdentity });
|
|
190
|
+
const edgeId = plugin.edge_id;
|
|
191
|
+
// Get existing credential IDs before pairing
|
|
192
|
+
const existingCreds = await fetchUserCredentials(endpoint, userId, controllerToken);
|
|
193
|
+
const existingIds = new Set(existingCreds.map((c) => c.credential_id));
|
|
194
|
+
// Create pairing session
|
|
195
|
+
const session = await createPairingSession(endpoint, userId, controllerToken, edgeId, 300);
|
|
196
|
+
const qr = await renderPairingQRCode(session.pair_url);
|
|
197
|
+
console.log(qr);
|
|
198
|
+
console.log(`Open this URL on your phone to pair:\n ${session.pair_url}\n`);
|
|
199
|
+
console.log(`Expires at: ${session.expires_at}`);
|
|
200
|
+
console.log("Waiting for phone to scan...\n");
|
|
201
|
+
await waitForPairingClaim(endpoint, userId, controllerToken, session.session_id);
|
|
202
|
+
// Fetch credentials and find the new one
|
|
203
|
+
const updatedCreds = await fetchUserCredentials(endpoint, userId, controllerToken);
|
|
204
|
+
const newCred = updatedCreds.find((c) => !existingIds.has(c.credential_id));
|
|
205
|
+
if (!newCred)
|
|
206
|
+
throw new Error("Pairing claimed but no new credential found");
|
|
207
|
+
// Try to get label/platform
|
|
208
|
+
let deviceLabel = preferredLabel ?? newCred.label ?? "";
|
|
209
|
+
let platform = newCred.platform ?? "unknown";
|
|
210
|
+
try {
|
|
211
|
+
const runtimeCfg = {
|
|
212
|
+
controlPlaneEndpoint: endpoint,
|
|
213
|
+
credentialId: newCred.credential_id,
|
|
214
|
+
controllerToken,
|
|
215
|
+
timeoutMs: 10_000,
|
|
216
|
+
};
|
|
217
|
+
const fetched = await fetchDeviceProfileOnce(runtimeCfg);
|
|
218
|
+
if (fetched) {
|
|
219
|
+
const st = extractStatic(fetched.rawAttrs);
|
|
220
|
+
if (!deviceLabel || deviceLabel === newCred.credential_id) {
|
|
221
|
+
deviceLabel = st.model && st.model !== "unknown" ? st.model : "";
|
|
222
|
+
}
|
|
223
|
+
if (st.platform === "ios" || st.platform === "android")
|
|
224
|
+
platform = st.platform;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
// fall through
|
|
229
|
+
}
|
|
230
|
+
if (!deviceLabel)
|
|
231
|
+
deviceLabel = `device-${Date.now().toString(36)}`;
|
|
232
|
+
const now = new Date().toISOString();
|
|
233
|
+
const deviceRecord = {
|
|
234
|
+
credential_id: newCred.credential_id,
|
|
235
|
+
label: deviceLabel,
|
|
236
|
+
platform,
|
|
237
|
+
paired_at: newCred.paired_at ?? now,
|
|
238
|
+
last_seen_at: now,
|
|
239
|
+
};
|
|
240
|
+
addDeviceToUser(userId, deviceRecord);
|
|
241
|
+
return deviceRecord;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Legacy: format pairing status (kept for backward compat).
|
|
245
|
+
*/
|
|
246
|
+
export function formatPairingStatus(userId) {
|
|
247
|
+
if (!userId)
|
|
248
|
+
return "Not paired. Run 'zhihand pair' to connect a device.";
|
|
249
|
+
const user = getUserRecord(userId);
|
|
250
|
+
if (!user)
|
|
147
251
|
return "Not paired. Run 'zhihand pair' to connect a device.";
|
|
148
|
-
|
|
149
|
-
`
|
|
150
|
-
`
|
|
151
|
-
`
|
|
152
|
-
|
|
153
|
-
|
|
252
|
+
const lines = [
|
|
253
|
+
`User: ${user.label} (${user.user_id})`,
|
|
254
|
+
`Devices: ${user.devices.length}`,
|
|
255
|
+
...user.devices.map((d) => ` - ${d.credential_id} (${d.label}, ${d.platform})`),
|
|
256
|
+
];
|
|
257
|
+
return lines.join("\n");
|
|
154
258
|
}
|
package/dist/core/registry.d.ts
CHANGED
|
@@ -1,67 +1,60 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Device registry — the single source of truth for all paired devices,
|
|
3
|
-
* their live state
|
|
4
|
-
* device routing.
|
|
3
|
+
* their live state, and multi-user WebSocket streams.
|
|
5
4
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Groups devices under users. Each user has one UserEventWebSocket.
|
|
6
|
+
* Online detection is server-authoritative (no local heartbeat polling).
|
|
7
|
+
* Config hot-reload via fs.watchFile.
|
|
8
8
|
*/
|
|
9
|
-
import { type DeviceRecord, type ZhiHandRuntimeConfig } from "./config.ts";
|
|
9
|
+
import { type DeviceRecord, type DevicePlatform, type ZhiHandRuntimeConfig } from "./config.ts";
|
|
10
10
|
import { type StaticContext, type Capabilities } from "./device.ts";
|
|
11
11
|
export interface DeviceState {
|
|
12
12
|
credentialId: string;
|
|
13
|
+
userId: string;
|
|
14
|
+
userLabel: string;
|
|
13
15
|
label: string;
|
|
14
|
-
platform:
|
|
16
|
+
platform: DevicePlatform;
|
|
15
17
|
online: boolean;
|
|
16
18
|
lastSeenAtMs: number;
|
|
17
19
|
profile: StaticContext | null;
|
|
18
20
|
capabilities: Capabilities | null;
|
|
19
21
|
profileReceivedAtMs: number;
|
|
20
22
|
rawAttributes: Record<string, unknown>;
|
|
21
|
-
sseController: AbortController | null;
|
|
22
|
-
sseConnected: boolean;
|
|
23
|
-
heartbeatTimer: ReturnType<typeof setInterval> | null;
|
|
24
23
|
record: DeviceRecord;
|
|
25
24
|
}
|
|
26
25
|
type ListChangedCb = () => void;
|
|
27
26
|
declare class Registry {
|
|
28
|
-
private
|
|
27
|
+
private userStates;
|
|
29
28
|
private listChangedSubs;
|
|
30
29
|
private debounceTimer;
|
|
31
30
|
private lastOnlineSet;
|
|
32
31
|
private initialized;
|
|
32
|
+
private configWatchActive;
|
|
33
|
+
private reconcileTimer;
|
|
33
34
|
get(credentialId: string): DeviceState | null;
|
|
34
35
|
list(): DeviceState[];
|
|
35
36
|
listOnline(): DeviceState[];
|
|
36
37
|
/**
|
|
37
|
-
*
|
|
38
|
-
* 1. If the user has explicitly set a default via `zhihand default <id>`
|
|
39
|
-
* AND that device is online → return it. Honoring an explicit user
|
|
40
|
-
* preference is the least-surprising UX.
|
|
41
|
-
* 2. Otherwise → most-recently-active online device (online[0] is sorted
|
|
42
|
-
* desc by lastSeenAtMs).
|
|
43
|
-
* 3. No online devices → null.
|
|
38
|
+
* Most-recently-active online device across all users.
|
|
44
39
|
*/
|
|
45
40
|
resolveDefault(): DeviceState | null;
|
|
41
|
+
isMultiUser(): boolean;
|
|
46
42
|
toRuntimeConfig(state: DeviceState): ZhiHandRuntimeConfig;
|
|
47
43
|
subscribe(cb: ListChangedCb): () => void;
|
|
44
|
+
init(): Promise<void>;
|
|
45
|
+
shutdown(): void;
|
|
48
46
|
private computeOnlineSet;
|
|
49
47
|
private setsEqual;
|
|
50
48
|
private scheduleListChanged;
|
|
51
|
-
private
|
|
49
|
+
private createUserState;
|
|
50
|
+
private makeDeviceState;
|
|
51
|
+
private populateDevicesFromConfig;
|
|
52
|
+
private fetchAndPopulateDevices;
|
|
53
|
+
private startUserStream;
|
|
52
54
|
private touchLastSeen;
|
|
53
|
-
private
|
|
54
|
-
private
|
|
55
|
-
private
|
|
56
|
-
private startSSE;
|
|
57
|
-
private stopSSE;
|
|
58
|
-
private makeState;
|
|
59
|
-
init(): Promise<void>;
|
|
60
|
-
addDevice(record: DeviceRecord): Promise<void>;
|
|
61
|
-
removeDevice(credentialId: string): void;
|
|
62
|
-
renameDevice(credentialId: string, label: string): void;
|
|
63
|
-
setDefault(credentialId: string): void;
|
|
64
|
-
shutdown(): void;
|
|
55
|
+
private startConfigWatch;
|
|
56
|
+
private stopConfigWatch;
|
|
57
|
+
private reconcileConfig;
|
|
65
58
|
}
|
|
66
59
|
export declare const registry: Registry;
|
|
67
60
|
export {};
|