@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/dist/cli.js CHANGED
@@ -92,75 +92,262 @@ function fund(walletAddress) {
92
92
  };
93
93
  }
94
94
 
95
+ // src/storage.ts
96
+ import { randomBytes } from "crypto";
97
+ import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
98
+ import { homedir } from "os";
99
+ import { dirname, join } from "path";
100
+
101
+ // src/types.ts
102
+ var WalletConfigMissingError = class extends Error {
103
+ constructor() {
104
+ super(
105
+ "Wallet config not found at ~/.keeperhub/wallet.json. Run `npx @keeperhub/wallet add` to provision."
106
+ );
107
+ this.name = "WalletConfigMissingError";
108
+ }
109
+ };
110
+ var WalletConfigCorruptError = class extends Error {
111
+ path;
112
+ constructor(path, reason) {
113
+ super(
114
+ `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).`
115
+ );
116
+ this.name = "WalletConfigCorruptError";
117
+ this.path = path;
118
+ }
119
+ };
120
+
121
+ // src/storage.ts
122
+ async function readWalletConfig() {
123
+ const walletPath = join(homedir(), ".keeperhub", "wallet.json");
124
+ let raw;
125
+ try {
126
+ raw = await readFile(walletPath, "utf-8");
127
+ } catch (err) {
128
+ if (err.code === "ENOENT") {
129
+ throw new WalletConfigMissingError();
130
+ }
131
+ throw err;
132
+ }
133
+ let parsed;
134
+ try {
135
+ parsed = JSON.parse(raw);
136
+ } catch (err) {
137
+ const reason = err instanceof Error ? err.message : String(err);
138
+ throw new WalletConfigCorruptError(walletPath, reason);
139
+ }
140
+ if (!(parsed.subOrgId && parsed.walletAddress && parsed.hmacSecret)) {
141
+ throw new WalletConfigCorruptError(walletPath, "missing required fields");
142
+ }
143
+ return parsed;
144
+ }
145
+ async function writeWalletConfig(config) {
146
+ const walletPath = join(homedir(), ".keeperhub", "wallet.json");
147
+ await mkdir(dirname(walletPath), { recursive: true, mode: 448 });
148
+ const suffix = randomBytes(8).toString("hex");
149
+ const tmpPath = `${walletPath}.${process.pid}.${suffix}.tmp`;
150
+ await writeFile(tmpPath, JSON.stringify(config, null, 2), { mode: 384 });
151
+ await chmod(tmpPath, 384);
152
+ await rename(tmpPath, walletPath);
153
+ }
154
+ function getWalletConfigPath() {
155
+ return join(homedir(), ".keeperhub", "wallet.json");
156
+ }
157
+
158
+ // src/provision.ts
159
+ var TRAILING_SLASH = /\/$/;
160
+ var WALLET_ADDRESS_PATTERN = /^0x[a-fA-F0-9]{40}$/;
161
+ var ProvisionResponseInvalidError = class extends Error {
162
+ code = "PROVISION_RESPONSE_INVALID";
163
+ constructor(message) {
164
+ super(message);
165
+ this.name = "ProvisionResponseInvalidError";
166
+ }
167
+ };
168
+ var ProvisionHttpError = class extends Error {
169
+ code = "PROVISION_HTTP_ERROR";
170
+ status;
171
+ body;
172
+ constructor(status, body) {
173
+ super(`provision failed: HTTP ${status}: ${body}`);
174
+ this.name = "ProvisionHttpError";
175
+ this.status = status;
176
+ this.body = body;
177
+ }
178
+ };
179
+ function resolveBaseUrl(override) {
180
+ const candidate = override ?? process.env.KEEPERHUB_API_URL ?? "https://app.keeperhub.com";
181
+ return candidate.replace(TRAILING_SLASH, "");
182
+ }
183
+ function isNonEmptyString(value) {
184
+ return typeof value === "string" && value.length > 0;
185
+ }
186
+ function validateProvisionResponse(data) {
187
+ if (typeof data !== "object" || data === null) {
188
+ throw new ProvisionResponseInvalidError(
189
+ "provision response is not an object"
190
+ );
191
+ }
192
+ const { subOrgId, walletAddress, hmacSecret } = data;
193
+ if (!(isNonEmptyString(subOrgId) && isNonEmptyString(walletAddress) && isNonEmptyString(hmacSecret))) {
194
+ throw new ProvisionResponseInvalidError(
195
+ "provision response missing subOrgId, walletAddress, or hmacSecret"
196
+ );
197
+ }
198
+ if (!WALLET_ADDRESS_PATTERN.test(walletAddress)) {
199
+ throw new ProvisionResponseInvalidError(
200
+ `provision response walletAddress is not a valid 0x-prefixed 40-hex address: ${walletAddress}`
201
+ );
202
+ }
203
+ return {
204
+ subOrgId,
205
+ walletAddress,
206
+ hmacSecret
207
+ };
208
+ }
209
+ async function provisionWallet(options = {}) {
210
+ const baseUrl = resolveBaseUrl(options.baseUrl);
211
+ const fetchImpl = options.fetchImpl ?? globalThis.fetch;
212
+ const response = await fetchImpl(`${baseUrl}/api/agentic-wallet/provision`, {
213
+ method: "POST",
214
+ headers: { "content-type": "application/json" },
215
+ body: "{}",
216
+ signal: AbortSignal.timeout(3e4)
217
+ });
218
+ if (!response.ok) {
219
+ const text = await response.text();
220
+ throw new ProvisionHttpError(response.status, text);
221
+ }
222
+ const raw = await response.json();
223
+ const data = validateProvisionResponse(raw);
224
+ await writeWalletConfig(data);
225
+ return data;
226
+ }
227
+
95
228
  // src/skill-install.ts
