@keeperhub/wallet 0.1.11 → 0.1.13
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/README.md +21 -5
- package/bin/keeperhub-wallet-mcp.js +21 -0
- package/dist/cli.cjs +562 -165
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +576 -164
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +573 -202
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +71 -245
- package/dist/index.d.ts +71 -245
- package/dist/index.js +587 -201
- package/dist/index.js.map +1 -1
- package/dist/mcp-server.cjs +1305 -0
- package/dist/mcp-server.cjs.map +1 -0
- package/dist/mcp-server.d.cts +54 -0
- package/dist/mcp-server.d.ts +54 -0
- package/dist/mcp-server.js +1283 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/payment-signer-CyeRXcX2.d.cts +236 -0
- package/dist/payment-signer-CyeRXcX2.d.ts +236 -0
- package/package.json +57 -54
- package/skill/keeperhub-wallet.skill.md +16 -9
package/dist/cli.cjs
CHANGED
|
@@ -111,75 +111,275 @@ function fund(walletAddress) {
|
|
|
111
111
|
};
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
// src/
|
|
115
|
-
var
|
|
116
|
-
|
|
114
|
+
// src/hmac.ts
|
|
115
|
+
var import_node_crypto = require("crypto");
|
|
116
|
+
function computeSignature(secret, method, path, subOrgId, body, timestamp) {
|
|
117
|
+
const bodyDigest = (0, import_node_crypto.createHash)("sha256").update(body).digest("hex");
|
|
118
|
+
const signingString = `${method}
|
|
119
|
+
${path}
|
|
120
|
+
${subOrgId}
|
|
121
|
+
${bodyDigest}
|
|
122
|
+
${timestamp}`;
|
|
123
|
+
return (0, import_node_crypto.createHmac)("sha256", secret).update(signingString).digest("hex");
|
|
124
|
+
}
|
|
125
|
+
function buildHmacHeaders(secret, method, path, subOrgId, body) {
|
|
126
|
+
const timestamp = String(Math.floor(Date.now() / 1e3));
|
|
127
|
+
const signature = computeSignature(
|
|
128
|
+
secret,
|
|
129
|
+
method,
|
|
130
|
+
path,
|
|
131
|
+
subOrgId,
|
|
132
|
+
body,
|
|
133
|
+
timestamp
|
|
134
|
+
);
|
|
135
|
+
return {
|
|
136
|
+
"X-KH-Sub-Org": subOrgId,
|
|
137
|
+
"X-KH-Timestamp": timestamp,
|
|
138
|
+
"X-KH-Signature": signature
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// src/storage.ts
|
|
143
|
+
var import_node_crypto2 = require("crypto");
|
|
117
144
|
var import_promises = require("fs/promises");
|
|
118
|
-
var
|
|
119
|
-
var
|
|
145
|
+
var import_node_os = require("os");
|
|
146
|
+
var import_node_path = require("path");
|
|
147
|
+
|
|
148
|
+
// src/types.ts
|
|
149
|
+
var WalletConfigMissingError = class extends Error {
|
|
150
|
+
constructor() {
|
|
151
|
+
super(
|
|
152
|
+
"Wallet config not found at ~/.keeperhub/wallet.json. Run `npx @keeperhub/wallet add` to provision."
|
|
153
|
+
);
|
|
154
|
+
this.name = "WalletConfigMissingError";
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
var WalletConfigCorruptError = class extends Error {
|
|
158
|
+
path;
|
|
159
|
+
constructor(path, reason) {
|
|
160
|
+
super(
|
|
161
|
+
`Wallet config at ${path} is unreadable: ${reason}. Repair the file by hand or delete it to re-provision a new wallet (this will abandon any funds held in the current wallet).`
|
|
162
|
+
);
|
|
163
|
+
this.name = "WalletConfigCorruptError";
|
|
164
|
+
this.path = path;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// src/storage.ts
|
|
169
|
+
async function readWalletConfig() {
|
|
170
|
+
const walletPath = (0, import_node_path.join)((0, import_node_os.homedir)(), ".keeperhub", "wallet.json");
|
|
171
|
+
let raw;
|
|
172
|
+
try {
|
|
173
|
+
raw = await (0, import_promises.readFile)(walletPath, "utf-8");
|
|
174
|
+
} catch (err) {
|
|
175
|
+
if (err.code === "ENOENT") {
|
|
176
|
+
throw new WalletConfigMissingError();
|
|
177
|
+
}
|
|
178
|
+
throw err;
|
|
179
|
+
}
|
|
180
|
+
let parsed;
|
|
181
|
+
try {
|
|
182
|
+
parsed = JSON.parse(raw);
|
|
183
|
+
} catch (err) {
|
|
184
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
185
|
+
throw new WalletConfigCorruptError(walletPath, reason);
|
|
186
|
+
}
|
|
187
|
+
if (!(parsed.subOrgId && parsed.walletAddress && parsed.hmacSecret)) {
|
|
188
|
+
throw new WalletConfigCorruptError(walletPath, "missing required fields");
|
|
189
|
+
}
|
|
190
|
+
return parsed;
|
|
191
|
+
}
|
|
192
|
+
async function writeWalletConfig(config) {
|
|
193
|
+
const walletPath = (0, import_node_path.join)((0, import_node_os.homedir)(), ".keeperhub", "wallet.json");
|
|
194
|
+
await (0, import_promises.mkdir)((0, import_node_path.dirname)(walletPath), { recursive: true, mode: 448 });
|
|
195
|
+
const suffix = (0, import_node_crypto2.randomBytes)(8).toString("hex");
|
|
196
|
+
const tmpPath = `${walletPath}.${process.pid}.${suffix}.tmp`;
|
|
197
|
+
await (0, import_promises.writeFile)(tmpPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
198
|
+
await (0, import_promises.chmod)(tmpPath, 384);
|
|
199
|
+
await (0, import_promises.rename)(tmpPath, walletPath);
|
|
200
|
+
}
|
|
201
|
+
function getWalletConfigPath() {
|
|
202
|
+
return (0, import_node_path.join)((0, import_node_os.homedir)(), ".keeperhub", "wallet.json");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// src/provision.ts
|
|
206
|
+
var TRAILING_SLASH = /\/$/;
|
|
207
|
+
var WALLET_ADDRESS_PATTERN = /^0x[a-fA-F0-9]{40}$/;
|
|
208
|
+
var ProvisionResponseInvalidError = class extends Error {
|
|
209
|
+
code = "PROVISION_RESPONSE_INVALID";
|
|
210
|
+
constructor(message) {
|
|
211
|
+
super(message);
|
|
212
|
+
this.name = "ProvisionResponseInvalidError";
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
var ProvisionHttpError = class extends Error {
|
|
216
|
+
code = "PROVISION_HTTP_ERROR";
|
|
217
|
+
status;
|
|
218
|
+
body;
|
|
219
|
+
constructor(status, body) {
|
|
220
|
+
super(`provision failed: HTTP ${status}: ${body}`);
|
|
221
|
+
this.name = "ProvisionHttpError";
|
|
222
|
+
this.status = status;
|
|
223
|
+
this.body = body;
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
function resolveBaseUrl(override) {
|
|
227
|
+
const candidate = override ?? process.env.KEEPERHUB_API_URL ?? "https://app.keeperhub.com";
|
|
228
|
+
return candidate.replace(TRAILING_SLASH, "");
|
|
229
|
+
}
|
|
230
|
+
function isNonEmptyString(value) {
|
|
231
|
+
return typeof value === "string" && value.length > 0;
|
|
232
|
+
}
|
|
233
|
+
function validateProvisionResponse(data) {
|
|
234
|
+
if (typeof data !== "object" || data === null) {
|
|
235
|
+
throw new ProvisionResponseInvalidError(
|
|
236
|
+
"provision response is not an object"
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
const { subOrgId, walletAddress, hmacSecret } = data;
|
|
240
|
+
if (!(isNonEmptyString(subOrgId) && isNonEmptyString(walletAddress) && isNonEmptyString(hmacSecret))) {
|
|
241
|
+
throw new ProvisionResponseInvalidError(
|
|
242
|
+
"provision response missing subOrgId, walletAddress, or hmacSecret"
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
if (!WALLET_ADDRESS_PATTERN.test(walletAddress)) {
|
|
246
|
+
throw new ProvisionResponseInvalidError(
|
|
247
|
+
`provision response walletAddress is not a valid 0x-prefixed 40-hex address: ${walletAddress}`
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
subOrgId,
|
|
252
|
+
walletAddress,
|
|
253
|
+
hmacSecret
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
async function provisionWallet(options = {}) {
|
|
257
|
+
const baseUrl = resolveBaseUrl(options.baseUrl);
|
|
258
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
259
|
+
const response = await fetchImpl(`${baseUrl}/api/agentic-wallet/provision`, {
|
|
260
|
+
method: "POST",
|
|
261
|
+
headers: { "content-type": "application/json" },
|
|
262
|
+
body: "{}",
|
|
263
|
+
signal: AbortSignal.timeout(3e4)
|
|
264
|
+
});
|
|
265
|
+
if (!response.ok) {
|
|
266
|
+
const text = await response.text();
|
|
267
|
+
throw new ProvisionHttpError(response.status, text);
|
|
268
|
+
}
|
|
269
|
+
const raw = await response.json();
|
|
270
|
+
const data = validateProvisionResponse(raw);
|
|
271
|
+
await writeWalletConfig(data);
|
|
272
|
+
return data;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// src/skill-install.ts
|
|
276
|
+
var import_node_crypto4 = require("crypto");
|
|
277
|
+
var import_promises3 = require("fs/promises");
|
|
278
|
+
var import_node_path5 = require("path");
|
|
279
|
+
var import_node_url2 = require("url");
|
|
120
280
|
|
|
121
281
|
// src/agent-detect.ts
|
|
122
282
|
var import_node_fs = require("fs");
|
|
123
|
-
var
|
|
124
|
-
var
|
|
283
|
+
var import_node_os2 = require("os");
|
|
284
|
+
var import_node_path2 = require("path");
|
|
125
285
|
var AGENT_SPECS = [
|
|
126
286
|
{
|
|
127
287
|
agent: "claude-code",
|
|
128
288
|
skillsRel: [".claude", "skills"],
|
|
129
289
|
settingsRel: [".claude", "settings.json"],
|
|
130
|
-
hookSupport: "claude-code"
|
|
290
|
+
hookSupport: "claude-code",
|
|
291
|
+
// ~/.claude.json is at HOME root (not under .claude/) and is large
|
|
292
|
+
// (100+KB on real installs). registerMcpServer reads/parses/rewrites it
|
|
293
|
+
// while preserving every other top-level key byte-for-byte.
|
|
294
|
+
mcpConfigRel: [".claude.json"],
|
|
295
|
+
mcpSupport: "claude-code"
|
|
131
296
|
},
|
|
132
297
|
{
|
|
133
298
|
agent: "cursor",
|
|
134
299
|
skillsRel: [".cursor", "skills"],
|
|
135
300
|
settingsRel: [".cursor", "settings.json"],
|
|
136
|
-
hookSupport: "notice"
|
|
301
|
+
hookSupport: "notice",
|
|
302
|
+
mcpConfigRel: [".cursor", "mcp.json"],
|
|
303
|
+
mcpSupport: "cursor"
|
|
137
304
|
},
|
|
138
305
|
{
|
|
139
306
|
agent: "cline",
|
|
140
307
|
skillsRel: [".cline", "skills"],
|
|
141
308
|
settingsRel: [".cline", "settings.json"],
|
|
142
|
-
hookSupport: "notice"
|
|
309
|
+
hookSupport: "notice",
|
|
310
|
+
// Cline keeps MCP state in a per-VS-Code-variant globalStorage path
|
|
311
|
+
// (e.g. ~/Library/Application Support/Code/User/globalStorage/
|
|
312
|
+
// saoudrizwan.claude-dev/settings/cline_mcp_settings.json) that is too
|
|
313
|
+
// fragile to auto-detect. Ship "notice" with a copy-paste entry shape
|
|
314
|
+
// instead of guessing the variant.
|
|
315
|
+
mcpSupport: "notice"
|
|
143
316
|
},
|
|
144
317
|
{
|
|
145
318
|
agent: "windsurf",
|
|
146
319
|
skillsRel: [".windsurf", "skills"],
|
|
147
320
|
settingsRel: [".windsurf", "settings.json"],
|
|
148
|
-
hookSupport: "notice"
|
|
321
|
+
hookSupport: "notice",
|
|
322
|
+
mcpConfigRel: [".codeium", "windsurf", "mcp_config.json"],
|
|
323
|
+
mcpSupport: "windsurf",
|
|
324
|
+
// Windsurf historically ships under both `.windsurf/` and the legacy
|
|
325
|
+
// `.codeium/windsurf/`; detect either.
|
|
326
|
+
extraDetect: [[".codeium", "windsurf"]]
|
|
149
327
|
},
|
|
150
328
|
{
|
|
151
329
|
agent: "opencode",
|
|
152
330
|
skillsRel: [".config", "opencode", "skills"],
|
|
153
331
|
settingsRel: [".config", "opencode", "settings.json"],
|
|
154
|
-
hookSupport: "notice"
|
|
332
|
+
hookSupport: "notice",
|
|
333
|
+
mcpConfigRel: [".config", "opencode", "opencode.json"],
|
|
334
|
+
mcpSupport: "opencode"
|
|
155
335
|
}
|
|
156
336
|
];
|
|
157
337
|
function detectAgents(homeOverride) {
|
|
158
|
-
const home = homeOverride ?? (0,
|
|
338
|
+
const home = homeOverride ?? (0, import_node_os2.homedir)();
|
|
159
339
|
const results = [];
|
|
160
340
|
for (const spec of AGENT_SPECS) {
|
|
161
|
-
const skillsDir = (0,
|
|
162
|
-
const settingsFile = (0,
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
341
|
+
const skillsDir = (0, import_node_path2.join)(home, ...spec.skillsRel);
|
|
342
|
+
const settingsFile = (0, import_node_path2.join)(home, ...spec.settingsRel);
|
|
343
|
+
let detected = (0, import_node_fs.existsSync)((0, import_node_path2.dirname)(skillsDir));
|
|
344
|
+
if (!detected && spec.extraDetect) {
|
|
345
|
+
for (const seg of spec.extraDetect) {
|
|
346
|
+
if ((0, import_node_fs.existsSync)((0, import_node_path2.join)(home, ...seg))) {
|
|
347
|
+
detected = true;
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
170
351
|
}
|
|
352
|
+
if (!detected) {
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
results.push({
|
|
356
|
+
agent: spec.agent,
|
|
357
|
+
skillsDir,
|
|
358
|
+
settingsFile,
|
|
359
|
+
hookSupport: spec.hookSupport,
|
|
360
|
+
mcpConfigRel: spec.mcpConfigRel,
|
|
361
|
+
mcpSupport: spec.mcpSupport
|
|
362
|
+
});
|
|
171
363
|
}
|
|
172
364
|
return results;
|
|
173
365
|
}
|
|
174
366
|
|
|
175
|
-
// src/
|
|
176
|
-
var
|
|
177
|
-
var
|
|
367
|
+
// src/mcp-register.ts
|
|
368
|
+
var import_node_crypto3 = require("crypto");
|
|
369
|
+
var import_promises2 = require("fs/promises");
|
|
370
|
+
var import_node_os3 = require("os");
|
|
371
|
+
var import_node_path4 = require("path");
|
|
372
|
+
|
|
373
|
+
// src/runtime-detect.ts
|
|
374
|
+
var import_node_child_process = require("child_process");
|
|
375
|
+
var import_node_fs2 = require("fs");
|
|
376
|
+
var import_node_path3 = require("path");
|
|
377
|
+
var import_node_url = require("url");
|
|
178
378
|
var PACKAGE_NAME = "@keeperhub/wallet";
|
|
179
379
|
function readPackageVersion() {
|
|
180
380
|
try {
|
|
181
|
-
const here = (0,
|
|
182
|
-
const pkgPath = (0,
|
|
381
|
+
const here = (0, import_node_path3.dirname)((0, import_node_url.fileURLToPath)(__filename));
|
|
382
|
+
const pkgPath = (0, import_node_path3.join)(here, "..", "package.json");
|
|
183
383
|
const raw = (0, import_node_fs2.readFileSync)(pkgPath, "utf-8");
|
|
184
384
|
const parsed = JSON.parse(raw);
|
|
185
385
|
if (typeof parsed.version === "string" && parsed.version.length > 0) {
|
|
@@ -189,9 +389,6 @@ function readPackageVersion() {
|
|
|
189
389
|
}
|
|
190
390
|
return "latest";
|
|
191
391
|
}
|
|
192
|
-
function buildNpxCommand(version) {
|
|
193
|
-
return `npx -y -p ${PACKAGE_NAME}@${version} ${HOOK_BIN}`;
|
|
194
|
-
}
|
|
195
392
|
function isNpxExecution() {
|
|
196
393
|
const execPath = process.env.npm_execpath;
|
|
197
394
|
if (typeof execPath !== "string" || execPath.length === 0) {
|
|
@@ -219,6 +416,144 @@ function isPathUnderTransientCache(resolvedPath) {
|
|
|
219
416
|
}
|
|
220
417
|
return false;
|
|
221
418
|
}
|
|
419
|
+
function resolveBinCommand(binName) {
|
|
420
|
+
const version = readPackageVersion();
|
|
421
|
+
const npxArgs = ["-y", "-p", `${PACKAGE_NAME}@${version}`, binName];
|
|
422
|
+
const npxCommandString = `npx ${npxArgs.join(" ")}`;
|
|
423
|
+
if (isNpxExecution()) {
|
|
424
|
+
return {
|
|
425
|
+
commandString: npxCommandString,
|
|
426
|
+
command: "npx",
|
|
427
|
+
args: npxArgs
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
try {
|
|
431
|
+
const resolved = (0, import_node_child_process.execFileSync)("/bin/sh", ["-c", `command -v ${binName}`], {
|
|
432
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
433
|
+
}).toString().trim();
|
|
434
|
+
if (resolved.length > 0 && !isPathUnderTransientCache(resolved)) {
|
|
435
|
+
return {
|
|
436
|
+
commandString: binName,
|
|
437
|
+
command: binName,
|
|
438
|
+
args: []
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
} catch {
|
|
442
|
+
}
|
|
443
|
+
return {
|
|
444
|
+
commandString: npxCommandString,
|
|
445
|
+
command: "npx",
|
|
446
|
+
args: npxArgs
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// src/mcp-register.ts
|
|
451
|
+
var MCP_BIN = "keeperhub-wallet-mcp";
|
|
452
|
+
var MCP_SERVER_NAME = "keeperhub-wallet";
|
|
453
|
+
function resolveMcpCommand() {
|
|
454
|
+
const envOverride = process.env.KEEPERHUB_WALLET_MCP_COMMAND;
|
|
455
|
+
if (envOverride && envOverride.length > 0) {
|
|
456
|
+
const parts = envOverride.trim().split(/\s+/);
|
|
457
|
+
const head = parts[0] ?? envOverride;
|
|
458
|
+
return { command: head, args: parts.slice(1) };
|
|
459
|
+
}
|
|
460
|
+
const resolved = resolveBinCommand(MCP_BIN);
|
|
461
|
+
return { command: resolved.command, args: resolved.args };
|
|
462
|
+
}
|
|
463
|
+
function buildStandardEntry(cmd) {
|
|
464
|
+
const entry = {
|
|
465
|
+
command: cmd.command,
|
|
466
|
+
args: cmd.args
|
|
467
|
+
};
|
|
468
|
+
if (cmd.env && Object.keys(cmd.env).length > 0) {
|
|
469
|
+
entry.env = cmd.env;
|
|
470
|
+
}
|
|
471
|
+
return entry;
|
|
472
|
+
}
|
|
473
|
+
function buildOpencodeEntry(cmd) {
|
|
474
|
+
return {
|
|
475
|
+
type: "local",
|
|
476
|
+
command: [cmd.command, ...cmd.args],
|
|
477
|
+
enabled: true,
|
|
478
|
+
environment: cmd.env ?? {}
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
async function readJsonOrEmpty(path) {
|
|
482
|
+
let raw = null;
|
|
483
|
+
try {
|
|
484
|
+
raw = await (0, import_promises2.readFile)(path, "utf-8");
|
|
485
|
+
} catch (err) {
|
|
486
|
+
if (err.code !== "ENOENT") {
|
|
487
|
+
throw err;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
if (raw === null) {
|
|
491
|
+
return {};
|
|
492
|
+
}
|
|
493
|
+
try {
|
|
494
|
+
return JSON.parse(raw);
|
|
495
|
+
} catch {
|
|
496
|
+
throw new Error(
|
|
497
|
+
`MCP config at ${path} is not valid JSON; aborting MCP registration`
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
async function writeJsonAtomic(path, payload) {
|
|
502
|
+
await (0, import_promises2.mkdir)((0, import_node_path4.dirname)(path), { recursive: true, mode: 448 });
|
|
503
|
+
const suffix = (0, import_node_crypto3.randomBytes)(8).toString("hex");
|
|
504
|
+
const tmpPath = `${path}.${process.pid}.${suffix}.tmp`;
|
|
505
|
+
try {
|
|
506
|
+
await (0, import_promises2.writeFile)(tmpPath, payload, { mode: 384 });
|
|
507
|
+
await (0, import_promises2.chmod)(tmpPath, 384);
|
|
508
|
+
await (0, import_promises2.rename)(tmpPath, path);
|
|
509
|
+
} catch (err) {
|
|
510
|
+
await (0, import_promises2.unlink)(tmpPath).catch(() => {
|
|
511
|
+
});
|
|
512
|
+
throw err;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
async function writeStandardMcp(path, entry) {
|
|
516
|
+
const config = await readJsonOrEmpty(path);
|
|
517
|
+
const servers = typeof config.mcpServers === "object" && config.mcpServers !== null ? config.mcpServers : {};
|
|
518
|
+
servers[MCP_SERVER_NAME] = entry;
|
|
519
|
+
config.mcpServers = servers;
|
|
520
|
+
const payload = `${JSON.stringify(config, null, 2)}
|
|
521
|
+
`;
|
|
522
|
+
await writeJsonAtomic(path, payload);
|
|
523
|
+
}
|
|
524
|
+
async function writeOpencodeMcp(path, entry) {
|
|
525
|
+
const config = await readJsonOrEmpty(path);
|
|
526
|
+
const servers = typeof config.mcp === "object" && config.mcp !== null ? config.mcp : {};
|
|
527
|
+
servers[MCP_SERVER_NAME] = entry;
|
|
528
|
+
config.mcp = servers;
|
|
529
|
+
const payload = `${JSON.stringify(config, null, 2)}
|
|
530
|
+
`;
|
|
531
|
+
await writeJsonAtomic(path, payload);
|
|
532
|
+
}
|
|
533
|
+
async function registerMcpServer(target, options = {}) {
|
|
534
|
+
if (target.mcpSupport === "notice") {
|
|
535
|
+
throw new Error(
|
|
536
|
+
`agent ${target.agent} does not support auto-registered MCP servers; surface a notice instead`
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
if (!target.mcpConfigRel) {
|
|
540
|
+
throw new Error(
|
|
541
|
+
`agent ${target.agent} has mcpSupport=${target.mcpSupport} but no mcpConfigRel path`
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
const home = options.homeOverride ?? (0, import_node_os3.homedir)();
|
|
545
|
+
const path = (0, import_node_path4.join)(home, ...target.mcpConfigRel);
|
|
546
|
+
const cmd = options.command ?? resolveMcpCommand();
|
|
547
|
+
if (target.mcpSupport === "opencode") {
|
|
548
|
+
await writeOpencodeMcp(path, buildOpencodeEntry(cmd));
|
|
549
|
+
} else {
|
|
550
|
+
await writeStandardMcp(path, buildStandardEntry(cmd));
|
|
551
|
+
}
|
|
552
|
+
return { path, name: MCP_SERVER_NAME };
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// src/skill-install.ts
|
|
556
|
+
var HOOK_BIN = "keeperhub-wallet-hook";
|
|
222
557
|
var KEEPERHUB_HOOK_MARKER = HOOK_BIN;
|
|
223
558
|
function filterKeeperhubHooksFromEntry(entry) {
|
|
224
559
|
if (typeof entry !== "object" || entry === null) {
|
|
@@ -245,19 +580,7 @@ function resolveHookCommand() {
|
|
|
245
580
|
if (envOverride && envOverride.length > 0) {
|
|
246
581
|
return envOverride;
|
|
247
582
|
}
|
|
248
|
-
|
|
249
|
-
return buildNpxCommand(readPackageVersion());
|
|
250
|
-
}
|
|
251
|
-
try {
|
|
252
|
-
const resolved = (0, import_node_child_process.execFileSync)("/bin/sh", ["-c", `command -v ${HOOK_BIN}`], {
|
|
253
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
254
|
-
}).toString().trim();
|
|
255
|
-
if (resolved.length > 0 && !isPathUnderTransientCache(resolved)) {
|
|
256
|
-
return HOOK_COMMAND_BARE;
|
|
257
|
-
}
|
|
258
|
-
} catch {
|
|
259
|
-
}
|
|
260
|
-
return buildNpxCommand(readPackageVersion());
|
|
583
|
+
return resolveBinCommand(HOOK_BIN).commandString;
|
|
261
584
|
}
|
|
262
585
|
function buildKeeperhubEntry(command) {
|
|
263
586
|
return {
|
|
@@ -266,8 +589,8 @@ function buildKeeperhubEntry(command) {
|
|
|
266
589
|
};
|
|
267
590
|
}
|
|
268
591
|
function resolveDefaultSkillSource() {
|
|
269
|
-
const here = (0,
|
|
270
|
-
return (0,
|
|
592
|
+
const here = (0, import_node_path5.dirname)((0, import_node_url2.fileURLToPath)(__filename));
|
|
593
|
+
return (0, import_node_path5.join)(here, "..", "skill", "keeperhub-wallet.skill.md");
|
|
271
594
|
}
|
|
272
595
|
function defaultNotice(msg) {
|
|
273
596
|
process.stderr.write(`${msg}
|
|
@@ -277,7 +600,7 @@ async function registerClaudeCodeHook(settingsPath, options = {}) {
|
|
|
277
600
|
const command = options.hookCommand ?? resolveHookCommand();
|
|
278
601
|
let raw = null;
|
|
279
602
|
try {
|
|
280
|
-
raw = await (0,
|
|
603
|
+
raw = await (0, import_promises3.readFile)(settingsPath, "utf-8");
|
|
281
604
|
} catch (err) {
|
|
282
605
|
if (err.code !== "ENOENT") {
|
|
283
606
|
throw err;
|
|
@@ -305,158 +628,136 @@ async function registerClaudeCodeHook(settingsPath, options = {}) {
|
|
|
305
628
|
filtered.push(buildKeeperhubEntry(command));
|
|
306
629
|
hooks.PreToolUse = filtered;
|
|
307
630
|
config.hooks = hooks;
|
|
308
|
-
await (0,
|
|
631
|
+
await (0, import_promises3.mkdir)((0, import_node_path5.dirname)(settingsPath), { recursive: true, mode: 448 });
|
|
309
632
|
const payload = `${JSON.stringify(config, null, 2)}
|
|
310
633
|
`;
|
|
311
|
-
|
|
312
|
-
|
|
634
|
+
const suffix = (0, import_node_crypto4.randomBytes)(8).toString("hex");
|
|
635
|
+
const tmpPath = `${settingsPath}.${process.pid}.${suffix}.tmp`;
|
|
636
|
+
try {
|
|
637
|
+
await (0, import_promises3.writeFile)(tmpPath, payload, { mode: 384 });
|
|
638
|
+
await (0, import_promises3.chmod)(tmpPath, 384);
|
|
639
|
+
await (0, import_promises3.rename)(tmpPath, settingsPath);
|
|
640
|
+
} catch (err) {
|
|
641
|
+
await (0, import_promises3.unlink)(tmpPath).catch(() => {
|
|
642
|
+
});
|
|
643
|
+
throw err;
|
|
644
|
+
}
|
|
313
645
|
}
|
|
314
646
|
async function writeSkillToAgent(agent, skillSource) {
|
|
315
|
-
await (0,
|
|
316
|
-
const target = (0,
|
|
317
|
-
await (0,
|
|
318
|
-
await (0,
|
|
647
|
+
await (0, import_promises3.mkdir)(agent.skillsDir, { recursive: true, mode: 493 });
|
|
648
|
+
const target = (0, import_node_path5.join)(agent.skillsDir, "keeperhub-wallet.skill.md");
|
|
649
|
+
await (0, import_promises3.copyFile)(skillSource, target);
|
|
650
|
+
await (0, import_promises3.chmod)(target, 420);
|
|
319
651
|
return { agent: agent.agent, path: target, status: "written" };
|
|
320
652
|
}
|
|
321
|
-
function
|
|
653
|
+
function buildHookNoticeMessage(agent, command) {
|
|
322
654
|
return `${agent.agent} does not support auto-registered PreToolUse hooks; run \`${command}\` on every tool use via ${agent.agent}'s settings file at ${agent.settingsFile}`;
|
|
323
655
|
}
|
|
656
|
+
function buildMcpNoticeMessage(agent, command) {
|
|
657
|
+
const cmd = [command.command, ...command.args].join(" ");
|
|
658
|
+
return `${agent.agent} does not support auto-registered MCP servers; add an entry named \`keeperhub-wallet\` running \`${cmd}\` to your MCP config manually`;
|
|
659
|
+
}
|
|
324
660
|
async function installSkill(options = {}) {
|
|
325
661
|
const agents = detectAgents(options.homeOverride);
|
|
326
662
|
const skillSource = options.skillSourcePath ?? resolveDefaultSkillSource();
|
|
327
663
|
const onNotice = options.onNotice ?? defaultNotice;
|
|
328
664
|
const hookCommand = options.hookCommand ?? resolveHookCommand();
|
|
665
|
+
const mcpCommand = options.mcpCommand ?? resolveMcpCommand();
|
|
329
666
|
const skillWrites = [];
|
|
330
667
|
const hookRegistrations = [];
|
|
668
|
+
const mcpRegistrations = [];
|
|
331
669
|
for (const agent of agents) {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
670
|
+
try {
|
|
671
|
+
const write = await writeSkillToAgent(agent, skillSource);
|
|
672
|
+
skillWrites.push(write);
|
|
673
|
+
} catch (err) {
|
|
674
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
675
|
+
skillWrites.push({
|
|
337
676
|
agent: agent.agent,
|
|
338
|
-
|
|
677
|
+
path: "",
|
|
678
|
+
status: "skipped"
|
|
339
679
|
});
|
|
680
|
+
onNotice(`${agent.agent}: skill copy failed (${message})`);
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
if (agent.hookSupport === "claude-code") {
|
|
684
|
+
try {
|
|
685
|
+
await registerClaudeCodeHook(agent.settingsFile, { hookCommand });
|
|
686
|
+
hookRegistrations.push({
|
|
687
|
+
agent: agent.agent,
|
|
688
|
+
status: "registered"
|
|
689
|
+
});
|
|
690
|
+
} catch (err) {
|
|
691
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
692
|
+
hookRegistrations.push({
|
|
693
|
+
agent: agent.agent,
|
|
694
|
+
status: "failed",
|
|
695
|
+
message
|
|
696
|
+
});
|
|
697
|
+
onNotice(`${agent.agent}: hook registration failed (${message})`);
|
|
698
|
+
}
|
|
340
699
|
} else {
|
|
341
|
-
const
|
|
700
|
+
const noticeMessage = buildHookNoticeMessage(agent, hookCommand);
|
|
342
701
|
hookRegistrations.push({
|
|
343
702
|
agent: agent.agent,
|
|
344
703
|
status: "notice",
|
|
345
|
-
message
|
|
704
|
+
message: noticeMessage
|
|
346
705
|
});
|
|
347
|
-
onNotice(
|
|
706
|
+
onNotice(noticeMessage);
|
|
348
707
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
708
|
+
if (agent.mcpSupport === "notice") {
|
|
709
|
+
const noticeMessage = buildMcpNoticeMessage(agent, mcpCommand);
|
|
710
|
+
mcpRegistrations.push({
|
|
711
|
+
agent: agent.agent,
|
|
712
|
+
status: "notice",
|
|
713
|
+
message: noticeMessage
|
|
714
|
+
});
|
|
715
|
+
onNotice(noticeMessage);
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
try {
|
|
719
|
+
const mcpResult = await registerMcpServer(agent, {
|
|
720
|
+
homeOverride: options.homeOverride,
|
|
721
|
+
command: mcpCommand
|
|
722
|
+
});
|
|
723
|
+
mcpRegistrations.push({
|
|
724
|
+
agent: agent.agent,
|
|
725
|
+
status: "registered",
|
|
726
|
+
path: mcpResult.path
|
|
727
|
+
});
|
|
728
|
+
} catch (err) {
|
|
729
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
730
|
+
mcpRegistrations.push({
|
|
731
|
+
agent: agent.agent,
|
|
732
|
+
status: "failed",
|
|
733
|
+
message
|
|
734
|
+
});
|
|
735
|
+
onNotice(`${agent.agent}: MCP registration failed (${message})`);
|
|
377
736
|
}
|
|
378
|
-
throw err;
|
|
379
|
-
}
|
|
380
|
-
const parsed = JSON.parse(raw);
|
|
381
|
-
if (!(parsed.subOrgId && parsed.walletAddress && parsed.hmacSecret)) {
|
|
382
|
-
throw new Error(`Malformed wallet.json at ${walletPath}`);
|
|
383
737
|
}
|
|
384
|
-
return
|
|
385
|
-
}
|
|
386
|
-
async function writeWalletConfig(config) {
|
|
387
|
-
const walletPath = (0, import_node_path3.join)((0, import_node_os2.homedir)(), ".keeperhub", "wallet.json");
|
|
388
|
-
await (0, import_promises2.mkdir)((0, import_node_path3.dirname)(walletPath), { recursive: true, mode: 448 });
|
|
389
|
-
await (0, import_promises2.writeFile)(walletPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
390
|
-
await (0, import_promises2.chmod)(walletPath, 384);
|
|
391
|
-
}
|
|
392
|
-
function getWalletConfigPath() {
|
|
393
|
-
return (0, import_node_path3.join)((0, import_node_os2.homedir)(), ".keeperhub", "wallet.json");
|
|
738
|
+
return { skillWrites, hookRegistrations, mcpRegistrations };
|
|
394
739
|
}
|
|
395
740
|
|
|
396
741
|
// src/cli.ts
|
|
397
|
-
var TRAILING_SLASH = /\/$/;
|
|
398
|
-
var WALLET_ADDRESS_PATTERN = /^0x[a-fA-F0-9]{40}$/;
|
|
399
|
-
function resolveBaseUrl(override) {
|
|
400
|
-
const candidate = override ?? process.env.KEEPERHUB_API_URL ?? "https://app.keeperhub.com";
|
|
401
|
-
return candidate.replace(TRAILING_SLASH, "");
|
|
402
|
-
}
|
|
403
|
-
function isNonEmptyString(value) {
|
|
404
|
-
return typeof value === "string" && value.length > 0;
|
|
405
|
-
}
|
|
406
|
-
function provisionInvalidError(message) {
|
|
407
|
-
const err = new Error(message);
|
|
408
|
-
err.code = "PROVISION_RESPONSE_INVALID";
|
|
409
|
-
return err;
|
|
410
|
-
}
|
|
411
|
-
function validateProvisionResponse(data) {
|
|
412
|
-
if (typeof data !== "object" || data === null) {
|
|
413
|
-
throw provisionInvalidError("provision response is not an object");
|
|
414
|
-
}
|
|
415
|
-
const { subOrgId, walletAddress, hmacSecret } = data;
|
|
416
|
-
if (!(isNonEmptyString(subOrgId) && isNonEmptyString(walletAddress) && isNonEmptyString(hmacSecret))) {
|
|
417
|
-
throw provisionInvalidError(
|
|
418
|
-
"provision response missing subOrgId, walletAddress, or hmacSecret"
|
|
419
|
-
);
|
|
420
|
-
}
|
|
421
|
-
if (!WALLET_ADDRESS_PATTERN.test(walletAddress)) {
|
|
422
|
-
throw provisionInvalidError(
|
|
423
|
-
`provision response walletAddress is not a valid 0x-prefixed 40-hex address: ${walletAddress}`
|
|
424
|
-
);
|
|
425
|
-
}
|
|
426
|
-
return {
|
|
427
|
-
subOrgId,
|
|
428
|
-
walletAddress,
|
|
429
|
-
hmacSecret
|
|
430
|
-
};
|
|
431
|
-
}
|
|
432
742
|
async function cmdAdd(opts = {}) {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
headers: { "content-type": "application/json" },
|
|
437
|
-
body: "{}"
|
|
438
|
-
});
|
|
439
|
-
if (!response.ok) {
|
|
440
|
-
const text = await response.text();
|
|
441
|
-
process.stderr.write(
|
|
442
|
-
`[keeperhub-wallet] provision failed: HTTP ${response.status}: ${text}
|
|
443
|
-
`
|
|
444
|
-
);
|
|
445
|
-
process.exit(1);
|
|
446
|
-
}
|
|
447
|
-
const raw = await response.json();
|
|
448
|
-
const data = validateProvisionResponse(raw);
|
|
449
|
-
await writeWalletConfig({
|
|
450
|
-
subOrgId: data.subOrgId,
|
|
451
|
-
walletAddress: data.walletAddress,
|
|
452
|
-
hmacSecret: data.hmacSecret
|
|
453
|
-
});
|
|
454
|
-
process.stdout.write(`subOrgId: ${data.subOrgId}
|
|
743
|
+
try {
|
|
744
|
+
const data = await provisionWallet({ baseUrl: opts.baseUrl });
|
|
745
|
+
process.stdout.write(`subOrgId: ${data.subOrgId}
|
|
455
746
|
`);
|
|
456
|
-
|
|
747
|
+
process.stdout.write(`walletAddress: ${data.walletAddress}
|
|
457
748
|
`);
|
|
458
|
-
|
|
749
|
+
process.stdout.write(`config written to ${getWalletConfigPath()}
|
|
459
750
|
`);
|
|
751
|
+
} catch (err) {
|
|
752
|
+
if (err instanceof ProvisionHttpError) {
|
|
753
|
+
process.stderr.write(
|
|
754
|
+
`[keeperhub-wallet] provision failed: HTTP ${err.status}: ${err.body}
|
|
755
|
+
`
|
|
756
|
+
);
|
|
757
|
+
process.exit(1);
|
|
758
|
+
}
|
|
759
|
+
throw err;
|
|
760
|
+
}
|
|
460
761
|
}
|
|
461
762
|
async function cmdFund() {
|
|
462
763
|
const wallet = await readWalletConfig();
|
|
@@ -483,6 +784,58 @@ async function cmdInfo() {
|
|
|
483
784
|
process.stdout.write(`walletAddress: ${wallet.walletAddress}
|
|
484
785
|
`);
|
|
485
786
|
}
|
|
787
|
+
var FEEDBACK_DEFAULT_BASE_URL = "https://app.keeperhub.com";
|
|
788
|
+
async function cmdFeedback(opts) {
|
|
789
|
+
const wallet = await readWalletConfig();
|
|
790
|
+
const baseUrl = (opts.baseUrl ?? FEEDBACK_DEFAULT_BASE_URL).replace(
|
|
791
|
+
/\/$/,
|
|
792
|
+
""
|
|
793
|
+
);
|
|
794
|
+
const path = "/api/agentic-wallet/feedback";
|
|
795
|
+
const body = {
|
|
796
|
+
executionId: opts.executionId,
|
|
797
|
+
value: Number.parseInt(opts.value, 10),
|
|
798
|
+
valueDecimals: Number.parseInt(opts.decimals ?? "0", 10)
|
|
799
|
+
};
|
|
800
|
+
if (opts.comment !== void 0) {
|
|
801
|
+
body.comment = opts.comment;
|
|
802
|
+
}
|
|
803
|
+
if (opts.agentId !== void 0) {
|
|
804
|
+
body.agentId = opts.agentId;
|
|
805
|
+
}
|
|
806
|
+
if (opts.chainId !== void 0) {
|
|
807
|
+
body.agentChainId = Number.parseInt(opts.chainId, 10);
|
|
808
|
+
}
|
|
809
|
+
const bodyJson = JSON.stringify(body);
|
|
810
|
+
const headers = buildHmacHeaders(
|
|
811
|
+
wallet.hmacSecret,
|
|
812
|
+
"POST",
|
|
813
|
+
path,
|
|
814
|
+
wallet.subOrgId,
|
|
815
|
+
bodyJson
|
|
816
|
+
);
|
|
817
|
+
const response = await fetch(`${baseUrl}${path}`, {
|
|
818
|
+
method: "POST",
|
|
819
|
+
headers: {
|
|
820
|
+
"content-type": "application/json",
|
|
821
|
+
...headers
|
|
822
|
+
},
|
|
823
|
+
body: bodyJson
|
|
824
|
+
});
|
|
825
|
+
const text = await response.text();
|
|
826
|
+
if (!response.ok) {
|
|
827
|
+
process.stderr.write(`HTTP ${response.status}: ${text}
|
|
828
|
+
`);
|
|
829
|
+
process.exit(1);
|
|
830
|
+
}
|
|
831
|
+
const parsed = JSON.parse(text);
|
|
832
|
+
process.stdout.write(`feedbackId: ${parsed.feedbackId ?? ""}
|
|
833
|
+
`);
|
|
834
|
+
process.stdout.write(`txHash: ${parsed.txHash ?? ""}
|
|
835
|
+
`);
|
|
836
|
+
process.stdout.write(`publicUrl: ${parsed.publicUrl ?? ""}
|
|
837
|
+
`);
|
|
838
|
+
}
|
|
486
839
|
async function runCli(argv = process.argv) {
|
|
487
840
|
const program = new import_commander.Command();
|
|
488
841
|
program.name("keeperhub-wallet").description(
|
|
@@ -502,6 +855,27 @@ async function runCli(argv = process.argv) {
|
|
|
502
855
|
program.command("info").description("Print subOrgId and walletAddress from local config").action(async () => {
|
|
503
856
|
await cmdInfo();
|
|
504
857
|
});
|
|
858
|
+
program.command("feedback").description(
|
|
859
|
+
"Submit ERC-8004 ReputationRegistry feedback for a workflow execution this wallet paid for. Signs giveFeedback() via Turnkey and broadcasts on Ethereum mainnet. Caller wallet pays gas natively."
|
|
860
|
+
).requiredOption(
|
|
861
|
+
"--execution-id <id>",
|
|
862
|
+
"workflow execution id to leave feedback for"
|
|
863
|
+
).requiredOption(
|
|
864
|
+
"--value <int>",
|
|
865
|
+
"raw int128 rating value (e.g. 5 with --decimals 0 for a 5-star rating)"
|
|
866
|
+
).option(
|
|
867
|
+
"--decimals <int>",
|
|
868
|
+
"decimals for value (0..18); 0 for integer scores, 1 for 0.1-step",
|
|
869
|
+
"0"
|
|
870
|
+
).option("--comment <text>", "optional plaintext comment").option(
|
|
871
|
+
"--agent-id <id>",
|
|
872
|
+
"rated agent NFT id (uint256 decimal); defaults to KeeperHub agent 31875"
|
|
873
|
+
).option(
|
|
874
|
+
"--chain-id <int>",
|
|
875
|
+
"agent chain id; defaults to 1 (Ethereum mainnet, only chain supported today)"
|
|
876
|
+
).option("--base-url <url>", "KeeperHub API base URL").action(async (opts) => {
|
|
877
|
+
await cmdFeedback(opts);
|
|
878
|
+
});
|
|
505
879
|
program.command("skill").description(
|
|
506
880
|
"Install the KeeperHub skill file into detected agent directories"
|
|
507
881
|
).addCommand(
|
|
@@ -524,6 +898,29 @@ async function runCli(argv = process.argv) {
|
|
|
524
898
|
} else if (reg.status === "notice") {
|
|
525
899
|
process.stderr.write(
|
|
526
900
|
`notice: ${reg.agent} -> ${reg.message ?? ""}
|
|
901
|
+
`
|
|
902
|
+
);
|
|
903
|
+
} else if (reg.status === "failed") {
|
|
904
|
+
process.stderr.write(
|
|
905
|
+
`hook: ${reg.agent} -> FAILED (${reg.message ?? "unknown error"})
|
|
906
|
+
`
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
for (const reg of result.mcpRegistrations) {
|
|
911
|
+
if (reg.status === "registered") {
|
|
912
|
+
process.stdout.write(
|
|
913
|
+
`mcp: ${reg.agent} -> registered at ${reg.path ?? "(unknown path)"}
|
|
914
|
+
`
|
|
915
|
+
);
|
|
916
|
+
} else if (reg.status === "notice") {
|
|
917
|
+
process.stderr.write(
|
|
918
|
+
`notice: ${reg.agent} mcp -> ${reg.message ?? ""}
|
|
919
|
+
`
|
|
920
|
+
);
|
|
921
|
+
} else if (reg.status === "failed") {
|
|
922
|
+
process.stderr.write(
|
|
923
|
+
`mcp: ${reg.agent} -> FAILED (${reg.message ?? "unknown error"})
|
|
527
924
|
`
|
|
528
925
|
);
|
|
529
926
|
}
|