@keeperhub/wallet 0.1.11 → 0.1.12
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 +16 -0
- package/bin/keeperhub-wallet-mcp.js +21 -0
- package/dist/cli.cjs +461 -165
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +475 -164
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +475 -177
- 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 +486 -173
- package/dist/index.js.map +1 -1
- package/dist/mcp-server.cjs +1206 -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 +1184 -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 +8 -1
package/dist/index.js
CHANGED
|
@@ -7,31 +7,51 @@ var AGENT_SPECS = [
|
|
|
7
7
|
agent: "claude-code",
|
|
8
8
|
skillsRel: [".claude", "skills"],
|
|
9
9
|
settingsRel: [".claude", "settings.json"],
|
|
10
|
-
hookSupport: "claude-code"
|
|
10
|
+
hookSupport: "claude-code",
|
|
11
|
+
// ~/.claude.json is at HOME root (not under .claude/) and is large
|
|
12
|
+
// (100+KB on real installs). registerMcpServer reads/parses/rewrites it
|
|
13
|
+
// while preserving every other top-level key byte-for-byte.
|
|
14
|
+
mcpConfigRel: [".claude.json"],
|
|
15
|
+
mcpSupport: "claude-code"
|
|
11
16
|
},
|
|
12
17
|
{
|
|
13
18
|
agent: "cursor",
|
|
14
19
|
skillsRel: [".cursor", "skills"],
|
|
15
20
|
settingsRel: [".cursor", "settings.json"],
|
|
16
|
-
hookSupport: "notice"
|
|
21
|
+
hookSupport: "notice",
|
|
22
|
+
mcpConfigRel: [".cursor", "mcp.json"],
|
|
23
|
+
mcpSupport: "cursor"
|
|
17
24
|
},
|
|
18
25
|
{
|
|
19
26
|
agent: "cline",
|
|
20
27
|
skillsRel: [".cline", "skills"],
|
|
21
28
|
settingsRel: [".cline", "settings.json"],
|
|
22
|
-
hookSupport: "notice"
|
|
29
|
+
hookSupport: "notice",
|
|
30
|
+
// Cline keeps MCP state in a per-VS-Code-variant globalStorage path
|
|
31
|
+
// (e.g. ~/Library/Application Support/Code/User/globalStorage/
|
|
32
|
+
// saoudrizwan.claude-dev/settings/cline_mcp_settings.json) that is too
|
|
33
|
+
// fragile to auto-detect. Ship "notice" with a copy-paste entry shape
|
|
34
|
+
// instead of guessing the variant.
|
|
35
|
+
mcpSupport: "notice"
|
|
23
36
|
},
|
|
24
37
|
{
|
|
25
38
|
agent: "windsurf",
|
|
26
39
|
skillsRel: [".windsurf", "skills"],
|
|
27
40
|
settingsRel: [".windsurf", "settings.json"],
|
|
28
|
-
hookSupport: "notice"
|
|
41
|
+
hookSupport: "notice",
|
|
42
|
+
mcpConfigRel: [".codeium", "windsurf", "mcp_config.json"],
|
|
43
|
+
mcpSupport: "windsurf",
|
|
44
|
+
// Windsurf historically ships under both `.windsurf/` and the legacy
|
|
45
|
+
// `.codeium/windsurf/`; detect either.
|
|
46
|
+
extraDetect: [[".codeium", "windsurf"]]
|
|
29
47
|
},
|
|
30
48
|
{
|
|
31
49
|
agent: "opencode",
|
|
32
50
|
skillsRel: [".config", "opencode", "skills"],
|
|
33
51
|
settingsRel: [".config", "opencode", "settings.json"],
|
|
34
|
-
hookSupport: "notice"
|
|
52
|
+
hookSupport: "notice",
|
|
53
|
+
mcpConfigRel: [".config", "opencode", "opencode.json"],
|
|
54
|
+
mcpSupport: "opencode"
|
|
35
55
|
}
|
|
36
56
|
];
|
|
37
57
|
function detectAgents(homeOverride) {
|
|
@@ -40,14 +60,26 @@ function detectAgents(homeOverride) {
|
|
|
40
60
|
for (const spec of AGENT_SPECS) {
|
|
41
61
|
const skillsDir = join(home, ...spec.skillsRel);
|
|
42
62
|
const settingsFile = join(home, ...spec.settingsRel);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
63
|
+
let detected = existsSync(dirname(skillsDir));
|
|
64
|
+
if (!detected && spec.extraDetect) {
|
|
65
|
+
for (const seg of spec.extraDetect) {
|
|
66
|
+
if (existsSync(join(home, ...seg))) {
|
|
67
|
+
detected = true;
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
50
71
|
}
|
|
72
|
+
if (!detected) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
results.push({
|
|
76
|
+
agent: spec.agent,
|
|
77
|
+
skillsDir,
|
|
78
|
+
settingsFile,
|
|
79
|
+
hookSupport: spec.hookSupport,
|
|
80
|
+
mcpConfigRel: spec.mcpConfigRel,
|
|
81
|
+
mcpSupport: spec.mcpSupport
|
|
82
|
+
});
|
|
51
83
|
}
|
|
52
84
|
return results;
|
|
53
85
|
}
|
|
@@ -146,19 +178,184 @@ function fund(walletAddress) {
|
|
|
146
178
|
};
|
|
147
179
|
}
|
|
148
180
|
|
|
181
|
+
// src/storage.ts
|
|
182
|
+
import { randomBytes } from "crypto";
|
|
183
|
+
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
184
|
+
import { homedir as homedir2 } from "os";
|
|
185
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
186
|
+
|
|
187
|
+
// src/types.ts
|
|
188
|
+
var KeeperHubError = class extends Error {
|
|
189
|
+
code;
|
|
190
|
+
constructor(code, message) {
|
|
191
|
+
super(message);
|
|
192
|
+
this.name = "KeeperHubError";
|
|
193
|
+
this.code = code;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
var WalletConfigMissingError = class extends Error {
|
|
197
|
+
constructor() {
|
|
198
|
+
super(
|
|
199
|
+
"Wallet config not found at ~/.keeperhub/wallet.json. Run `npx @keeperhub/wallet add` to provision."
|
|
200
|
+
);
|
|
201
|
+
this.name = "WalletConfigMissingError";
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
var WalletConfigCorruptError = class extends Error {
|
|
205
|
+
path;
|
|
206
|
+
constructor(path, reason) {
|
|
207
|
+
super(
|
|
208
|
+
`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).`
|
|
209
|
+
);
|
|
210
|
+
this.name = "WalletConfigCorruptError";
|
|
211
|
+
this.path = path;
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// src/storage.ts
|
|
216
|
+
async function readWalletConfig() {
|
|
217
|
+
const walletPath = join2(homedir2(), ".keeperhub", "wallet.json");
|
|
218
|
+
let raw;
|
|
219
|
+
try {
|
|
220
|
+
raw = await readFile(walletPath, "utf-8");
|
|
221
|
+
} catch (err) {
|
|
222
|
+
if (err.code === "ENOENT") {
|
|
223
|
+
throw new WalletConfigMissingError();
|
|
224
|
+
}
|
|
225
|
+
throw err;
|
|
226
|
+
}
|
|
227
|
+
let parsed;
|
|
228
|
+
try {
|
|
229
|
+
parsed = JSON.parse(raw);
|
|
230
|
+
} catch (err) {
|
|
231
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
232
|
+
throw new WalletConfigCorruptError(walletPath, reason);
|
|
233
|
+
}
|
|
234
|
+
if (!(parsed.subOrgId && parsed.walletAddress && parsed.hmacSecret)) {
|
|
235
|
+
throw new WalletConfigCorruptError(walletPath, "missing required fields");
|
|
236
|
+
}
|
|
237
|
+
return parsed;
|
|
238
|
+
}
|
|
239
|
+
async function writeWalletConfig(config) {
|
|
240
|
+
const walletPath = join2(homedir2(), ".keeperhub", "wallet.json");
|
|
241
|
+
await mkdir(dirname2(walletPath), { recursive: true, mode: 448 });
|
|
242
|
+
const suffix = randomBytes(8).toString("hex");
|
|
243
|
+
const tmpPath = `${walletPath}.${process.pid}.${suffix}.tmp`;
|
|
244
|
+
await writeFile(tmpPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
245
|
+
await chmod(tmpPath, 384);
|
|
246
|
+
await rename(tmpPath, walletPath);
|
|
247
|
+
}
|
|
248
|
+
function getWalletConfigPath() {
|
|
249
|
+
return join2(homedir2(), ".keeperhub", "wallet.json");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// src/provision.ts
|
|
253
|
+
var TRAILING_SLASH = /\/$/;
|
|
254
|
+
var WALLET_ADDRESS_PATTERN = /^0x[a-fA-F0-9]{40}$/;
|
|
255
|
+
var ProvisionResponseInvalidError = class extends Error {
|
|
256
|
+
code = "PROVISION_RESPONSE_INVALID";
|
|
257
|
+
constructor(message) {
|
|
258
|
+
super(message);
|
|
259
|
+
this.name = "ProvisionResponseInvalidError";
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
var ProvisionHttpError = class extends Error {
|
|
263
|
+
code = "PROVISION_HTTP_ERROR";
|
|
264
|
+
status;
|
|
265
|
+
body;
|
|
266
|
+
constructor(status, body) {
|
|
267
|
+
super(`provision failed: HTTP ${status}: ${body}`);
|
|
268
|
+
this.name = "ProvisionHttpError";
|
|
269
|
+
this.status = status;
|
|
270
|
+
this.body = body;
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
function resolveBaseUrl(override) {
|
|
274
|
+
const candidate = override ?? process.env.KEEPERHUB_API_URL ?? "https://app.keeperhub.com";
|
|
275
|
+
return candidate.replace(TRAILING_SLASH, "");
|
|
276
|
+
}
|
|
277
|
+
function isNonEmptyString(value) {
|
|
278
|
+
return typeof value === "string" && value.length > 0;
|
|
279
|
+
}
|
|
280
|
+
function validateProvisionResponse(data) {
|
|
281
|
+
if (typeof data !== "object" || data === null) {
|
|
282
|
+
throw new ProvisionResponseInvalidError(
|
|
283
|
+
"provision response is not an object"
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
const { subOrgId, walletAddress, hmacSecret } = data;
|
|
287
|
+
if (!(isNonEmptyString(subOrgId) && isNonEmptyString(walletAddress) && isNonEmptyString(hmacSecret))) {
|
|
288
|
+
throw new ProvisionResponseInvalidError(
|
|
289
|
+
"provision response missing subOrgId, walletAddress, or hmacSecret"
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
if (!WALLET_ADDRESS_PATTERN.test(walletAddress)) {
|
|
293
|
+
throw new ProvisionResponseInvalidError(
|
|
294
|
+
`provision response walletAddress is not a valid 0x-prefixed 40-hex address: ${walletAddress}`
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
subOrgId,
|
|
299
|
+
walletAddress,
|
|
300
|
+
hmacSecret
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
async function provisionWallet(options = {}) {
|
|
304
|
+
const baseUrl = resolveBaseUrl(options.baseUrl);
|
|
305
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
306
|
+
const response = await fetchImpl(`${baseUrl}/api/agentic-wallet/provision`, {
|
|
307
|
+
method: "POST",
|
|
308
|
+
headers: { "content-type": "application/json" },
|
|
309
|
+
body: "{}",
|
|
310
|
+
signal: AbortSignal.timeout(3e4)
|
|
311
|
+
});
|
|
312
|
+
if (!response.ok) {
|
|
313
|
+
const text = await response.text();
|
|
314
|
+
throw new ProvisionHttpError(response.status, text);
|
|
315
|
+
}
|
|
316
|
+
const raw = await response.json();
|
|
317
|
+
const data = validateProvisionResponse(raw);
|
|
318
|
+
await writeWalletConfig(data);
|
|
319
|
+
return data;
|
|
320
|
+
}
|
|
321
|
+
|
|
149
322
|
// src/skill-install.ts
|
|
323
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
324
|
+
import {
|
|
325
|
+
chmod as chmod3,
|
|
326
|
+
copyFile,
|
|
327
|
+
mkdir as mkdir3,
|
|
328
|
+
readFile as readFile3,
|
|
329
|
+
rename as rename3,
|
|
330
|
+
unlink as unlink2,
|
|
331
|
+
writeFile as writeFile3
|
|
332
|
+
} from "fs/promises";
|
|
333
|
+
import { dirname as dirname5, join as join5 } from "path";
|
|
334
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
335
|
+
|
|
336
|
+
// src/mcp-register.ts
|
|
337
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
338
|
+
import {
|
|
339
|
+
chmod as chmod2,
|
|
340
|
+
mkdir as mkdir2,
|
|
341
|
+
readFile as readFile2,
|
|
342
|
+
rename as rename2,
|
|
343
|
+
unlink,
|
|
344
|
+
writeFile as writeFile2
|
|
345
|
+
} from "fs/promises";
|
|
346
|
+
import { homedir as homedir3 } from "os";
|
|
347
|
+
import { dirname as dirname4, join as join4 } from "path";
|
|
348
|
+
|
|
349
|
+
// src/runtime-detect.ts
|
|
150
350
|
import { execFileSync } from "child_process";
|
|
151
351
|
import { readFileSync } from "fs";
|
|
152
|
-
import {
|
|
153
|
-
import { dirname as dirname2, join as join2 } from "path";
|
|
352
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
154
353
|
import { fileURLToPath } from "url";
|
|
155
|
-
var HOOK_BIN = "keeperhub-wallet-hook";
|
|
156
|
-
var HOOK_COMMAND_BARE = HOOK_BIN;
|
|
157
354
|
var PACKAGE_NAME = "@keeperhub/wallet";
|
|
158
355
|
function readPackageVersion() {
|
|
159
356
|
try {
|
|
160
|
-
const here =
|
|
161
|
-
const pkgPath =
|
|
357
|
+
const here = dirname3(fileURLToPath(import.meta.url));
|
|
358
|
+
const pkgPath = join3(here, "..", "package.json");
|
|
162
359
|
const raw = readFileSync(pkgPath, "utf-8");
|
|
163
360
|
const parsed = JSON.parse(raw);
|
|
164
361
|
if (typeof parsed.version === "string" && parsed.version.length > 0) {
|
|
@@ -168,9 +365,6 @@ function readPackageVersion() {
|
|
|
168
365
|
}
|
|
169
366
|
return "latest";
|
|
170
367
|
}
|
|
171
|
-
function buildNpxCommand(version) {
|
|
172
|
-
return `npx -y -p ${PACKAGE_NAME}@${version} ${HOOK_BIN}`;
|
|
173
|
-
}
|
|
174
368
|
function isNpxExecution() {
|
|
175
369
|
const execPath = process.env.npm_execpath;
|
|
176
370
|
if (typeof execPath !== "string" || execPath.length === 0) {
|
|
@@ -198,6 +392,144 @@ function isPathUnderTransientCache(resolvedPath) {
|
|
|
198
392
|
}
|
|
199
393
|
return false;
|
|
200
394
|
}
|
|
395
|
+
function resolveBinCommand(binName) {
|
|
396
|
+
const version = readPackageVersion();
|
|
397
|
+
const npxArgs = ["-y", "-p", `${PACKAGE_NAME}@${version}`, binName];
|
|
398
|
+
const npxCommandString = `npx ${npxArgs.join(" ")}`;
|
|
399
|
+
if (isNpxExecution()) {
|
|
400
|
+
return {
|
|
401
|
+
commandString: npxCommandString,
|
|
402
|
+
command: "npx",
|
|
403
|
+
args: npxArgs
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
try {
|
|
407
|
+
const resolved = execFileSync("/bin/sh", ["-c", `command -v ${binName}`], {
|
|
408
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
409
|
+
}).toString().trim();
|
|
410
|
+
if (resolved.length > 0 && !isPathUnderTransientCache(resolved)) {
|
|
411
|
+
return {
|
|
412
|
+
commandString: binName,
|
|
413
|
+
command: binName,
|
|
414
|
+
args: []
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
} catch {
|
|
418
|
+
}
|
|
419
|
+
return {
|
|
420
|
+
commandString: npxCommandString,
|
|
421
|
+
command: "npx",
|
|
422
|
+
args: npxArgs
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// src/mcp-register.ts
|
|
427
|
+
var MCP_BIN = "keeperhub-wallet-mcp";
|
|
428
|
+
var MCP_SERVER_NAME = "keeperhub-wallet";
|
|
429
|
+
function resolveMcpCommand() {
|
|
430
|
+
const envOverride = process.env.KEEPERHUB_WALLET_MCP_COMMAND;
|
|
431
|
+
if (envOverride && envOverride.length > 0) {
|
|
432
|
+
const parts = envOverride.trim().split(/\s+/);
|
|
433
|
+
const head = parts[0] ?? envOverride;
|
|
434
|
+
return { command: head, args: parts.slice(1) };
|
|
435
|
+
}
|
|
436
|
+
const resolved = resolveBinCommand(MCP_BIN);
|
|
437
|
+
return { command: resolved.command, args: resolved.args };
|
|
438
|
+
}
|
|
439
|
+
function buildStandardEntry(cmd) {
|
|
440
|
+
const entry = {
|
|
441
|
+
command: cmd.command,
|
|
442
|
+
args: cmd.args
|
|
443
|
+
};
|
|
444
|
+
if (cmd.env && Object.keys(cmd.env).length > 0) {
|
|
445
|
+
entry.env = cmd.env;
|
|
446
|
+
}
|
|
447
|
+
return entry;
|
|
448
|
+
}
|
|
449
|
+
function buildOpencodeEntry(cmd) {
|
|
450
|
+
return {
|
|
451
|
+
type: "local",
|
|
452
|
+
command: [cmd.command, ...cmd.args],
|
|
453
|
+
enabled: true,
|
|
454
|
+
environment: cmd.env ?? {}
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
async function readJsonOrEmpty(path) {
|
|
458
|
+
let raw = null;
|
|
459
|
+
try {
|
|
460
|
+
raw = await readFile2(path, "utf-8");
|
|
461
|
+
} catch (err) {
|
|
462
|
+
if (err.code !== "ENOENT") {
|
|
463
|
+
throw err;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (raw === null) {
|
|
467
|
+
return {};
|
|
468
|
+
}
|
|
469
|
+
try {
|
|
470
|
+
return JSON.parse(raw);
|
|
471
|
+
} catch {
|
|
472
|
+
throw new Error(
|
|
473
|
+
`MCP config at ${path} is not valid JSON; aborting MCP registration`
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
async function writeJsonAtomic(path, payload) {
|
|
478
|
+
await mkdir2(dirname4(path), { recursive: true, mode: 448 });
|
|
479
|
+
const suffix = randomBytes2(8).toString("hex");
|
|
480
|
+
const tmpPath = `${path}.${process.pid}.${suffix}.tmp`;
|
|
481
|
+
try {
|
|
482
|
+
await writeFile2(tmpPath, payload, { mode: 384 });
|
|
483
|
+
await chmod2(tmpPath, 384);
|
|
484
|
+
await rename2(tmpPath, path);
|
|
485
|
+
} catch (err) {
|
|
486
|
+
await unlink(tmpPath).catch(() => {
|
|
487
|
+
});
|
|
488
|
+
throw err;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
async function writeStandardMcp(path, entry) {
|
|
492
|
+
const config = await readJsonOrEmpty(path);
|
|
493
|
+
const servers = typeof config.mcpServers === "object" && config.mcpServers !== null ? config.mcpServers : {};
|
|
494
|
+
servers[MCP_SERVER_NAME] = entry;
|
|
495
|
+
config.mcpServers = servers;
|
|
496
|
+
const payload = `${JSON.stringify(config, null, 2)}
|
|
497
|
+
`;
|
|
498
|
+
await writeJsonAtomic(path, payload);
|
|
499
|
+
}
|
|
500
|
+
async function writeOpencodeMcp(path, entry) {
|
|
501
|
+
const config = await readJsonOrEmpty(path);
|
|
502
|
+
const servers = typeof config.mcp === "object" && config.mcp !== null ? config.mcp : {};
|
|
503
|
+
servers[MCP_SERVER_NAME] = entry;
|
|
504
|
+
config.mcp = servers;
|
|
505
|
+
const payload = `${JSON.stringify(config, null, 2)}
|
|
506
|
+
`;
|
|
507
|
+
await writeJsonAtomic(path, payload);
|
|
508
|
+
}
|
|
509
|
+
async function registerMcpServer(target, options = {}) {
|
|
510
|
+
if (target.mcpSupport === "notice") {
|
|
511
|
+
throw new Error(
|
|
512
|
+
`agent ${target.agent} does not support auto-registered MCP servers; surface a notice instead`
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
if (!target.mcpConfigRel) {
|
|
516
|
+
throw new Error(
|
|
517
|
+
`agent ${target.agent} has mcpSupport=${target.mcpSupport} but no mcpConfigRel path`
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
const home = options.homeOverride ?? homedir3();
|
|
521
|
+
const path = join4(home, ...target.mcpConfigRel);
|
|
522
|
+
const cmd = options.command ?? resolveMcpCommand();
|
|
523
|
+
if (target.mcpSupport === "opencode") {
|
|
524
|
+
await writeOpencodeMcp(path, buildOpencodeEntry(cmd));
|
|
525
|
+
} else {
|
|
526
|
+
await writeStandardMcp(path, buildStandardEntry(cmd));
|
|
527
|
+
}
|
|
528
|
+
return { path, name: MCP_SERVER_NAME };
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// src/skill-install.ts
|
|
532
|
+
var HOOK_BIN = "keeperhub-wallet-hook";
|
|
201
533
|
var KEEPERHUB_HOOK_MARKER = HOOK_BIN;
|
|
202
534
|
function filterKeeperhubHooksFromEntry(entry) {
|
|
203
535
|
if (typeof entry !== "object" || entry === null) {
|
|
@@ -224,19 +556,7 @@ function resolveHookCommand() {
|
|
|
224
556
|
if (envOverride && envOverride.length > 0) {
|
|
225
557
|
return envOverride;
|
|
226
558
|
}
|
|
227
|
-
|
|
228
|
-
return buildNpxCommand(readPackageVersion());
|
|
229
|
-
}
|
|
230
|
-
try {
|
|
231
|
-
const resolved = execFileSync("/bin/sh", ["-c", `command -v ${HOOK_BIN}`], {
|
|
232
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
233
|
-
}).toString().trim();
|
|
234
|
-
if (resolved.length > 0 && !isPathUnderTransientCache(resolved)) {
|
|
235
|
-
return HOOK_COMMAND_BARE;
|
|
236
|
-
}
|
|
237
|
-
} catch {
|
|
238
|
-
}
|
|
239
|
-
return buildNpxCommand(readPackageVersion());
|
|
559
|
+
return resolveBinCommand(HOOK_BIN).commandString;
|
|
240
560
|
}
|
|
241
561
|
function buildKeeperhubEntry(command) {
|
|
242
562
|
return {
|
|
@@ -245,8 +565,8 @@ function buildKeeperhubEntry(command) {
|
|
|
245
565
|
};
|
|
246
566
|
}
|
|
247
567
|
function resolveDefaultSkillSource() {
|
|
248
|
-
const here =
|
|
249
|
-
return
|
|
568
|
+
const here = dirname5(fileURLToPath2(import.meta.url));
|
|
569
|
+
return join5(here, "..", "skill", "keeperhub-wallet.skill.md");
|
|
250
570
|
}
|
|
251
571
|
function defaultNotice(msg) {
|
|
252
572
|
process.stderr.write(`${msg}
|
|
@@ -256,7 +576,7 @@ async function registerClaudeCodeHook(settingsPath, options = {}) {
|
|
|
256
576
|
const command = options.hookCommand ?? resolveHookCommand();
|
|
257
577
|
let raw = null;
|
|
258
578
|
try {
|
|
259
|
-
raw = await
|
|
579
|
+
raw = await readFile3(settingsPath, "utf-8");
|
|
260
580
|
} catch (err) {
|
|
261
581
|
if (err.code !== "ENOENT") {
|
|
262
582
|
throw err;
|
|
@@ -284,166 +604,136 @@ async function registerClaudeCodeHook(settingsPath, options = {}) {
|
|
|
284
604
|
filtered.push(buildKeeperhubEntry(command));
|
|
285
605
|
hooks.PreToolUse = filtered;
|
|
286
606
|
config.hooks = hooks;
|
|
287
|
-
await
|
|
607
|
+
await mkdir3(dirname5(settingsPath), { recursive: true, mode: 448 });
|
|
288
608
|
const payload = `${JSON.stringify(config, null, 2)}
|
|
289
609
|
`;
|
|
290
|
-
|
|
291
|
-
|
|
610
|
+
const suffix = randomBytes3(8).toString("hex");
|
|
611
|
+
const tmpPath = `${settingsPath}.${process.pid}.${suffix}.tmp`;
|
|
612
|
+
try {
|
|
613
|
+
await writeFile3(tmpPath, payload, { mode: 384 });
|
|
614
|
+
await chmod3(tmpPath, 384);
|
|
615
|
+
await rename3(tmpPath, settingsPath);
|
|
616
|
+
} catch (err) {
|
|
617
|
+
await unlink2(tmpPath).catch(() => {
|
|
618
|
+
});
|
|
619
|
+
throw err;
|
|
620
|
+
}
|
|
292
621
|
}
|
|
293
622
|
async function writeSkillToAgent(agent, skillSource) {
|
|
294
|
-
await
|
|
295
|
-
const target =
|
|
623
|
+
await mkdir3(agent.skillsDir, { recursive: true, mode: 493 });
|
|
624
|
+
const target = join5(agent.skillsDir, "keeperhub-wallet.skill.md");
|
|
296
625
|
await copyFile(skillSource, target);
|
|
297
|
-
await
|
|
626
|
+
await chmod3(target, 420);
|
|
298
627
|
return { agent: agent.agent, path: target, status: "written" };
|
|
299
628
|
}
|
|
300
|
-
function
|
|
629
|
+
function buildHookNoticeMessage(agent, command) {
|
|
301
630
|
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}`;
|
|
302
631
|
}
|
|
632
|
+
function buildMcpNoticeMessage(agent, command) {
|
|
633
|
+
const cmd = [command.command, ...command.args].join(" ");
|
|
634
|
+
return `${agent.agent} does not support auto-registered MCP servers; add an entry named \`keeperhub-wallet\` running \`${cmd}\` to your MCP config manually`;
|
|
635
|
+
}
|
|
303
636
|
async function installSkill(options = {}) {
|
|
304
637
|
const agents = detectAgents(options.homeOverride);
|
|
305
638
|
const skillSource = options.skillSourcePath ?? resolveDefaultSkillSource();
|
|
306
639
|
const onNotice = options.onNotice ?? defaultNotice;
|
|
307
640
|
const hookCommand = options.hookCommand ?? resolveHookCommand();
|
|
641
|
+
const mcpCommand = options.mcpCommand ?? resolveMcpCommand();
|
|
308
642
|
const skillWrites = [];
|
|
309
643
|
const hookRegistrations = [];
|
|
644
|
+
const mcpRegistrations = [];
|
|
310
645
|
for (const agent of agents) {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
646
|
+
try {
|
|
647
|
+
const write = await writeSkillToAgent(agent, skillSource);
|
|
648
|
+
skillWrites.push(write);
|
|
649
|
+
} catch (err) {
|
|
650
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
651
|
+
skillWrites.push({
|
|
316
652
|
agent: agent.agent,
|
|
317
|
-
|
|
653
|
+
path: "",
|
|
654
|
+
status: "skipped"
|
|
318
655
|
});
|
|
656
|
+
onNotice(`${agent.agent}: skill copy failed (${message})`);
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
if (agent.hookSupport === "claude-code") {
|
|
660
|
+
try {
|
|
661
|
+
await registerClaudeCodeHook(agent.settingsFile, { hookCommand });
|
|
662
|
+
hookRegistrations.push({
|
|
663
|
+
agent: agent.agent,
|
|
664
|
+
status: "registered"
|
|
665
|
+
});
|
|
666
|
+
} catch (err) {
|
|
667
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
668
|
+
hookRegistrations.push({
|
|
669
|
+
agent: agent.agent,
|
|
670
|
+
status: "failed",
|
|
671
|
+
message
|
|
672
|
+
});
|
|
673
|
+
onNotice(`${agent.agent}: hook registration failed (${message})`);
|
|
674
|
+
}
|
|
319
675
|
} else {
|
|
320
|
-
const
|
|
676
|
+
const noticeMessage = buildHookNoticeMessage(agent, hookCommand);
|
|
321
677
|
hookRegistrations.push({
|
|
322
678
|
agent: agent.agent,
|
|
323
679
|
status: "notice",
|
|
324
|
-
message
|
|
680
|
+
message: noticeMessage
|
|
325
681
|
});
|
|
326
|
-
onNotice(
|
|
682
|
+
onNotice(noticeMessage);
|
|
327
683
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
async function readWalletConfig() {
|
|
357
|
-
const walletPath = join3(homedir2(), ".keeperhub", "wallet.json");
|
|
358
|
-
let raw;
|
|
359
|
-
try {
|
|
360
|
-
raw = await readFile2(walletPath, "utf-8");
|
|
361
|
-
} catch (err) {
|
|
362
|
-
if (err.code === "ENOENT") {
|
|
363
|
-
throw new WalletConfigMissingError();
|
|
684
|
+
if (agent.mcpSupport === "notice") {
|
|
685
|
+
const noticeMessage = buildMcpNoticeMessage(agent, mcpCommand);
|
|
686
|
+
mcpRegistrations.push({
|
|
687
|
+
agent: agent.agent,
|
|
688
|
+
status: "notice",
|
|
689
|
+
message: noticeMessage
|
|
690
|
+
});
|
|
691
|
+
onNotice(noticeMessage);
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
try {
|
|
695
|
+
const mcpResult = await registerMcpServer(agent, {
|
|
696
|
+
homeOverride: options.homeOverride,
|
|
697
|
+
command: mcpCommand
|
|
698
|
+
});
|
|
699
|
+
mcpRegistrations.push({
|
|
700
|
+
agent: agent.agent,
|
|
701
|
+
status: "registered",
|
|
702
|
+
path: mcpResult.path
|
|
703
|
+
});
|
|
704
|
+
} catch (err) {
|
|
705
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
706
|
+
mcpRegistrations.push({
|
|
707
|
+
agent: agent.agent,
|
|
708
|
+
status: "failed",
|
|
709
|
+
message
|
|
710
|
+
});
|
|
711
|
+
onNotice(`${agent.agent}: MCP registration failed (${message})`);
|
|
364
712
|
}
|
|
365
|
-
throw err;
|
|
366
|
-
}
|
|
367
|
-
const parsed = JSON.parse(raw);
|
|
368
|
-
if (!(parsed.subOrgId && parsed.walletAddress && parsed.hmacSecret)) {
|
|
369
|
-
throw new Error(`Malformed wallet.json at ${walletPath}`);
|
|
370
713
|
}
|
|
371
|
-
return
|
|
372
|
-
}
|
|
373
|
-
async function writeWalletConfig(config) {
|
|
374
|
-
const walletPath = join3(homedir2(), ".keeperhub", "wallet.json");
|
|
375
|
-
await mkdir2(dirname3(walletPath), { recursive: true, mode: 448 });
|
|
376
|
-
await writeFile2(walletPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
377
|
-
await chmod2(walletPath, 384);
|
|
378
|
-
}
|
|
379
|
-
function getWalletConfigPath() {
|
|
380
|
-
return join3(homedir2(), ".keeperhub", "wallet.json");
|
|
714
|
+
return { skillWrites, hookRegistrations, mcpRegistrations };
|
|
381
715
|
}
|
|
382
716
|
|
|
383
717
|
// src/cli.ts
|
|
384
|
-
var TRAILING_SLASH = /\/$/;
|
|
385
|
-
var WALLET_ADDRESS_PATTERN = /^0x[a-fA-F0-9]{40}$/;
|
|
386
|
-
function resolveBaseUrl(override) {
|
|
387
|
-
const candidate = override ?? process.env.KEEPERHUB_API_URL ?? "https://app.keeperhub.com";
|
|
388
|
-
return candidate.replace(TRAILING_SLASH, "");
|
|
389
|
-
}
|
|
390
|
-
function isNonEmptyString(value) {
|
|
391
|
-
return typeof value === "string" && value.length > 0;
|
|
392
|
-
}
|
|
393
|
-
function provisionInvalidError(message) {
|
|
394
|
-
const err = new Error(message);
|
|
395
|
-
err.code = "PROVISION_RESPONSE_INVALID";
|
|
396
|
-
return err;
|
|
397
|
-
}
|
|
398
|
-
function validateProvisionResponse(data) {
|
|
399
|
-
if (typeof data !== "object" || data === null) {
|
|
400
|
-
throw provisionInvalidError("provision response is not an object");
|
|
401
|
-
}
|
|
402
|
-
const { subOrgId, walletAddress, hmacSecret } = data;
|
|
403
|
-
if (!(isNonEmptyString(subOrgId) && isNonEmptyString(walletAddress) && isNonEmptyString(hmacSecret))) {
|
|
404
|
-
throw provisionInvalidError(
|
|
405
|
-
"provision response missing subOrgId, walletAddress, or hmacSecret"
|
|
406
|
-
);
|
|
407
|
-
}
|
|
408
|
-
if (!WALLET_ADDRESS_PATTERN.test(walletAddress)) {
|
|
409
|
-
throw provisionInvalidError(
|
|
410
|
-
`provision response walletAddress is not a valid 0x-prefixed 40-hex address: ${walletAddress}`
|
|
411
|
-
);
|
|
412
|
-
}
|
|
413
|
-
return {
|
|
414
|
-
subOrgId,
|
|
415
|
-
walletAddress,
|
|
416
|
-
hmacSecret
|
|
417
|
-
};
|
|
418
|
-
}
|
|
419
718
|
async function cmdAdd(opts = {}) {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
headers: { "content-type": "application/json" },
|
|
424
|
-
body: "{}"
|
|
425
|
-
});
|
|
426
|
-
if (!response.ok) {
|
|
427
|
-
const text = await response.text();
|
|
428
|
-
process.stderr.write(
|
|
429
|
-
`[keeperhub-wallet] provision failed: HTTP ${response.status}: ${text}
|
|
430
|
-
`
|
|
431
|
-
);
|
|
432
|
-
process.exit(1);
|
|
433
|
-
}
|
|
434
|
-
const raw = await response.json();
|
|
435
|
-
const data = validateProvisionResponse(raw);
|
|
436
|
-
await writeWalletConfig({
|
|
437
|
-
subOrgId: data.subOrgId,
|
|
438
|
-
walletAddress: data.walletAddress,
|
|
439
|
-
hmacSecret: data.hmacSecret
|
|
440
|
-
});
|
|
441
|
-
process.stdout.write(`subOrgId: ${data.subOrgId}
|
|
719
|
+
try {
|
|
720
|
+
const data = await provisionWallet({ baseUrl: opts.baseUrl });
|
|
721
|
+
process.stdout.write(`subOrgId: ${data.subOrgId}
|
|
442
722
|
`);
|
|
443
|
-
|
|
723
|
+
process.stdout.write(`walletAddress: ${data.walletAddress}
|
|
444
724
|
`);
|
|
445
|
-
|
|
725
|
+
process.stdout.write(`config written to ${getWalletConfigPath()}
|
|
446
726
|
`);
|
|
727
|
+
} catch (err) {
|
|
728
|
+
if (err instanceof ProvisionHttpError) {
|
|
729
|
+
process.stderr.write(
|
|
730
|
+
`[keeperhub-wallet] provision failed: HTTP ${err.status}: ${err.body}
|
|
731
|
+
`
|
|
732
|
+
);
|
|
733
|
+
process.exit(1);
|
|
734
|
+
}
|
|
735
|
+
throw err;
|
|
736
|
+
}
|
|
447
737
|
}
|
|
448
738
|
async function cmdFund() {
|
|
449
739
|
const wallet = await readWalletConfig();
|
|
@@ -511,6 +801,29 @@ async function runCli(argv = process.argv) {
|
|
|
511
801
|
} else if (reg.status === "notice") {
|
|
512
802
|
process.stderr.write(
|
|
513
803
|
`notice: ${reg.agent} -> ${reg.message ?? ""}
|
|
804
|
+
`
|
|
805
|
+
);
|
|
806
|
+
} else if (reg.status === "failed") {
|
|
807
|
+
process.stderr.write(
|
|
808
|
+
`hook: ${reg.agent} -> FAILED (${reg.message ?? "unknown error"})
|
|
809
|
+
`
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
for (const reg of result.mcpRegistrations) {
|
|
814
|
+
if (reg.status === "registered") {
|
|
815
|
+
process.stdout.write(
|
|
816
|
+
`mcp: ${reg.agent} -> registered at ${reg.path ?? "(unknown path)"}
|
|
817
|
+
`
|
|
818
|
+
);
|
|
819
|
+
} else if (reg.status === "notice") {
|
|
820
|
+
process.stderr.write(
|
|
821
|
+
`notice: ${reg.agent} mcp -> ${reg.message ?? ""}
|
|
822
|
+
`
|
|
823
|
+
);
|
|
824
|
+
} else if (reg.status === "failed") {
|
|
825
|
+
process.stderr.write(
|
|
826
|
+
`mcp: ${reg.agent} -> FAILED (${reg.message ?? "unknown error"})
|
|
514
827
|
`
|
|
515
828
|
);
|
|
516
829
|
}
|
|
@@ -639,9 +952,9 @@ var KeeperHubClient = class {
|
|
|
639
952
|
};
|
|
640
953
|
|
|
641
954
|
// src/safety-config.ts
|
|
642
|
-
import { chmod as
|
|
643
|
-
import { homedir as
|
|
644
|
-
import { dirname as
|
|
955
|
+
import { chmod as chmod4, mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
|
|
956
|
+
import { homedir as homedir4 } from "os";
|
|
957
|
+
import { dirname as dirname6, join as join6 } from "path";
|
|
645
958
|
var DEFAULT_SAFETY_CONFIG = {
|
|
646
959
|
auto_approve_max_usd: 5,
|
|
647
960
|
ask_threshold_usd: 50,
|
|
@@ -654,20 +967,20 @@ var DEFAULT_SAFETY_CONFIG = {
|
|
|
654
967
|
]
|
|
655
968
|
};
|
|
656
969
|
function getSafetyPath() {
|
|
657
|
-
return
|
|
970
|
+
return join6(homedir4(), ".keeperhub", "safety.json");
|
|
658
971
|
}
|
|
659
972
|
async function loadSafetyConfig() {
|
|
660
973
|
const path = getSafetyPath();
|
|
661
974
|
let raw;
|
|
662
975
|
try {
|
|
663
|
-
raw = await
|
|
976
|
+
raw = await readFile4(path, "utf-8");
|
|
664
977
|
} catch (err) {
|
|
665
978
|
if (err.code === "ENOENT") {
|
|
666
|
-
await
|
|
667
|
-
await
|
|
979
|
+
await mkdir4(dirname6(path), { recursive: true, mode: 448 });
|
|
980
|
+
await writeFile4(path, JSON.stringify(DEFAULT_SAFETY_CONFIG, null, 2), {
|
|
668
981
|
mode: 420
|
|
669
982
|
});
|
|
670
|
-
await
|
|
983
|
+
await chmod4(path, 420);
|
|
671
984
|
return DEFAULT_SAFETY_CONFIG;
|
|
672
985
|
}
|
|
673
986
|
throw err;
|
|
@@ -864,7 +1177,7 @@ function parseMppChallenge(response) {
|
|
|
864
1177
|
}
|
|
865
1178
|
|
|
866
1179
|
// src/payment-signer.ts
|
|
867
|
-
import { randomBytes } from "crypto";
|
|
1180
|
+
import { randomBytes as randomBytes4 } from "crypto";
|
|
868
1181
|
|
|
869
1182
|
// src/workflow-slug.ts
|
|
870
1183
|
var KEEPERHUB_WORKFLOW_RE = /\/api\/mcp\/workflows\/([a-zA-Z0-9_-]+)\/call(?:\/?)(?:\?|$|#)/;
|
|
@@ -1044,7 +1357,7 @@ function createPaymentSigner(opts = {}) {
|
|
|
1044
1357
|
const now = Math.floor(Date.now() / 1e3);
|
|
1045
1358
|
const validAfter = now - VALID_AFTER_PAST_SLACK_SECONDS;
|
|
1046
1359
|
const validBefore = now + accept.maxTimeoutSeconds;
|
|
1047
|
-
const nonce = `0x${
|
|
1360
|
+
const nonce = `0x${randomBytes4(NONCE_BYTES).toString("hex")}`;
|
|
1048
1361
|
const client = clientFactory(wallet);
|
|
1049
1362
|
const signature = await signOrPoll(client, {
|
|
1050
1363
|
chain: "base",
|