@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/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
- if (existsSync(dirname(skillsDir))) {
44
- results.push({
45
- agent: spec.agent,
46
- skillsDir,
47
- settingsFile,
48
- hookSupport: spec.hookSupport
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
+ }
71
+ }
72
+ if (!detected) {
73
+ continue;
50
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,212 @@ function fund(walletAddress) {
146
178
  };
147
179
  }
148
180
 
181
+ // src/hmac.ts
182
+ import { createHash, createHmac } from "crypto";
183
+ function computeSignature(secret, method, path, subOrgId, body, timestamp) {
184
+ const bodyDigest = createHash("sha256").update(body).digest("hex");
185
+ const signingString = `${method}
186
+ ${path}
187
+ ${subOrgId}
188
+ ${bodyDigest}
189
+ ${timestamp}`;
190
+ return createHmac("sha256", secret).update(signingString).digest("hex");
191
+ }
192
+ function buildHmacHeaders(secret, method, path, subOrgId, body) {
193
+ const timestamp = String(Math.floor(Date.now() / 1e3));
194
+ const signature = computeSignature(
195
+ secret,
196
+ method,
197
+ path,
198
+ subOrgId,
199
+ body,
200
+ timestamp
201
+ );
202
+ return {
203
+ "X-KH-Sub-Org": subOrgId,
204
+ "X-KH-Timestamp": timestamp,
205
+ "X-KH-Signature": signature
206
+ };
207
+ }
208
+
209
+ // src/storage.ts
210
+ import { randomBytes } from "crypto";
211
+ import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
212
+ import { homedir as homedir2 } from "os";
213
+ import { dirname as dirname2, join as join2 } from "path";
214
+
215
+ // src/types.ts
216
+ var KeeperHubError = class extends Error {
217
+ code;
218
+ constructor(code, message) {
219
+ super(message);
220
+ this.name = "KeeperHubError";
221
+ this.code = code;
222
+ }
223
+ };
224
+ var WalletConfigMissingError = class extends Error {
225
+ constructor() {
226
+ super(
227
+ "Wallet config not found at ~/.keeperhub/wallet.json. Run `npx @keeperhub/wallet add` to provision."
228
+ );
229
+ this.name = "WalletConfigMissingError";
230
+ }
231
+ };
232
+ var WalletConfigCorruptError = class extends Error {
233
+ path;
234
+ constructor(path, reason) {
235
+ super(
236
+ `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).`
237
+ );
238
+ this.name = "WalletConfigCorruptError";
239
+ this.path = path;
240
+ }
241
+ };
242
+
243
+ // src/storage.ts
244
+ async function readWalletConfig() {
245
+ const walletPath = join2(homedir2(), ".keeperhub", "wallet.json");
246
+ let raw;
247
+ try {
248
+ raw = await readFile(walletPath, "utf-8");
249
+ } catch (err) {
250
+ if (err.code === "ENOENT") {
251
+ throw new WalletConfigMissingError();
252
+ }
253
+ throw err;
254
+ }
255
+ let parsed;
256
+ try {
257
+ parsed = JSON.parse(raw);
258
+ } catch (err) {
259
+ const reason = err instanceof Error ? err.message : String(err);
260
+ throw new WalletConfigCorruptError(walletPath, reason);
261
+ }
262
+ if (!(parsed.subOrgId && parsed.walletAddress && parsed.hmacSecret)) {
263
+ throw new WalletConfigCorruptError(walletPath, "missing required fields");
264
+ }
265
+ return parsed;
266
+ }
267
+ async function writeWalletConfig(config) {
268
+ const walletPath = join2(homedir2(), ".keeperhub", "wallet.json");
269
+ await mkdir(dirname2(walletPath), { recursive: true, mode: 448 });
270
+ const suffix = randomBytes(8).toString("hex");
271
+ const tmpPath = `${walletPath}.${process.pid}.${suffix}.tmp`;
272
+ await writeFile(tmpPath, JSON.stringify(config, null, 2), { mode: 384 });
273
+ await chmod(tmpPath, 384);
274
+ await rename(tmpPath, walletPath);
275
+ }
276
+ function getWalletConfigPath() {
277
+ return join2(homedir2(), ".keeperhub", "wallet.json");
278
+ }
279
+
280
+ // src/provision.ts
281
+ var TRAILING_SLASH = /\/$/;
282
+ var WALLET_ADDRESS_PATTERN = /^0x[a-fA-F0-9]{40}$/;
283
+ var ProvisionResponseInvalidError = class extends Error {
284
+ code = "PROVISION_RESPONSE_INVALID";
285
+ constructor(message) {
286
+ super(message);
287
+ this.name = "ProvisionResponseInvalidError";
288
+ }
289
+ };
290
+ var ProvisionHttpError = class extends Error {
291
+ code = "PROVISION_HTTP_ERROR";
292
+ status;
293
+ body;
294
+ constructor(status, body) {
295
+ super(`provision failed: HTTP ${status}: ${body}`);
296
+ this.name = "ProvisionHttpError";
297
+ this.status = status;
298
+ this.body = body;
299
+ }
300
+ };
301
+ function resolveBaseUrl(override) {
302
+ const candidate = override ?? process.env.KEEPERHUB_API_URL ?? "https://app.keeperhub.com";
303
+ return candidate.replace(TRAILING_SLASH, "");
304
+ }
305
+ function isNonEmptyString(value) {
306
+ return typeof value === "string" && value.length > 0;
307
+ }
308
+ function validateProvisionResponse(data) {
309
+ if (typeof data !== "object" || data === null) {
310
+ throw new ProvisionResponseInvalidError(
311
+ "provision response is not an object"
312
+ );
313
+ }
314
+ const { subOrgId, walletAddress, hmacSecret } = data;
315
+ if (!(isNonEmptyString(subOrgId) && isNonEmptyString(walletAddress) && isNonEmptyString(hmacSecret))) {
316
+ throw new ProvisionResponseInvalidError(
317
+ "provision response missing subOrgId, walletAddress, or hmacSecret"
318
+ );
319
+ }
320
+ if (!WALLET_ADDRESS_PATTERN.test(walletAddress)) {
321
+ throw new ProvisionResponseInvalidError(
322
+ `provision response walletAddress is not a valid 0x-prefixed 40-hex address: ${walletAddress}`
323
+ );
324
+ }
325
+ return {
326
+ subOrgId,
327
+ walletAddress,
328
+ hmacSecret
329
+ };
330
+ }
331
+ async function provisionWallet(options = {}) {
332
+ const baseUrl = resolveBaseUrl(options.baseUrl);
333
+ const fetchImpl = options.fetchImpl ?? globalThis.fetch;
334
+ const response = await fetchImpl(`${baseUrl}/api/agentic-wallet/provision`, {
335
+ method: "POST",
336
+ headers: { "content-type": "application/json" },
337
+ body: "{}",
338
+ signal: AbortSignal.timeout(3e4)
339
+ });
340
+ if (!response.ok) {
341
+ const text = await response.text();
342
+ throw new ProvisionHttpError(response.status, text);
343
+ }
344
+ const raw = await response.json();
345
+ const data = validateProvisionResponse(raw);
346
+ await writeWalletConfig(data);
347
+ return data;
348
+ }
349
+
149
350
  // src/skill-install.ts
351
+ import { randomBytes as randomBytes3 } from "crypto";
352
+ import {
353
+ chmod as chmod3,
354
+ copyFile,
355
+ mkdir as mkdir3,
356
+ readFile as readFile3,
357
+ rename as rename3,
358
+ unlink as unlink2,
359
+ writeFile as writeFile3
360
+ } from "fs/promises";
361
+ import { dirname as dirname5, join as join5 } from "path";
362
+ import { fileURLToPath as fileURLToPath2 } from "url";
363
+
364
+ // src/mcp-register.ts
365
+ import { randomBytes as randomBytes2 } from "crypto";
366
+ import {
367
+ chmod as chmod2,
368
+ mkdir as mkdir2,
369
+ readFile as readFile2,
370
+ rename as rename2,
371
+ unlink,
372
+ writeFile as writeFile2
373
+ } from "fs/promises";
374
+ import { homedir as homedir3 } from "os";
375
+ import { dirname as dirname4, join as join4 } from "path";
376
+
377
+ // src/runtime-detect.ts
150
378
  import { execFileSync } from "child_process";
151
379
  import { readFileSync } from "fs";
152
- import { chmod, copyFile, mkdir, readFile, writeFile } from "fs/promises";
153
- import { dirname as dirname2, join as join2 } from "path";
380
+ import { dirname as dirname3, join as join3 } from "path";
154
381
  import { fileURLToPath } from "url";
155
- var HOOK_BIN = "keeperhub-wallet-hook";
156
- var HOOK_COMMAND_BARE = HOOK_BIN;
157
382
  var PACKAGE_NAME = "@keeperhub/wallet";
158
383
  function readPackageVersion() {
159
384
  try {
160
- const here = dirname2(fileURLToPath(import.meta.url));
161
- const pkgPath = join2(here, "..", "package.json");
385
+ const here = dirname3(fileURLToPath(import.meta.url));
386
+ const pkgPath = join3(here, "..", "package.json");
162
387
  const raw = readFileSync(pkgPath, "utf-8");
163
388
  const parsed = JSON.parse(raw);
164
389
  if (typeof parsed.version === "string" && parsed.version.length > 0) {
@@ -168,9 +393,6 @@ function readPackageVersion() {
168
393
  }
169
394
  return "latest";
170
395
  }
171
- function buildNpxCommand(version) {
172
- return `npx -y -p ${PACKAGE_NAME}@${version} ${HOOK_BIN}`;
173
- }
174
396
  function isNpxExecution() {
175
397
  const execPath = process.env.npm_execpath;
176
398
  if (typeof execPath !== "string" || execPath.length === 0) {
@@ -198,6 +420,144 @@ function isPathUnderTransientCache(resolvedPath) {
198
420
  }
199
421
  return false;
200
422
  }
423
+ function resolveBinCommand(binName) {
424
+ const version = readPackageVersion();
425
+ const npxArgs = ["-y", "-p", `${PACKAGE_NAME}@${version}`, binName];
426
+ const npxCommandString = `npx ${npxArgs.join(" ")}`;
427
+ if (isNpxExecution()) {
428
+ return {
429
+ commandString: npxCommandString,
430
+ command: "npx",
431
+ args: npxArgs
432
+ };
433
+ }
434
+ try {
435
+ const resolved = execFileSync("/bin/sh", ["-c", `command -v ${binName}`], {
436
+ stdio: ["ignore", "pipe", "ignore"]
437
+ }).toString().trim();
438
+ if (resolved.length > 0 && !isPathUnderTransientCache(resolved)) {
439
+ return {
440
+ commandString: binName,
441
+ command: binName,
442
+ args: []
443
+ };
444
+ }
445
+ } catch {
446
+ }
447
+ return {
448
+ commandString: npxCommandString,
449
+ command: "npx",
450
+ args: npxArgs
451
+ };
452
+ }
453
+
454
+ // src/mcp-register.ts
455
+ var MCP_BIN = "keeperhub-wallet-mcp";
456
+ var MCP_SERVER_NAME = "keeperhub-wallet";
457
+ function resolveMcpCommand() {
458
+ const envOverride = process.env.KEEPERHUB_WALLET_MCP_COMMAND;
459
+ if (envOverride && envOverride.length > 0) {
460
+ const parts = envOverride.trim().split(/\s+/);
461
+ const head = parts[0] ?? envOverride;
462
+ return { command: head, args: parts.slice(1) };
463
+ }
464
+ const resolved = resolveBinCommand(MCP_BIN);
465
+ return { command: resolved.command, args: resolved.args };
466
+ }
467
+ function buildStandardEntry(cmd) {
468
+ const entry = {
469
+ command: cmd.command,
470
+ args: cmd.args
471
+ };
472
+ if (cmd.env && Object.keys(cmd.env).length > 0) {
473
+ entry.env = cmd.env;
474
+ }
475
+ return entry;
476
+ }
477
+ function buildOpencodeEntry(cmd) {
478
+ return {
479
+ type: "local",
480
+ command: [cmd.command, ...cmd.args],
481
+ enabled: true,
482
+ environment: cmd.env ?? {}
483
+ };
484
+ }
485
+ async function readJsonOrEmpty(path) {
486
+ let raw = null;
487
+ try {
488
+ raw = await readFile2(path, "utf-8");
489
+ } catch (err) {
490
+ if (err.code !== "ENOENT") {
491
+ throw err;
492
+ }
493
+ }
494
+ if (raw === null) {
495
+ return {};
496
+ }
497
+ try {
498
+ return JSON.parse(raw);
499
+ } catch {
500
+ throw new Error(
501
+ `MCP config at ${path} is not valid JSON; aborting MCP registration`
502
+ );
503
+ }
504
+ }
505
+ async function writeJsonAtomic(path, payload) {
506
+ await mkdir2(dirname4(path), { recursive: true, mode: 448 });
507
+ const suffix = randomBytes2(8).toString("hex");
508
+ const tmpPath = `${path}.${process.pid}.${suffix}.tmp`;
509
+ try {
510
+ await writeFile2(tmpPath, payload, { mode: 384 });
511
+ await chmod2(tmpPath, 384);
512
+ await rename2(tmpPath, path);
513
+ } catch (err) {
514
+ await unlink(tmpPath).catch(() => {
515
+ });
516
+ throw err;
517
+ }
518
+ }
519
+ async function writeStandardMcp(path, entry) {
520
+ const config = await readJsonOrEmpty(path);
521
+ const servers = typeof config.mcpServers === "object" && config.mcpServers !== null ? config.mcpServers : {};
522
+ servers[MCP_SERVER_NAME] = entry;
523
+ config.mcpServers = servers;
524
+ const payload = `${JSON.stringify(config, null, 2)}
525
+ `;
526
+ await writeJsonAtomic(path, payload);
527
+ }
528
+ async function writeOpencodeMcp(path, entry) {
529
+ const config = await readJsonOrEmpty(path);
530
+ const servers = typeof config.mcp === "object" && config.mcp !== null ? config.mcp : {};
531
+ servers[MCP_SERVER_NAME] = entry;
532
+ config.mcp = servers;
533
+ const payload = `${JSON.stringify(config, null, 2)}
534
+ `;
535
+ await writeJsonAtomic(path, payload);
536
+ }
537
+ async function registerMcpServer(target, options = {}) {
538
+ if (target.mcpSupport === "notice") {
539
+ throw new Error(
540
+ `agent ${target.agent} does not support auto-registered MCP servers; surface a notice instead`
541
+ );
542
+ }
543
+ if (!target.mcpConfigRel) {
544
+ throw new Error(
545
+ `agent ${target.agent} has mcpSupport=${target.mcpSupport} but no mcpConfigRel path`
546
+ );
547
+ }
548
+ const home = options.homeOverride ?? homedir3();
549
+ const path = join4(home, ...target.mcpConfigRel);
550
+ const cmd = options.command ?? resolveMcpCommand();
551
+ if (target.mcpSupport === "opencode") {
552
+ await writeOpencodeMcp(path, buildOpencodeEntry(cmd));
553
+ } else {
554
+ await writeStandardMcp(path, buildStandardEntry(cmd));
555
+ }
556
+ return { path, name: MCP_SERVER_NAME };
557
+ }
558
+
559
+ // src/skill-install.ts
560
+ var HOOK_BIN = "keeperhub-wallet-hook";
201
561
  var KEEPERHUB_HOOK_MARKER = HOOK_BIN;
202
562
  function filterKeeperhubHooksFromEntry(entry) {
203
563
  if (typeof entry !== "object" || entry === null) {
@@ -224,19 +584,7 @@ function resolveHookCommand() {
224
584
  if (envOverride && envOverride.length > 0) {
225
585
  return envOverride;
226
586
  }
227
- if (isNpxExecution()) {
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());
587
+ return resolveBinCommand(HOOK_BIN).commandString;
240
588
  }
241
589
  function buildKeeperhubEntry(command) {
242
590
  return {
@@ -245,8 +593,8 @@ function buildKeeperhubEntry(command) {
245
593
  };
246
594
  }
247
595
  function resolveDefaultSkillSource() {
248
- const here = dirname2(fileURLToPath(import.meta.url));
249
- return join2(here, "..", "skill", "keeperhub-wallet.skill.md");
596
+ const here = dirname5(fileURLToPath2(import.meta.url));
597
+ return join5(here, "..", "skill", "keeperhub-wallet.skill.md");
250
598
  }
251
599
  function defaultNotice(msg) {
252
600
  process.stderr.write(`${msg}
@@ -256,7 +604,7 @@ async function registerClaudeCodeHook(settingsPath, options = {}) {
256
604
  const command = options.hookCommand ?? resolveHookCommand();
257
605
  let raw = null;
258
606
  try {
259
- raw = await readFile(settingsPath, "utf-8");
607
+ raw = await readFile3(settingsPath, "utf-8");
260
608
  } catch (err) {
261
609
  if (err.code !== "ENOENT") {
262
610
  throw err;
@@ -284,166 +632,136 @@ async function registerClaudeCodeHook(settingsPath, options = {}) {
284
632
  filtered.push(buildKeeperhubEntry(command));
285
633
  hooks.PreToolUse = filtered;
286
634
  config.hooks = hooks;
287
- await mkdir(dirname2(settingsPath), { recursive: true, mode: 448 });
635
+ await mkdir3(dirname5(settingsPath), { recursive: true, mode: 448 });
288
636
  const payload = `${JSON.stringify(config, null, 2)}
289
637
  `;
290
- await writeFile(settingsPath, payload, { mode: 384 });
291
- await chmod(settingsPath, 384);
638
+ const suffix = randomBytes3(8).toString("hex");
639
+ const tmpPath = `${settingsPath}.${process.pid}.${suffix}.tmp`;
640
+ try {
641
+ await writeFile3(tmpPath, payload, { mode: 384 });
642
+ await chmod3(tmpPath, 384);
643
+ await rename3(tmpPath, settingsPath);
644
+ } catch (err) {
645
+ await unlink2(tmpPath).catch(() => {
646
+ });
647
+ throw err;
648
+ }
292
649
  }
293
650
  async function writeSkillToAgent(agent, skillSource) {
294
- await mkdir(agent.skillsDir, { recursive: true, mode: 493 });
295
- const target = join2(agent.skillsDir, "keeperhub-wallet.skill.md");
651
+ await mkdir3(agent.skillsDir, { recursive: true, mode: 493 });
652
+ const target = join5(agent.skillsDir, "keeperhub-wallet.skill.md");
296
653
  await copyFile(skillSource, target);
297
- await chmod(target, 420);
654
+ await chmod3(target, 420);
298
655
  return { agent: agent.agent, path: target, status: "written" };
299
656
  }
300
- function buildNoticeMessage(agent, command) {
657
+ function buildHookNoticeMessage(agent, command) {
301
658
  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
659
  }
660
+ function buildMcpNoticeMessage(agent, command) {
661
+ const cmd = [command.command, ...command.args].join(" ");
662
+ return `${agent.agent} does not support auto-registered MCP servers; add an entry named \`keeperhub-wallet\` running \`${cmd}\` to your MCP config manually`;
663
+ }
303
664
  async function installSkill(options = {}) {
304
665
  const agents = detectAgents(options.homeOverride);
305
666
  const skillSource = options.skillSourcePath ?? resolveDefaultSkillSource();
306
667
  const onNotice = options.onNotice ?? defaultNotice;
307
668
  const hookCommand = options.hookCommand ?? resolveHookCommand();
669
+ const mcpCommand = options.mcpCommand ?? resolveMcpCommand();
308
670
  const skillWrites = [];
309
671
  const hookRegistrations = [];
672
+ const mcpRegistrations = [];
310
673
  for (const agent of agents) {
311
- const write = await writeSkillToAgent(agent, skillSource);
312
- skillWrites.push(write);
313
- if (agent.hookSupport === "claude-code") {
314
- await registerClaudeCodeHook(agent.settingsFile, { hookCommand });
315
- hookRegistrations.push({
674
+ try {
675
+ const write = await writeSkillToAgent(agent, skillSource);
676
+ skillWrites.push(write);
677
+ } catch (err) {
678
+ const message = err instanceof Error ? err.message : String(err);
679
+ skillWrites.push({
316
680
  agent: agent.agent,
317
- status: "registered"
681
+ path: "",
682
+ status: "skipped"
318
683
  });
684
+ onNotice(`${agent.agent}: skill copy failed (${message})`);
685
+ continue;
686
+ }
687
+ if (agent.hookSupport === "claude-code") {
688
+ try {
689
+ await registerClaudeCodeHook(agent.settingsFile, { hookCommand });
690
+ hookRegistrations.push({
691
+ agent: agent.agent,
692
+ status: "registered"
693
+ });
694
+ } catch (err) {
695
+ const message = err instanceof Error ? err.message : String(err);
696
+ hookRegistrations.push({
697
+ agent: agent.agent,
698
+ status: "failed",
699
+ message
700
+ });
701
+ onNotice(`${agent.agent}: hook registration failed (${message})`);
702
+ }
319
703
  } else {
320
- const message = buildNoticeMessage(agent, hookCommand);
704
+ const noticeMessage = buildHookNoticeMessage(agent, hookCommand);
321
705
  hookRegistrations.push({
322
706
  agent: agent.agent,
323
707
  status: "notice",
324
- message
708
+ message: noticeMessage
325
709
  });
326
- onNotice(message);
710
+ onNotice(noticeMessage);
327
711
  }
328
- }
329
- return { skillWrites, hookRegistrations };
330
- }
331
-
332
- // src/storage.ts
333
- import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
334
- import { homedir as homedir2 } from "os";
335
- import { dirname as dirname3, join as join3 } from "path";
336
-
337
- // src/types.ts
338
- var KeeperHubError = class extends Error {
339
- code;
340
- constructor(code, message) {
341
- super(message);
342
- this.name = "KeeperHubError";
343
- this.code = code;
344
- }
345
- };
346
- var WalletConfigMissingError = class extends Error {
347
- constructor() {
348
- super(
349
- "Wallet config not found at ~/.keeperhub/wallet.json. Run `npx @keeperhub/wallet add` to provision."
350
- );
351
- this.name = "WalletConfigMissingError";
352
- }
353
- };
354
-
355
- // src/storage.ts
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();
712
+ if (agent.mcpSupport === "notice") {
713
+ const noticeMessage = buildMcpNoticeMessage(agent, mcpCommand);
714
+ mcpRegistrations.push({
715
+ agent: agent.agent,
716
+ status: "notice",
717
+ message: noticeMessage
718
+ });
719
+ onNotice(noticeMessage);
720
+ continue;
721
+ }
722
+ try {
723
+ const mcpResult = await registerMcpServer(agent, {
724
+ homeOverride: options.homeOverride,
725
+ command: mcpCommand
726
+ });
727
+ mcpRegistrations.push({
728
+ agent: agent.agent,
729
+ status: "registered",
730
+ path: mcpResult.path
731
+ });
732
+ } catch (err) {
733
+ const message = err instanceof Error ? err.message : String(err);
734
+ mcpRegistrations.push({
735
+ agent: agent.agent,
736
+ status: "failed",
737
+ message
738
+ });
739
+ onNotice(`${agent.agent}: MCP registration failed (${message})`);
364
740
  }
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
741
  }
371
- return parsed;
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");
742
+ return { skillWrites, hookRegistrations, mcpRegistrations };
381
743
  }
382
744
 
383
745
  // 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
746
  async function cmdAdd(opts = {}) {
420
- const baseUrl = resolveBaseUrl(opts.baseUrl);
421
- const response = await fetch(`${baseUrl}/api/agentic-wallet/provision`, {
422
- method: "POST",
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}
747
+ try {
748
+ const data = await provisionWallet({ baseUrl: opts.baseUrl });
749
+ process.stdout.write(`subOrgId: ${data.subOrgId}
442
750
  `);
443
- process.stdout.write(`walletAddress: ${data.walletAddress}
751
+ process.stdout.write(`walletAddress: ${data.walletAddress}
444
752
  `);
445
- process.stdout.write(`config written to ${getWalletConfigPath()}
753
+ process.stdout.write(`config written to ${getWalletConfigPath()}
446
754
  `);
755
+ } catch (err) {
756
+ if (err instanceof ProvisionHttpError) {
757
+ process.stderr.write(
758
+ `[keeperhub-wallet] provision failed: HTTP ${err.status}: ${err.body}
759
+ `
760
+ );
761
+ process.exit(1);
762
+ }
763
+ throw err;
764
+ }
447
765
  }
448
766
  async function cmdFund() {
449
767
  const wallet = await readWalletConfig();
@@ -470,6 +788,58 @@ async function cmdInfo() {
470
788
  process.stdout.write(`walletAddress: ${wallet.walletAddress}
471
789
  `);
472
790
  }
791
+ var FEEDBACK_DEFAULT_BASE_URL = "https://app.keeperhub.com";
792
+ async function cmdFeedback(opts) {
793
+ const wallet = await readWalletConfig();
794
+ const baseUrl = (opts.baseUrl ?? FEEDBACK_DEFAULT_BASE_URL).replace(
795
+ /\/$/,
796
+ ""
797
+ );
798
+ const path = "/api/agentic-wallet/feedback";
799
+ const body = {
800
+ executionId: opts.executionId,
801
+ value: Number.parseInt(opts.value, 10),
802
+ valueDecimals: Number.parseInt(opts.decimals ?? "0", 10)
803
+ };
804
+ if (opts.comment !== void 0) {
805
+ body.comment = opts.comment;
806
+ }
807
+ if (opts.agentId !== void 0) {
808
+ body.agentId = opts.agentId;
809
+ }
810
+ if (opts.chainId !== void 0) {
811
+ body.agentChainId = Number.parseInt(opts.chainId, 10);
812
+ }
813
+ const bodyJson = JSON.stringify(body);
814
+ const headers = buildHmacHeaders(
815
+ wallet.hmacSecret,
816
+ "POST",
817
+ path,
818
+ wallet.subOrgId,
819
+ bodyJson
820
+ );
821
+ const response = await fetch(`${baseUrl}${path}`, {
822
+ method: "POST",
823
+ headers: {
824
+ "content-type": "application/json",
825
+ ...headers
826
+ },
827
+ body: bodyJson
828
+ });
829
+ const text = await response.text();
830
+ if (!response.ok) {
831
+ process.stderr.write(`HTTP ${response.status}: ${text}
832
+ `);
833
+ process.exit(1);
834
+ }
835
+ const parsed = JSON.parse(text);
836
+ process.stdout.write(`feedbackId: ${parsed.feedbackId ?? ""}
837
+ `);
838
+ process.stdout.write(`txHash: ${parsed.txHash ?? ""}
839
+ `);
840
+ process.stdout.write(`publicUrl: ${parsed.publicUrl ?? ""}
841
+ `);
842
+ }
473
843
  async function runCli(argv = process.argv) {
474
844
  const program = new Command();
475
845
  program.name("keeperhub-wallet").description(
@@ -489,6 +859,27 @@ async function runCli(argv = process.argv) {
489
859
  program.command("info").description("Print subOrgId and walletAddress from local config").action(async () => {
490
860
  await cmdInfo();
491
861
  });
862
+ program.command("feedback").description(
863
+ "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."
864
+ ).requiredOption(
865
+ "--execution-id <id>",
866
+ "workflow execution id to leave feedback for"
867
+ ).requiredOption(
868
+ "--value <int>",
869
+ "raw int128 rating value (e.g. 5 with --decimals 0 for a 5-star rating)"
870
+ ).option(
871
+ "--decimals <int>",
872
+ "decimals for value (0..18); 0 for integer scores, 1 for 0.1-step",
873
+ "0"
874
+ ).option("--comment <text>", "optional plaintext comment").option(
875
+ "--agent-id <id>",
876
+ "rated agent NFT id (uint256 decimal); defaults to KeeperHub agent 31875"
877
+ ).option(
878
+ "--chain-id <int>",
879
+ "agent chain id; defaults to 1 (Ethereum mainnet, only chain supported today)"
880
+ ).option("--base-url <url>", "KeeperHub API base URL").action(async (opts) => {
881
+ await cmdFeedback(opts);
882
+ });
492
883
  program.command("skill").description(
493
884
  "Install the KeeperHub skill file into detected agent directories"
494
885
  ).addCommand(
@@ -511,6 +902,29 @@ async function runCli(argv = process.argv) {
511
902
  } else if (reg.status === "notice") {
512
903
  process.stderr.write(
513
904
  `notice: ${reg.agent} -> ${reg.message ?? ""}
905
+ `
906
+ );
907
+ } else if (reg.status === "failed") {
908
+ process.stderr.write(
909
+ `hook: ${reg.agent} -> FAILED (${reg.message ?? "unknown error"})
910
+ `
911
+ );
912
+ }
913
+ }
914
+ for (const reg of result.mcpRegistrations) {
915
+ if (reg.status === "registered") {
916
+ process.stdout.write(
917
+ `mcp: ${reg.agent} -> registered at ${reg.path ?? "(unknown path)"}
918
+ `
919
+ );
920
+ } else if (reg.status === "notice") {
921
+ process.stderr.write(
922
+ `notice: ${reg.agent} mcp -> ${reg.message ?? ""}
923
+ `
924
+ );
925
+ } else if (reg.status === "failed") {
926
+ process.stderr.write(
927
+ `mcp: ${reg.agent} -> FAILED (${reg.message ?? "unknown error"})
514
928
  `
515
929
  );
516
930
  }
@@ -538,34 +952,6 @@ async function runCli(argv = process.argv) {
538
952
  }
539
953
  }
540
954
 
541
- // src/hmac.ts
542
- import { createHash, createHmac } from "crypto";
543
- function computeSignature(secret, method, path, subOrgId, body, timestamp) {
544
- const bodyDigest = createHash("sha256").update(body).digest("hex");
545
- const signingString = `${method}
546
- ${path}
547
- ${subOrgId}
548
- ${bodyDigest}
549
- ${timestamp}`;
550
- return createHmac("sha256", secret).update(signingString).digest("hex");
551
- }
552
- function buildHmacHeaders(secret, method, path, subOrgId, body) {
553
- const timestamp = String(Math.floor(Date.now() / 1e3));
554
- const signature = computeSignature(
555
- secret,
556
- method,
557
- path,
558
- subOrgId,
559
- body,
560
- timestamp
561
- );
562
- return {
563
- "X-KH-Sub-Org": subOrgId,
564
- "X-KH-Timestamp": timestamp,
565
- "X-KH-Signature": signature
566
- };
567
- }
568
-
569
955
  // src/client.ts
570
956
  var TRAILING_SLASH2 = /\/$/;
571
957
  function defaultCodeForStatus(status) {
@@ -639,9 +1025,9 @@ var KeeperHubClient = class {
639
1025
  };
640
1026
 
641
1027
  // src/safety-config.ts
642
- import { chmod as chmod3, mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
643
- import { homedir as homedir3 } from "os";
644
- import { dirname as dirname4, join as join4 } from "path";
1028
+ import { chmod as chmod4, mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
1029
+ import { homedir as homedir4 } from "os";
1030
+ import { dirname as dirname6, join as join6 } from "path";
645
1031
  var DEFAULT_SAFETY_CONFIG = {
646
1032
  auto_approve_max_usd: 5,
647
1033
  ask_threshold_usd: 50,
@@ -654,20 +1040,20 @@ var DEFAULT_SAFETY_CONFIG = {
654
1040
  ]
655
1041
  };
656
1042
  function getSafetyPath() {
657
- return join4(homedir3(), ".keeperhub", "safety.json");
1043
+ return join6(homedir4(), ".keeperhub", "safety.json");
658
1044
  }
659
1045
  async function loadSafetyConfig() {
660
1046
  const path = getSafetyPath();
661
1047
  let raw;
662
1048
  try {
663
- raw = await readFile3(path, "utf-8");
1049
+ raw = await readFile4(path, "utf-8");
664
1050
  } catch (err) {
665
1051
  if (err.code === "ENOENT") {
666
- await mkdir3(dirname4(path), { recursive: true, mode: 448 });
667
- await writeFile3(path, JSON.stringify(DEFAULT_SAFETY_CONFIG, null, 2), {
1052
+ await mkdir4(dirname6(path), { recursive: true, mode: 448 });
1053
+ await writeFile4(path, JSON.stringify(DEFAULT_SAFETY_CONFIG, null, 2), {
668
1054
  mode: 420
669
1055
  });
670
- await chmod3(path, 420);
1056
+ await chmod4(path, 420);
671
1057
  return DEFAULT_SAFETY_CONFIG;
672
1058
  }
673
1059
  throw err;
@@ -864,7 +1250,7 @@ function parseMppChallenge(response) {
864
1250
  }
865
1251
 
866
1252
  // src/payment-signer.ts
867
- import { randomBytes } from "crypto";
1253
+ import { randomBytes as randomBytes4 } from "crypto";
868
1254
 
869
1255
  // src/workflow-slug.ts
870
1256
  var KEEPERHUB_WORKFLOW_RE = /\/api\/mcp\/workflows\/([a-zA-Z0-9_-]+)\/call(?:\/?)(?:\?|$|#)/;
@@ -1044,7 +1430,7 @@ function createPaymentSigner(opts = {}) {
1044
1430
  const now = Math.floor(Date.now() / 1e3);
1045
1431
  const validAfter = now - VALID_AFTER_PAST_SLACK_SECONDS;
1046
1432
  const validBefore = now + accept.maxTimeoutSeconds;
1047
- const nonce = `0x${randomBytes(NONCE_BYTES).toString("hex")}`;
1433
+ const nonce = `0x${randomBytes4(NONCE_BYTES).toString("hex")}`;
1048
1434
  const client = clientFactory(wallet);
1049
1435
  const signature = await signOrPoll(client, {
1050
1436
  chain: "base",