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