96
- import { execFileSync } from "child_process";
97
- import { chmod, copyFile, mkdir, readFile, writeFile } from "fs/promises";
98
- import { readFileSync } from "fs";
99
- import { dirname as dirname2, join as join2 } from "path";
100
- import { fileURLToPath } from "url";
229
+ import { randomBytes as randomBytes3 } from "crypto";
230
+ import {
231
+ chmod as chmod3,
232
+ copyFile,
233
+ mkdir as mkdir3,
234
+ readFile as readFile3,
235
+ rename as rename3,
236
+ unlink as unlink2,
237
+ writeFile as writeFile3
238
+ } from "fs/promises";
239
+ import { dirname as dirname5, join as join5 } from "path";
240
+ import { fileURLToPath as fileURLToPath2 } from "url";
101
241
 
102
242
  // src/agent-detect.ts
103
243
  import { existsSync } from "fs";
104
- import { homedir } from "os";
105
- import { dirname, join } from "path";
244
+ import { homedir as homedir2 } from "os";
245
+ import { dirname as dirname2, join as join2 } from "path";
106
246
  var AGENT_SPECS = [
107
247
  {
108
248
  agent: "claude-code",
109
249
  skillsRel: [".claude", "skills"],
110
250
  settingsRel: [".claude", "settings.json"],
111
- hookSupport: "claude-code"
251
+ hookSupport: "claude-code",
252
+ // ~/.claude.json is at HOME root (not under .claude/) and is large
253
+ // (100+KB on real installs). registerMcpServer reads/parses/rewrites it
254
+ // while preserving every other top-level key byte-for-byte.
255
+ mcpConfigRel: [".claude.json"],
256
+ mcpSupport: "claude-code"
112
257
  },
113
258
  {
114
259
  agent: "cursor",
115
260
  skillsRel: [".cursor", "skills"],
116
261
  settingsRel: [".cursor", "settings.json"],
117
- hookSupport: "notice"
262
+ hookSupport: "notice",
263
+ mcpConfigRel: [".cursor", "mcp.json"],
264
+ mcpSupport: "cursor"
118
265
  },
119
266
  {
120
267
  agent: "cline",
121
268
  skillsRel: [".cline", "skills"],
122
269
  settingsRel: [".cline", "settings.json"],
123
- hookSupport: "notice"
270
+ hookSupport: "notice",
271
+ // Cline keeps MCP state in a per-VS-Code-variant globalStorage path
272
+ // (e.g. ~/Library/Application Support/Code/User/globalStorage/
273
+ // saoudrizwan.claude-dev/settings/cline_mcp_settings.json) that is too
274
+ // fragile to auto-detect. Ship "notice" with a copy-paste entry shape
275
+ // instead of guessing the variant.
276
+ mcpSupport: "notice"
124
277
  },
125
278
  {
126
279
  agent: "windsurf",
127
280
  skillsRel: [".windsurf", "skills"],
128
281
  settingsRel: [".windsurf", "settings.json"],
129
- hookSupport: "notice"
282
+ hookSupport: "notice",
283
+ mcpConfigRel: [".codeium", "windsurf", "mcp_config.json"],
284
+ mcpSupport: "windsurf",
285
+ // Windsurf historically ships under both `.windsurf/` and the legacy
286
+ // `.codeium/windsurf/`; detect either.
287
+ extraDetect: [[".codeium", "windsurf"]]
130
288
  },
131
289
  {
132
290
  agent: "opencode",
133
291
  skillsRel: [".config", "opencode", "skills"],
134
292
  settingsRel: [".config", "opencode", "settings.json"],
135
- hookSupport: "notice"
293
+ hookSupport: "notice",
294
+ mcpConfigRel: [".config", "opencode", "opencode.json"],
295
+ mcpSupport: "opencode"
136
296
  }
137
297
  ];
