@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/README.md +21 -5
- package/bin/keeperhub-wallet-mcp.js +21 -0
- package/dist/cli.cjs +562 -165
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +576 -164
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +573 -202
- 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 +587 -201
- package/dist/index.js.map +1 -1
- package/dist/mcp-server.cjs +1305 -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 +1283 -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 +16 -9
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 {
|
|
97
|
-
import {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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 ??
|
|
327
|
+
const home = homeOverride ?? homedir2();
|
|
140
328
|
const results = [];
|
|
141
329
|
for (const spec of AGENT_SPECS) {
|
|
142
|
-
const skillsDir =
|
|
143
|
-
const settingsFile =
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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/
|
|
157
|
-
|
|
158
|
-
|
|
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 =
|
|
163
|
-
const pkgPath =
|
|
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
|
-
|
|
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 =
|
|
251
|
-
return
|
|
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
|
|
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
|
|
627
|
+
await mkdir3(dirname5(settingsPath), { recursive: true, mode: 448 });
|
|
290
628
|
const payload = `${JSON.stringify(config, null, 2)}
|
|
291
629
|
`;
|
|
292
|
-
|
|
293
|
-
|
|
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
|
|
297
|
-
const target =
|
|
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
|
|
646
|
+
await chmod3(target, 420);
|
|
300
647
|
return { agent: agent.agent, path: target, status: "written" };
|
|
301
648
|
}
|
|
302
|
-
function
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
702
|
+
onNotice(noticeMessage);
|
|
329
703
|
}
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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}
|
|
739
|
+
try {
|
|
740
|
+
const data = await provisionWallet({ baseUrl: opts.baseUrl });
|
|
741
|
+
process.stdout.write(`subOrgId: ${data.subOrgId}
|
|
436
742
|
`);
|
|
437
|
-
|
|
743
|
+
process.stdout.write(`walletAddress: ${data.walletAddress}
|
|
438
744
|
`);
|
|
439
|
-
|
|
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
|
}
|