@keeperhub/wallet 0.1.11 → 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 +461 -165
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +475 -164
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +475 -177
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +71 -245
- package/dist/index.d.ts +71 -245
- package/dist/index.js +486 -173
- 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 -1
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 {
|
|
97
|
-
import {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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 ??
|
|
299
|
+
const home = homeOverride ?? homedir2();
|
|
140
300
|
const results = [];
|
|
141
301
|
for (const spec of AGENT_SPECS) {
|
|
142
|
-
const skillsDir =
|
|
143
|
-
const settingsFile =
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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/
|
|
157
|
-
|
|
158
|
-
|
|
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 =
|
|
163
|
-
const pkgPath =
|
|
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,6 @@ function readPackageVersion() {
|
|
|
170
357
|
}
|
|
171
358
|
return "latest";
|
|
172
359
|
}
|
|
173
|
-
function buildNpxCommand(version) {
|
|
174
|
-
return `npx -y -p ${PACKAGE_NAME}@${version} ${HOOK_BIN}`;
|
|
175
|
-
}
|
|
176
360
|
function isNpxExecution() {
|
|
177
361
|
const execPath = process.env.npm_execpath;
|
|
178
362
|
if (typeof execPath !== "string" || execPath.length === 0) {
|
|
@@ -200,6 +384,144 @@ function isPathUnderTransientCache(resolvedPath) {
|
|
|
200
384
|
}
|
|
201
385
|
return false;
|
|
202
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 };
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// src/skill-install.ts
|
|
524
|
+
var HOOK_BIN = "keeperhub-wallet-hook";
|
|
203
525
|
var KEEPERHUB_HOOK_MARKER = HOOK_BIN;
|
|
204
526
|
function filterKeeperhubHooksFromEntry(entry) {
|
|
205
527
|
if (typeof entry !== "object" || entry === null) {
|
|
@@ -226,19 +548,7 @@ function resolveHookCommand() {
|
|
|
226
548
|
if (envOverride && envOverride.length > 0) {
|
|
227
549
|
return envOverride;
|
|
228
550
|
}
|
|
229
|
-
|
|
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());
|
|
551
|
+
return resolveBinCommand(HOOK_BIN).commandString;
|
|
242
552
|
}
|
|
243
553
|
function buildKeeperhubEntry(command) {
|
|
244
554
|
return {
|
|
@@ -247,8 +557,8 @@ function buildKeeperhubEntry(command) {
|
|
|
247
557
|
};
|
|
248
558
|
}
|
|
249
559
|
function resolveDefaultSkillSource() {
|
|
250
|
-
const here =
|
|
251
|
-
return
|
|
560
|
+
const here = dirname5(fileURLToPath2(import.meta.url));
|
|
561
|
+
return join5(here, "..", "skill", "keeperhub-wallet.skill.md");
|
|
252
562
|
}
|
|
253
563
|
function defaultNotice(msg) {
|
|
254
564
|
process.stderr.write(`${msg}
|
|
@@ -258,7 +568,7 @@ async function registerClaudeCodeHook(settingsPath, options = {}) {
|
|
|
258
568
|
const command = options.hookCommand ?? resolveHookCommand();
|
|
259
569
|
let raw = null;
|
|
260
570
|
try {
|
|
261
|
-
raw = await
|
|
571
|
+
raw = await readFile3(settingsPath, "utf-8");
|
|
262
572
|
} catch (err) {
|
|
263
573
|
if (err.code !== "ENOENT") {
|
|
264
574
|
throw err;
|
|
@@ -286,158 +596,136 @@ async function registerClaudeCodeHook(settingsPath, options = {}) {
|
|
|
286
596
|
filtered.push(buildKeeperhubEntry(command));
|
|
287
597
|
hooks.PreToolUse = filtered;
|
|
288
598
|
config.hooks = hooks;
|
|
289
|
-
await
|
|
599
|
+
await mkdir3(dirname5(settingsPath), { recursive: true, mode: 448 });
|
|
290
600
|
const payload = `${JSON.stringify(config, null, 2)}
|
|
291
601
|
`;
|
|
292
|
-
|
|
293
|
-
|
|
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
|
+
}
|
|
294
613
|
}
|
|
295
614
|
async function writeSkillToAgent(agent, skillSource) {
|
|
296
|
-
await
|
|
297
|
-
const target =
|
|
615
|
+
await mkdir3(agent.skillsDir, { recursive: true, mode: 493 });
|
|
616
|
+
const target = join5(agent.skillsDir, "keeperhub-wallet.skill.md");
|
|
298
617
|
await copyFile(skillSource, target);
|
|
299
|
-
await
|
|
618
|
+
await chmod3(target, 420);
|
|
300
619
|
return { agent: agent.agent, path: target, status: "written" };
|
|
301
620
|
}
|
|
302
|
-
function
|
|
621
|
+
function buildHookNoticeMessage(agent, command) {
|
|
303
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}`;
|
|
304
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
|
+
}
|
|
305
628
|
async function installSkill(options = {}) {
|
|
306
629
|
const agents = detectAgents(options.homeOverride);
|
|
307
630
|
const skillSource = options.skillSourcePath ?? resolveDefaultSkillSource();
|
|
308
631
|
const onNotice = options.onNotice ?? defaultNotice;
|
|
309
632
|
const hookCommand = options.hookCommand ?? resolveHookCommand();
|
|
633
|
+
const mcpCommand = options.mcpCommand ?? resolveMcpCommand();
|
|
310
634
|
const skillWrites = [];
|
|
311
635
|
const hookRegistrations = [];
|
|
636
|
+
const mcpRegistrations = [];
|
|
312
637
|
for (const agent of agents) {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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({
|
|
318
644
|
agent: agent.agent,
|
|
319
|
-
|
|
645
|
+
path: "",
|
|
646
|
+
status: "skipped"
|
|
320
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
|
+
}
|
|
321
667
|
} else {
|
|
322
|
-
const
|
|
668
|
+
const noticeMessage = buildHookNoticeMessage(agent, hookCommand);
|
|
323
669
|
hookRegistrations.push({
|
|
324
670
|
agent: agent.agent,
|
|
325
671
|
status: "notice",
|
|
326
|
-
message
|
|
672
|
+
message: noticeMessage
|
|
327
673
|
});
|
|
328
|
-
onNotice(
|
|
674
|
+
onNotice(noticeMessage);
|
|
329
675
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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})`);
|
|
358
704
|
}
|
|
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
705
|
}
|
|
365
|
-
return
|
|
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");
|
|
706
|
+
return { skillWrites, hookRegistrations, mcpRegistrations };
|
|
375
707
|
}
|
|
376
708
|
|
|
377
709
|
// 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
710
|
async function cmdAdd(opts = {}) {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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}
|
|
711
|
+
try {
|
|
712
|
+
const data = await provisionWallet({ baseUrl: opts.baseUrl });
|
|
713
|
+
process.stdout.write(`subOrgId: ${data.subOrgId}
|
|
436
714
|
`);
|
|
437
|
-
|
|
715
|
+
process.stdout.write(`walletAddress: ${data.walletAddress}
|
|
438
716
|
`);
|
|
439
|
-
|
|
717
|
+
process.stdout.write(`config written to ${getWalletConfigPath()}
|
|
440
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
|
+
}
|
|
441
729
|
}
|
|
442
730
|
async function cmdFund() {
|
|
443
731
|
const wallet = await readWalletConfig();
|
|
@@ -505,6 +793,29 @@ async function runCli(argv = process.argv) {
|
|
|
505
793
|
} else if (reg.status === "notice") {
|
|
506
794
|
process.stderr.write(
|
|
507
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"})
|
|
508
819
|
`
|
|
509
820
|
);
|
|
510
821
|
}
|