138
298
  function detectAgents(homeOverride) {
139
- const home = homeOverride ?? homedir();
299
+ const home = homeOverride ?? homedir2();
140
300
  const results = [];
141
301
  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
- });
302
+ const skillsDir = join2(home, ...spec.skillsRel);
303
+ const settingsFile = join2(home, ...spec.settingsRel);
304
+ let detected = existsSync(dirname2(skillsDir));
305
+ if (!detected && spec.extraDetect) {
306
+ for (const seg of spec.extraDetect) {
307
+ if (existsSync(join2(home, ...seg))) {
308
+ detected = true;
309
+ break;
310
+ }
311
+ }
312
+ }
313
+ if (!detected) {
314
+ continue;
151
315
  }
316
+ results.push({
317
+ agent: spec.agent,
318
+ skillsDir,
319
+ settingsFile,
320
+ hookSupport: spec.hookSupport,
321
+ mcpConfigRel: spec.mcpConfigRel,
322
+ mcpSupport: spec.mcpSupport
323
+ });
152
324
  }
153
325
  return results;
154
326
  }
155
327
 
156
- // src/skill-install.ts
157
- var HOOK_BIN = "keeperhub-wallet-hook";
158
- var HOOK_COMMAND_BARE = HOOK_BIN;
328
+ // src/mcp-register.ts
329
+ import { randomBytes as randomBytes2 } from "crypto";
330
+ import {
331
+ chmod as chmod2,
332
+ mkdir as mkdir2,
333
+ readFile as readFile2,
334
+ rename as rename2,
335
+ unlink,
336
+ writeFile as writeFile2
337
+ } from "fs/promises";
338
+ import { homedir as homedir3 } from "os";
339
+ import { dirname as dirname4, join as join4 } from "path";
340
+
341
+ // src/runtime-detect.ts
342
+ import { execFileSync } from "child_process";
343
+ import { readFileSync } from "fs";
344
+ import { dirname as dirname3, join as join3 } from "path";
345
+ import { fileURLToPath } from "url";
159
346
  var PACKAGE_NAME = "@keeperhub/wallet";
