@keeperhub/wallet 0.1.10 → 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 +487 -159
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +501 -158
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +501 -171
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +78 -239
- package/dist/index.d.ts +78 -239
- package/dist/index.js +512 -167
- 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 -2
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
|
-
import { chmod, copyFile, mkdir, readFile, writeFile } from "fs/promises";
|
|
152
351
|
import { readFileSync } from "fs";
|
|
153
|
-
import { dirname as
|
|
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,171 @@ function readPackageVersion() {
|
|
|
168
365
|
}
|
|
169
366
|
return "latest";
|
|
170
367
|
}
|
|
171
|
-
function
|
|
172
|
-
|
|
368
|
+
function isNpxExecution() {
|
|
369
|
+
const execPath = process.env.npm_execpath;
|
|
370
|
+
if (typeof execPath !== "string" || execPath.length === 0) {
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
if (/(?:^|[\\/])npx-cli\.(?:js|cjs|mjs)$/i.test(execPath)) {
|
|
374
|
+
return true;
|
|
375
|
+
}
|
|
376
|
+
if (/(?:^|[\\/])npx(?:\.cmd|\.exe|\.ps1)?$/i.test(execPath)) {
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
var TRANSIENT_CACHE_PATTERNS = [
|
|
382
|
+
/[\\/]_npx[\\/]/,
|
|
383
|
+
/[\\/]dlx-[A-Za-z0-9]+[\\/]/,
|
|
384
|
+
/[\\/]xfs-[A-Za-z0-9]+[\\/]/,
|
|
385
|
+
/[\\/]\.bun[\\/]install[\\/]cache[\\/]/
|
|
386
|
+
];
|
|
387
|
+
function isPathUnderTransientCache(resolvedPath) {
|
|
388
|
+
for (const re of TRANSIENT_CACHE_PATTERNS) {
|
|
389
|
+
if (re.test(resolvedPath)) {
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return false;
|
|
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 };
|
|
173
529
|
}
|
|
530
|
+
|
|
531
|
+
// src/skill-install.ts
|
|
532
|
+
var HOOK_BIN = "keeperhub-wallet-hook";
|
|
174
533
|
var KEEPERHUB_HOOK_MARKER = HOOK_BIN;
|
|
175
534
|
function filterKeeperhubHooksFromEntry(entry) {
|
|
176
535
|
if (typeof entry !== "object" || entry === null) {
|
|
@@ -197,14 +556,7 @@ function resolveHookCommand() {
|
|
|
197
556
|
if (envOverride && envOverride.length > 0) {
|
|
198
557
|
return envOverride;
|
|
199
558
|
}
|
|
200
|
-
|
|
201
|
-
execFileSync("/bin/sh", ["-c", `command -v ${HOOK_BIN}`], {
|
|
202
|
-
stdio: "ignore"
|
|
203
|
-
});
|
|
204
|
-
return HOOK_COMMAND_BARE;
|
|
205
|
-
} catch {
|
|
206
|
-
return buildNpxCommand(readPackageVersion());
|
|
207
|
-
}
|
|
559
|
+
return resolveBinCommand(HOOK_BIN).commandString;
|
|
208
560
|
}
|
|
209
561
|
function buildKeeperhubEntry(command) {
|
|
210
562
|
return {
|
|
@@ -213,8 +565,8 @@ function buildKeeperhubEntry(command) {
|
|
|
213
565
|
};
|
|
214
566
|
}
|
|
215
567
|
function resolveDefaultSkillSource() {
|
|
216
|
-
const here =
|
|
217
|
-
return
|
|
568
|
+
const here = dirname5(fileURLToPath2(import.meta.url));
|
|
569
|
+
return join5(here, "..", "skill", "keeperhub-wallet.skill.md");
|
|
218
570
|
}
|
|
219
571
|
function defaultNotice(msg) {
|
|
220
572
|
process.stderr.write(`${msg}
|
|
@@ -224,7 +576,7 @@ async function registerClaudeCodeHook(settingsPath, options = {}) {
|
|
|
224
576
|
const command = options.hookCommand ?? resolveHookCommand();
|
|
225
577
|
let raw = null;
|
|
226
578
|
try {
|
|
227
|
-
raw = await
|
|
579
|
+
raw = await readFile3(settingsPath, "utf-8");
|
|
228
580
|
} catch (err) {
|
|
229
581
|
if (err.code !== "ENOENT") {
|
|
230
582
|
throw err;
|
|
@@ -252,166 +604,136 @@ async function registerClaudeCodeHook(settingsPath, options = {}) {
|
|
|
252
604
|
filtered.push(buildKeeperhubEntry(command));
|
|
253
605
|
hooks.PreToolUse = filtered;
|
|
254
606
|
config.hooks = hooks;
|
|
255
|
-
await
|
|
607
|
+
await mkdir3(dirname5(settingsPath), { recursive: true, mode: 448 });
|
|
256
608
|
const payload = `${JSON.stringify(config, null, 2)}
|
|
257
609
|
`;
|
|
258
|
-
|
|
259
|
-
|
|
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
|
+
}
|
|
260
621
|
}
|
|
261
622
|
async function writeSkillToAgent(agent, skillSource) {
|
|
262
|
-
await
|
|
263
|
-
const target =
|
|
623
|
+
await mkdir3(agent.skillsDir, { recursive: true, mode: 493 });
|
|
624
|
+
const target = join5(agent.skillsDir, "keeperhub-wallet.skill.md");
|
|
264
625
|
await copyFile(skillSource, target);
|
|
265
|
-
await
|
|
626
|
+
await chmod3(target, 420);
|
|
266
627
|
return { agent: agent.agent, path: target, status: "written" };
|
|
267
628
|
}
|
|
268
|
-
function
|
|
629
|
+
function buildHookNoticeMessage(agent, command) {
|
|
269
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}`;
|
|
270
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
|
+
}
|
|
271
636
|
async function installSkill(options = {}) {
|
|
272
637
|
const agents = detectAgents(options.homeOverride);
|
|
273
638
|
const skillSource = options.skillSourcePath ?? resolveDefaultSkillSource();
|
|
274
639
|
const onNotice = options.onNotice ?? defaultNotice;
|
|
275
640
|
const hookCommand = options.hookCommand ?? resolveHookCommand();
|
|
641
|
+
const mcpCommand = options.mcpCommand ?? resolveMcpCommand();
|
|
276
642
|
const skillWrites = [];
|
|
277
643
|
const hookRegistrations = [];
|
|
644
|
+
const mcpRegistrations = [];
|
|
278
645
|
for (const agent of agents) {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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({
|
|
284
652
|
agent: agent.agent,
|
|
285
|
-
|
|
653
|
+
path: "",
|
|
654
|
+
status: "skipped"
|
|
286
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
|
+
}
|
|
287
675
|
} else {
|
|
288
|
-
const
|
|
676
|
+
const noticeMessage = buildHookNoticeMessage(agent, hookCommand);
|
|
289
677
|
hookRegistrations.push({
|
|
290
678
|
agent: agent.agent,
|
|
291
679
|
status: "notice",
|
|
292
|
-
message
|
|
680
|
+
message: noticeMessage
|
|
293
681
|
});
|
|
294
|
-
onNotice(
|
|
682
|
+
onNotice(noticeMessage);
|
|
295
683
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
async function readWalletConfig() {
|
|
325
|
-
const walletPath = join3(homedir2(), ".keeperhub", "wallet.json");
|
|
326
|
-
let raw;
|
|
327
|
-
try {
|
|
328
|
-
raw = await readFile2(walletPath, "utf-8");
|
|
329
|
-
} catch (err) {
|
|
330
|
-
if (err.code === "ENOENT") {
|
|
331
|
-
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})`);
|
|
332
712
|
}
|
|
333
|
-
throw err;
|
|
334
|
-
}
|
|
335
|
-
const parsed = JSON.parse(raw);
|
|
336
|
-
if (!(parsed.subOrgId && parsed.walletAddress && parsed.hmacSecret)) {
|
|
337
|
-
throw new Error(`Malformed wallet.json at ${walletPath}`);
|
|
338
713
|
}
|
|
339
|
-
return
|
|
340
|
-
}
|
|
341
|
-
async function writeWalletConfig(config) {
|
|
342
|
-
const walletPath = join3(homedir2(), ".keeperhub", "wallet.json");
|
|
343
|
-
await mkdir2(dirname3(walletPath), { recursive: true, mode: 448 });
|
|
344
|
-
await writeFile2(walletPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
345
|
-
await chmod2(walletPath, 384);
|
|
346
|
-
}
|
|
347
|
-
function getWalletConfigPath() {
|
|
348
|
-
return join3(homedir2(), ".keeperhub", "wallet.json");
|
|
714
|
+
return { skillWrites, hookRegistrations, mcpRegistrations };
|
|
349
715
|
}
|
|
350
716
|
|
|
351
717
|
// src/cli.ts
|
|
352
|
-
var TRAILING_SLASH = /\/$/;
|
|
353
|
-
var WALLET_ADDRESS_PATTERN = /^0x[a-fA-F0-9]{40}$/;
|
|
354
|
-
function resolveBaseUrl(override) {
|
|
355
|
-
const candidate = override ?? process.env.KEEPERHUB_API_URL ?? "https://app.keeperhub.com";
|
|
356
|
-
return candidate.replace(TRAILING_SLASH, "");
|
|
357
|
-
}
|
|
358
|
-
function isNonEmptyString(value) {
|
|
359
|
-
return typeof value === "string" && value.length > 0;
|
|
360
|
-
}
|
|
361
|
-
function provisionInvalidError(message) {
|
|
362
|
-
const err = new Error(message);
|
|
363
|
-
err.code = "PROVISION_RESPONSE_INVALID";
|
|
364
|
-
return err;
|
|
365
|
-
}
|
|
366
|
-
function validateProvisionResponse(data) {
|
|
367
|
-
if (typeof data !== "object" || data === null) {
|
|
368
|
-
throw provisionInvalidError("provision response is not an object");
|
|
369
|
-
}
|
|
370
|
-
const { subOrgId, walletAddress, hmacSecret } = data;
|
|
371
|
-
if (!(isNonEmptyString(subOrgId) && isNonEmptyString(walletAddress) && isNonEmptyString(hmacSecret))) {
|
|
372
|
-
throw provisionInvalidError(
|
|
373
|
-
"provision response missing subOrgId, walletAddress, or hmacSecret"
|
|
374
|
-
);
|
|
375
|
-
}
|
|
376
|
-
if (!WALLET_ADDRESS_PATTERN.test(walletAddress)) {
|
|
377
|
-
throw provisionInvalidError(
|
|
378
|
-
`provision response walletAddress is not a valid 0x-prefixed 40-hex address: ${walletAddress}`
|
|
379
|
-
);
|
|
380
|
-
}
|
|
381
|
-
return {
|
|
382
|
-
subOrgId,
|
|
383
|
-
walletAddress,
|
|
384
|
-
hmacSecret
|
|
385
|
-
};
|
|
386
|
-
}
|
|
387
718
|
async function cmdAdd(opts = {}) {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
headers: { "content-type": "application/json" },
|
|
392
|
-
body: "{}"
|
|
393
|
-
});
|
|
394
|
-
if (!response.ok) {
|
|
395
|
-
const text = await response.text();
|
|
396
|
-
process.stderr.write(
|
|
397
|
-
`[keeperhub-wallet] provision failed: HTTP ${response.status}: ${text}
|
|
398
|
-
`
|
|
399
|
-
);
|
|
400
|
-
process.exit(1);
|
|
401
|
-
}
|
|
402
|
-
const raw = await response.json();
|
|
403
|
-
const data = validateProvisionResponse(raw);
|
|
404
|
-
await writeWalletConfig({
|
|
405
|
-
subOrgId: data.subOrgId,
|
|
406
|
-
walletAddress: data.walletAddress,
|
|
407
|
-
hmacSecret: data.hmacSecret
|
|
408
|
-
});
|
|
409
|
-
process.stdout.write(`subOrgId: ${data.subOrgId}
|
|
719
|
+
try {
|
|
720
|
+
const data = await provisionWallet({ baseUrl: opts.baseUrl });
|
|
721
|
+
process.stdout.write(`subOrgId: ${data.subOrgId}
|
|
410
722
|
`);
|
|
411
|
-
|
|
723
|
+
process.stdout.write(`walletAddress: ${data.walletAddress}
|
|
412
724
|
`);
|
|
413
|
-
|
|
725
|
+
process.stdout.write(`config written to ${getWalletConfigPath()}
|
|
414
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
|
+
}
|
|
415
737
|
}
|
|
416
738
|
async function cmdFund() {
|
|
417
739
|
const wallet = await readWalletConfig();
|
|
@@ -479,6 +801,29 @@ async function runCli(argv = process.argv) {
|
|
|
479
801
|
} else if (reg.status === "notice") {
|
|
480
802
|
process.stderr.write(
|
|
481
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"})
|
|
482
827
|
`
|
|
483
828
|
);
|
|
484
829
|
}
|
|
@@ -607,9 +952,9 @@ var KeeperHubClient = class {
|
|
|
607
952
|
};
|
|
608
953
|
|
|
609
954
|
// src/safety-config.ts
|
|
610
|
-
import { chmod as
|
|
611
|
-
import { homedir as
|
|
612
|
-
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";
|
|
613
958
|
var DEFAULT_SAFETY_CONFIG = {
|
|
614
959
|
auto_approve_max_usd: 5,
|
|
615
960
|
ask_threshold_usd: 50,
|
|
@@ -622,20 +967,20 @@ var DEFAULT_SAFETY_CONFIG = {
|
|
|
622
967
|
]
|
|
623
968
|
};
|
|
624
969
|
function getSafetyPath() {
|
|
625
|
-
return
|
|
970
|
+
return join6(homedir4(), ".keeperhub", "safety.json");
|
|
626
971
|
}
|
|
627
972
|
async function loadSafetyConfig() {
|
|
628
973
|
const path = getSafetyPath();
|
|
629
974
|
let raw;
|
|
630
975
|
try {
|
|
631
|
-
raw = await
|
|
976
|
+
raw = await readFile4(path, "utf-8");
|
|
632
977
|
} catch (err) {
|
|
633
978
|
if (err.code === "ENOENT") {
|
|
634
|
-
await
|
|
635
|
-
await
|
|
979
|
+
await mkdir4(dirname6(path), { recursive: true, mode: 448 });
|
|
980
|
+
await writeFile4(path, JSON.stringify(DEFAULT_SAFETY_CONFIG, null, 2), {
|
|
636
981
|
mode: 420
|
|
637
982
|
});
|
|
638
|
-
await
|
|
983
|
+
await chmod4(path, 420);
|
|
639
984
|
return DEFAULT_SAFETY_CONFIG;
|
|
640
985
|
}
|
|
641
986
|
throw err;
|
|
@@ -832,7 +1177,7 @@ function parseMppChallenge(response) {
|
|
|
832
1177
|
}
|
|
833
1178
|
|
|
834
1179
|
// src/payment-signer.ts
|
|
835
|
-
import { randomBytes } from "crypto";
|
|
1180
|
+
import { randomBytes as randomBytes4 } from "crypto";
|
|
836
1181
|
|
|
837
1182
|
// src/workflow-slug.ts
|
|
838
1183
|
var KEEPERHUB_WORKFLOW_RE = /\/api\/mcp\/workflows\/([a-zA-Z0-9_-]+)\/call(?:\/?)(?:\?|$|#)/;
|
|
@@ -1012,7 +1357,7 @@ function createPaymentSigner(opts = {}) {
|
|
|
1012
1357
|
const now = Math.floor(Date.now() / 1e3);
|
|
1013
1358
|
const validAfter = now - VALID_AFTER_PAST_SLACK_SECONDS;
|
|
1014
1359
|
const validBefore = now + accept.maxTimeoutSeconds;
|
|
1015
|
-
const nonce = `0x${
|
|
1360
|
+
const nonce = `0x${randomBytes4(NONCE_BYTES).toString("hex")}`;
|
|
1016
1361
|
const client = clientFactory(wallet);
|
|
1017
1362
|
const signature = await signOrPoll(client, {
|
|
1018
1363
|
chain: "base",
|