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