160
347
  function readPackageVersion() {
161
348
  try {
162
- const here = dirname2(fileURLToPath(import.meta.url));
163
- const pkgPath = join2(here, "..", "package.json");
349
+ const here = dirname3(fileURLToPath(import.meta.url));
350
+ const pkgPath = join3(here, "..", "package.json");
164
351
  const raw = readFileSync(pkgPath, "utf-8");
165
352
  const parsed = JSON.parse(raw);
166
353
  if (typeof parsed.version === "string" && parsed.version.length > 0) {
@@ -170,9 +357,171 @@ function readPackageVersion() {
170
357
  }
171
358
  return "latest";
172
359
  }
173
- function buildNpxCommand(version) {
174
- return `npx -y -p ${PACKAGE_NAME}@${version} ${HOOK_BIN}`;
360
+ function isNpxExecution() {
361
+ const execPath = process.env.npm_execpath;
362
+ if (typeof execPath !== "string" || execPath.length === 0) {
363
+ return false;
364
+ }
365
+ if (/(?:^|[\\/])npx-cli\.(?:js|cjs|mjs)$/i.test(execPath)) {
366
+ return true;
367
+ }
368
+ if (/(?:^|[\\/])npx(?:\.cmd|\.exe|\.ps1)?$/i.test(execPath)) {
369
+ return true;
370
+ }
371
+ return false;
372
+ }
373
+ var TRANSIENT_CACHE_PATTERNS = [
374
+ /[\\/]_npx[\\/]/,
375
+ /[\\/]dlx-[A-Za-z0-9]+[\\/]/,
376
+ /[\\/]xfs-[A-Za-z0-9]+[\\/]/,
377
+ /[\\/]\.bun[\\/]install[\\/]cache[\\/]/
378
+ ];
379
+ function isPathUnderTransientCache(resolvedPath) {
380
+ for (const re of TRANSIENT_CACHE_PATTERNS) {
381
+ if (re.test(resolvedPath)) {
382
+ return true;
383
+ }
384
+ }
385
+ return false;
386
+ }
387
+ function resolveBinCommand(binName) {
388
+ const version = readPackageVersion();
389
+ const npxArgs = ["-y", "-p", `${PACKAGE_NAME}@${version}`, binName];
390
+ const npxCommandString = `npx ${npxArgs.join(" ")}`;
391
+ if (isNpxExecution()) {
392
+ return {
393
+ commandString: npxCommandString,
394
+ command: "npx",
395
+ args: npxArgs
396
+ };
397
+ }
398
+ try {
399
+ const resolved = execFileSync("/bin/sh", ["-c", `command -v ${binName}`], {
400
+ stdio: ["ignore", "pipe", "ignore"]
401
+ }).toString().trim();
402
+ if (resolved.length > 0 && !isPathUnderTransientCache(resolved)) {
403
+ return {
404
+ commandString: binName,
405
+ command: binName,
406
+ args: []
407
+ };
408
+ }
409
+ } catch {
410
+ }
411
+ return {
412
+ commandString: npxCommandString,
413
+ command: "npx",
414
+ args: npxArgs
415
+ };
416
+ }
417
+
418
+ // src/mcp-register.ts
419
+ var MCP_BIN = "keeperhub-wallet-mcp";
420
+ var MCP_SERVER_NAME = "keeperhub-wallet";
421
+ function resolveMcpCommand() {
422
+ const envOverride = process.env.KEEPERHUB_WALLET_MCP_COMMAND;
423
+ if (envOverride && envOverride.length > 0) {
424
+ const parts = envOverride.trim().split(/\s+/);
425
+ const head = parts[0] ?? envOverride;
426
+ return { command: head, args: parts.slice(1) };
427
+ }
428
+ const resolved = resolveBinCommand(MCP_BIN);
429
+ return { command: resolved.command, args: resolved.args };
430
+ }
431
+ function buildStandardEntry(cmd) {
432
+ const entry = {
433
+ command: cmd.command,
434
+ args: cmd.args
435
+ };
436
+ if (cmd.env && Object.keys(cmd.env).length > 0) {
437
+ entry.env = cmd.env;
438
+ }
439
+ return entry;
440
+ }
441
+ function buildOpencodeEntry(cmd) {
442
+ return {
443
+ type: "local",
444
+ command: [cmd.command, ...cmd.args],
445
+ enabled: true,
446
+ environment: cmd.env ?? {}
447
+ };
448
+ }
449
+ async function readJsonOrEmpty(path) {
450
+ let raw = null;
451
+ try {
452
+ raw = await readFile2(path, "utf-8");
453
+ } catch (err) {
454
+ if (err.code !== "ENOENT") {
455
+ throw err;
456
+ }
457
+ }
458
+ if (raw === null) {
459
+ return {};
460
+ }
461
+ try {
462
+ return JSON.parse(raw);
463
+ } catch {
464
+ throw new Error(
465
+ `MCP config at ${path} is not valid JSON; aborting MCP registration`
466
+ );
467
+ }
468
+ }
469
+ async function writeJsonAtomic(path, payload) {
470
+ await mkdir2(dirname4(path), { recursive: true, mode: 448 });
471
+ const suffix = randomBytes2(8).toString("hex");
472
+ const tmpPath = `${path}.${process.pid}.${suffix}.tmp`;
473
+ try {
474
+ await writeFile2(tmpPath, payload, { mode: 384 });
475
+ await chmod2(tmpPath, 384);
476
+ await rename2(tmpPath, path);
477
+ } catch (err) {
478
+ await unlink(tmpPath).catch(() => {
479
+ });
480
+ throw err;
481
+ }
482
+ }
483
+ async function writeStandardMcp(path, entry) {
484
+ const config = await readJsonOrEmpty(path);
485
+ const servers = typeof config.mcpServers === "object" && config.mcpServers !== null ? config.mcpServers : {};
486
+ servers[MCP_SERVER_NAME] = entry;
487
+ config.mcpServers = servers;
488
+ const payload = `${JSON.stringify(config, null, 2)}
489
+ `;
490
+ await writeJsonAtomic(path, payload);
491
+ }
492
+ async function writeOpencodeMcp(path, entry) {
493
+ const config = await readJsonOrEmpty(path);
494
+ const servers = typeof config.mcp === "object" && config.mcp !== null ? config.mcp : {};
495
+ servers[MCP_SERVER_NAME] = entry;
496
+ config.mcp = servers;
497
+ const payload = `${JSON.stringify(config, null, 2)}
498
+ `;
499
+ await writeJsonAtomic(path, payload);
500
+ }
501
+ async function registerMcpServer(target, options = {}) {
502
+ if (target.mcpSupport === "notice") {
503
+ throw new Error(
504
+ `agent ${target.agent} does not support auto-registered MCP servers; surface a notice instead`
505
+ );
506
+ }
507
+ if (!target.mcpConfigRel) {
508
+ throw new Error(
509
+ `agent ${target.agent} has mcpSupport=${target.mcpSupport} but no mcpConfigRel path`
510
+ );
511
+ }
512
+ const home = options.homeOverride ?? homedir3();
513
+ const path = join4(home, ...target.mcpConfigRel);
514
+ const cmd = options.command ?? resolveMcpCommand();
515
+ if (target.mcpSupport === "opencode") {
516
+ await writeOpencodeMcp(path, buildOpencodeEntry(cmd));
517
+ } else {
518
+ await writeStandardMcp(path, buildStandardEntry(cmd));
519
+ }
520
+ return { path, name: MCP_SERVER_NAME };
175
521
  }
522
+
523
+ // src/skill-install.ts
524
+ var HOOK_BIN = "keeperhub-wallet-hook";
176
525
  var KEEPERHUB_HOOK_MARKER = HOOK_BIN;
177
526
  function filterKeeperhubHooksFromEntry(entry) {
178
527
  if (typeof entry !== "object" || entry === null) {
@@ -199,14 +548,7 @@ function resolveHookCommand() {
199
548
  if (envOverride && envOverride.length > 0) {
200
549
  return envOverride;
201
550
  }
202
- try {
203
- execFileSync("/bin/sh", ["-c", `command -v ${HOOK_BIN}`], {
204
- stdio: "ignore"
205
- });
206
- return HOOK_COMMAND_BARE;
207
- } catch {
208
- return buildNpxCommand(readPackageVersion());
209
- }
551
+ return resolveBinCommand(HOOK_BIN).commandString;
210
552
  }
211
553
  function buildKeeperhubEntry(command) {
212
554
  return {
@@ -215,8 +557,8 @@ function buildKeeperhubEntry(command) {
215
557
  };
216
558
  }
217
559
  function resolveDefaultSkillSource() {
218
- const here = dirname2(fileURLToPath(import.meta.url));
219
- return join2(here, "..", "skill", "keeperhub-wallet.skill.md");
560
+ const here = dirname5(fileURLToPath2(import.meta.url));
561
+ return join5(here, "..", "skill", "keeperhub-wallet.skill.md");
220
562
  }
221
563
  function defaultNotice(msg) {
222
564
  process.stderr.write(`${msg}
@@ -226,7 +568,7 @@ async function registerClaudeCodeHook(settingsPath, options = {}) {
226
568
  const command = options.hookCommand ?? resolveHookCommand();
227
569
  let raw = null;
228
570
  try {
229
- raw = await readFile(settingsPath, "utf-8");
571
+ raw = await readFile3(settingsPath, "utf-8");
230
572
  } catch (err) {
231
573
  if (err.code !== "ENOENT") {
232
574
  throw err;
@@ -254,158 +596,136 @@ async function registerClaudeCodeHook(settingsPath, options = {}) {
254
596
  filtered.push(buildKeeperhubEntry(command));
255
597
  hooks.PreToolUse = filtered;
256
598
  config.hooks = hooks;
257
- await mkdir(dirname2(settingsPath), { recursive: true, mode: 448 });
599
+ await mkdir3(dirname5(settingsPath), { recursive: true, mode: 448 });
258
600
  const payload = `${JSON.stringify(config, null, 2)}
259
601
  `;
260
- await writeFile(settingsPath, payload, { mode: 384 });
261
- await chmod(settingsPath, 384);
602
+ const suffix = randomBytes3(8).toString("hex");
603
+ const tmpPath = `${settingsPath}.${process.pid}.${suffix}.tmp`;
604
+ try {
605
+ await writeFile3(tmpPath, payload, { mode: 384 });
606
+ await chmod3(tmpPath, 384);
607
+ await rename3(tmpPath, settingsPath);
608
+ } catch (err) {
609
+ await unlink2(tmpPath).catch(() => {
610
+ });
611
+ throw err;
612
+ }
262
613
  }
263
614
  async function writeSkillToAgent(agent, skillSource) {
264
- await mkdir(agent.skillsDir, { recursive: true, mode: 493 });
265
- const target = join2(agent.skillsDir, "keeperhub-wallet.skill.md");
615
+ await mkdir3(agent.skillsDir, { recursive: true, mode: 493 });
616
+ const target = join5(agent.skillsDir, "keeperhub-wallet.skill.md");
266
617
  await copyFile(skillSource, target);
267
- await chmod(target, 420);
618
+ await chmod3(target, 420);
268
619
  return { agent: agent.agent, path: target, status: "written" };
269
620
  }
270
- function buildNoticeMessage(agent, command) {
621
+ function buildHookNoticeMessage(agent, command) {
271
622
  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}`;
272
623
  }
624
+ function buildMcpNoticeMessage(agent, command) {
625
+ const cmd = [command.command, ...command.args].join(" ");
626
+ return `${agent.agent} does not support auto-registered MCP servers; add an entry named \`keeperhub-wallet\` running \`${cmd}\` to your MCP config manually`;
627
+ }
273
628
  async function installSkill(options = {}) {
274
629
  const agents = detectAgents(options.homeOverride);
275
630
  const skillSource = options.skillSourcePath ?? resolveDefaultSkillSource();
276
631
  const onNotice = options.onNotice ?? defaultNotice;
277
632
  const hookCommand = options.hookCommand ?? resolveHookCommand();
633
+ const mcpCommand = options.mcpCommand ?? resolveMcpCommand();
278
634
  const skillWrites = [];
279
635
  const hookRegistrations = [];
636
+ const mcpRegistrations = [];
280
637
  for (const agent of agents) {
281
- const write = await writeSkillToAgent(agent, skillSource);
282
- skillWrites.push(write);
283
- if (agent.hookSupport === "claude-code") {
284
- await registerClaudeCodeHook(agent.settingsFile, { hookCommand });
285
- hookRegistrations.push({
638
+ try {
639
+ const write = await writeSkillToAgent(agent, skillSource);
640
+ skillWrites.push(write);
641
+ } catch (err) {
642
+ const message = err instanceof Error ? err.message : String(err);
643
+ skillWrites.push({
286
644
  agent: agent.agent,
287
- status: "registered"
645
+ path: "",
646
+ status: "skipped"
288
647
  });
648
+ onNotice(`${agent.agent}: skill copy failed (${message})`);
649
+ continue;
650
+ }
651
+ if (agent.hookSupport === "claude-code") {
652
+ try {
653
+ await registerClaudeCodeHook(agent.settingsFile, { hookCommand });
654
+ hookRegistrations.push({
655
+ agent: agent.agent,
656
+ status: "registered"
657
+ });
658
+ } catch (err) {
659
+ const message = err instanceof Error ? err.message : String(err);
660
+ hookRegistrations.push({
661
+ agent: agent.agent,
662
+ status: "failed",
663
+ message
664
+ });
665
+ onNotice(`${agent.agent}: hook registration failed (${message})`);
666
+ }
289
667
  } else {
290
- const message = buildNoticeMessage(agent, hookCommand);
668
+ const noticeMessage = buildHookNoticeMessage(agent, hookCommand);
291
669
  hookRegistrations.push({
292
670
  agent: agent.agent,
293
671
  status: "notice",
294
- message
672
+ message: noticeMessage
295
673
  });
296
- onNotice(message);
674
+ onNotice(noticeMessage);
297
675
  }
298
- }
299
- return { skillWrites, hookRegistrations };
300
- }
301
-
302
- // src/storage.ts
303
- import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
304
- import { homedir as homedir2 } from "os";
305
- import { dirname as dirname3, join as join3 } from "path";
306
-
307
- // src/types.ts
308
- var WalletConfigMissingError = class extends Error {
309
- constructor() {
310
- super(
311
- "Wallet config not found at ~/.keeperhub/wallet.json. Run `npx @keeperhub/wallet add` to provision."
312
- );
313
- this.name = "WalletConfigMissingError";
314
- }
315
- };
316
-
317
- // src/storage.ts
318
- async function readWalletConfig() {
319
- const walletPath = join3(homedir2(), ".keeperhub", "wallet.json");
320
- let raw;
321
- try {
322
- raw = await readFile2(walletPath, "utf-8");
323
- } catch (err) {
324
- if (err.code === "ENOENT") {
325
- throw new WalletConfigMissingError();
676
+ if (agent.mcpSupport === "notice") {
677
+ const noticeMessage = buildMcpNoticeMessage(agent, mcpCommand);
678
+ mcpRegistrations.push({
679
+ agent: agent.agent,
680
+ status: "notice",
681
+ message: noticeMessage
682
+ });
683
+ onNotice(noticeMessage);
684
+ continue;
685
+ }
686
+ try {
687
+ const mcpResult = await registerMcpServer(agent, {
688
+ homeOverride: options.homeOverride,
689
+ command: mcpCommand
690
+ });
691
+ mcpRegistrations.push({
692
+ agent: agent.agent,
693
+ status: "registered",
694
+ path: mcpResult.path
695
+ });
696
+ } catch (err) {
697
+ const message = err instanceof Error ? err.message : String(err);
698
+ mcpRegistrations.push({
699
+ agent: agent.agent,
700
+ status: "failed",
701
+ message
702
+ });
703
+ onNotice(`${agent.agent}: MCP registration failed (${message})`);
326
704
  }
327
- throw err;
328
- }
329
- const parsed = JSON.parse(raw);
330
- if (!(parsed.subOrgId && parsed.walletAddress && parsed.hmacSecret)) {
331
- throw new Error(`Malformed wallet.json at ${walletPath}`);
332
705
  }
333
- return parsed;
334
- }
335
- async function writeWalletConfig(config) {
336
- const walletPath = join3(homedir2(), ".keeperhub", "wallet.json");
337
- await mkdir2(dirname3(walletPath), { recursive: true, mode: 448 });
338
- await writeFile2(walletPath, JSON.stringify(config, null, 2), { mode: 384 });
339
- await chmod2(walletPath, 384);
340
- }
341
- function getWalletConfigPath() {
342
- return join3(homedir2(), ".keeperhub", "wallet.json");
706
+ return { skillWrites, hookRegistrations, mcpRegistrations };
343
707
  }
344
708
 
345
709
  // src/cli.ts
346
- var TRAILING_SLASH = /\/$/;
347
- var WALLET_ADDRESS_PATTERN = /^0x[a-fA-F0-9]{40}$/;
348
- function resolveBaseUrl(override) {
349
- const candidate = override ?? process.env.KEEPERHUB_API_URL ?? "https://app.keeperhub.com";
350
- return candidate.replace(TRAILING_SLASH, "");
351
- }
352
- function isNonEmptyString(value) {
353
- return typeof value === "string" && value.length > 0;
354
- }
355
- function provisionInvalidError(message) {
356
- const err = new Error(message);
357
- err.code = "PROVISION_RESPONSE_INVALID";
358
- return err;
359
- }
360
- function validateProvisionResponse(data) {
361
- if (typeof data !== "object" || data === null) {
362
- throw provisionInvalidError("provision response is not an object");
363
- }
364
- const { subOrgId, walletAddress, hmacSecret } = data;
365
- if (!(isNonEmptyString(subOrgId) && isNonEmptyString(walletAddress) && isNonEmptyString(hmacSecret))) {
366
- throw provisionInvalidError(
367
- "provision response missing subOrgId, walletAddress, or hmacSecret"
368
- );
369
- }
370
- if (!WALLET_ADDRESS_PATTERN.test(walletAddress)) {
371
- throw provisionInvalidError(
372
- `provision response walletAddress is not a valid 0x-prefixed 40-hex address: ${walletAddress}`
373
- );
374
- }
375
- return {
376
- subOrgId,
377
- walletAddress,
378
- hmacSecret
379
- };
380
- }
381
710
  async function cmdAdd(opts = {}) {
382
- const baseUrl = resolveBaseUrl(opts.baseUrl);
383
- const response = await fetch(`${baseUrl}/api/agentic-wallet/provision`, {
384
- method: "POST",
385
- headers: { "content-type": "application/json" },
386
- body: "{}"
387
- });
388
- if (!response.ok) {
389
- const text = await response.text();
390
- process.stderr.write(
391
- `[keeperhub-wallet] provision failed: HTTP ${response.status}: ${text}
392
- `
393
- );
394
- process.exit(1);
395
- }
396
- const raw = await response.json();
397
- const data = validateProvisionResponse(raw);
398
- await writeWalletConfig({
399
- subOrgId: data.subOrgId,
400
- walletAddress: data.walletAddress,
401
- hmacSecret: data.hmacSecret
402
- });
403
- process.stdout.write(`subOrgId: ${data.subOrgId}
711
+ try {
712
+ const data = await provisionWallet({ baseUrl: opts.baseUrl });
713
+ process.stdout.write(`subOrgId: ${data.subOrgId}
404
714
  `);
405
- process.stdout.write(`walletAddress: ${data.walletAddress}
715
+ process.stdout.write(`walletAddress: ${data.walletAddress}
406
716
  `);
407
- process.stdout.write(`config written to ${getWalletConfigPath()}
717
+ process.stdout.write(`config written to ${getWalletConfigPath()}
408
718
  `);
719
+ } catch (err) {
720
+ if (err instanceof ProvisionHttpError) {
721
+ process.stderr.write(
722
+ `[keeperhub-wallet] provision failed: HTTP ${err.status}: ${err.body}
723
+ `
724
+ );
725
+ process.exit(1);
726
+ }
727
+ throw err;
728
+ }
409
729
  }
410
730
  async function cmdFund() {
411
731
  const wallet = await readWalletConfig();
@@ -473,6 +793,29 @@ async function runCli(argv = process.argv) {
473
793
  } else if (reg.status === "notice") {
474
794
  process.stderr.write(
475
795
  `notice: ${reg.agent} -> ${reg.message ?? ""}
796
+ `
797
+ );
798
+ } else if (reg.status === "failed") {
799
+ process.stderr.write(
800
+ `hook: ${reg.agent} -> FAILED (${reg.message ?? "unknown error"})
801
+ `
802
+ );
803
+ }
804
+ }
805
+ for (const reg of result.mcpRegistrations) {
806
+ if (reg.status === "registered") {
807
+ process.stdout.write(
808
+ `mcp: ${reg.agent} -> registered at ${reg.path ?? "(unknown path)"}
809
+ `
810
+ );
811
+ } else if (reg.status === "notice") {
812
+ process.stderr.write(
813
+ `notice: ${reg.agent} mcp -> ${reg.message ?? ""}
814
+ `
815
+ );
816
+ } else if (reg.status === "failed") {
817
+ process.stderr.write(
818
+ `mcp: ${reg.agent} -> FAILED (${reg.message ?? "unknown error"})
476
819
  `
477
820
  );
478
821
  }