@keeperhub/wallet 0.1.0
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/LICENSE +19 -0
- package/README.md +23 -0
- package/bin/keeperhub-wallet-hook.js +9 -0
- package/bin/keeperhub-wallet.js +7 -0
- package/dist/cli.cjs +636 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +3 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +616 -0
- package/dist/cli.js.map +1 -0
- package/dist/hook-entrypoint.cjs +390 -0
- package/dist/hook-entrypoint.cjs.map +1 -0
- package/dist/hook-entrypoint.d.cts +17 -0
- package/dist/hook-entrypoint.d.ts +17 -0
- package/dist/hook-entrypoint.js +363 -0
- package/dist/hook-entrypoint.js.map +1 -0
- package/dist/index.cjs +1114 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +328 -0
- package/dist/index.d.ts +328 -0
- package/dist/index.js +1065 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -0
- package/skill/keeperhub-wallet.skill.md +62 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
// src/cli.ts
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
|
|
4
|
+
// src/balance.ts
|
|
5
|
+
import {
|
|
6
|
+
createPublicClient,
|
|
7
|
+
erc20Abi,
|
|
8
|
+
formatUnits,
|
|
9
|
+
http
|
|
10
|
+
} from "viem";
|
|
11
|
+
|
|
12
|
+
// src/chains.ts
|
|
13
|
+
import { defineChain } from "viem";
|
|
14
|
+
import { base } from "viem/chains";
|
|
15
|
+
var tempo = defineChain({
|
|
16
|
+
id: 4217,
|
|
17
|
+
name: "Tempo",
|
|
18
|
+
nativeCurrency: { decimals: 18, name: "Ether", symbol: "ETH" },
|
|
19
|
+
rpcUrls: {
|
|
20
|
+
default: {
|
|
21
|
+
http: [process.env.TEMPO_RPC_URL ?? "https://rpc.tempo.xyz"]
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
blockExplorers: {
|
|
25
|
+
default: { name: "Tempo Explorer", url: "https://explorer.tempo.xyz" }
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
var BASE_USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
29
|
+
var TEMPO_USDC_E = "0x20c000000000000000000000b9537d11c60e8b50";
|
|
30
|
+
|
|
31
|
+
// src/hmac.ts
|
|
32
|
+
import { createHash, createHmac } from "crypto";
|
|
33
|
+
function computeSignature(secret, method, path, subOrgId, body, timestamp) {
|
|
34
|
+
const bodyDigest = createHash("sha256").update(body).digest("hex");
|
|
35
|
+
const signingString = `${method}
|
|
36
|
+
${path}
|
|
37
|
+
${subOrgId}
|
|
38
|
+
${bodyDigest}
|
|
39
|
+
${timestamp}`;
|
|
40
|
+
return createHmac("sha256", secret).update(signingString).digest("hex");
|
|
41
|
+
}
|
|
42
|
+
function buildHmacHeaders(secret, method, path, subOrgId, body) {
|
|
43
|
+
const timestamp = String(Math.floor(Date.now() / 1e3));
|
|
44
|
+
const signature = computeSignature(
|
|
45
|
+
secret,
|
|
46
|
+
method,
|
|
47
|
+
path,
|
|
48
|
+
subOrgId,
|
|
49
|
+
body,
|
|
50
|
+
timestamp
|
|
51
|
+
);
|
|
52
|
+
return {
|
|
53
|
+
"X-KH-Sub-Org": subOrgId,
|
|
54
|
+
"X-KH-Timestamp": timestamp,
|
|
55
|
+
"X-KH-Signature": signature
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/types.ts
|
|
60
|
+
var KeeperHubError = class extends Error {
|
|
61
|
+
code;
|
|
62
|
+
constructor(code, message) {
|
|
63
|
+
super(message);
|
|
64
|
+
this.name = "KeeperHubError";
|
|
65
|
+
this.code = code;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
var WalletConfigMissingError = class extends Error {
|
|
69
|
+
constructor() {
|
|
70
|
+
super(
|
|
71
|
+
"Wallet config not found at ~/.keeperhub/wallet.json. Run `npx @keeperhub/wallet add` to provision."
|
|
72
|
+
);
|
|
73
|
+
this.name = "WalletConfigMissingError";
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// src/client.ts
|
|
78
|
+
var TRAILING_SLASH = /\/$/;
|
|
79
|
+
function defaultCodeForStatus(status) {
|
|
80
|
+
if (status === 401) {
|
|
81
|
+
return "HMAC_INVALID";
|
|
82
|
+
}
|
|
83
|
+
if (status === 403) {
|
|
84
|
+
return "POLICY_BLOCKED";
|
|
85
|
+
}
|
|
86
|
+
if (status === 404) {
|
|
87
|
+
return "NOT_FOUND";
|
|
88
|
+
}
|
|
89
|
+
if (status === 502) {
|
|
90
|
+
return "TURNKEY_UPSTREAM";
|
|
91
|
+
}
|
|
92
|
+
return `HTTP_${status}`;
|
|
93
|
+
}
|
|
94
|
+
var KeeperHubClient = class {
|
|
95
|
+
baseUrl;
|
|
96
|
+
fetchImpl;
|
|
97
|
+
wallet;
|
|
98
|
+
constructor(wallet, opts = {}) {
|
|
99
|
+
this.wallet = wallet;
|
|
100
|
+
const envBase = process.env.KEEPERHUB_API_URL;
|
|
101
|
+
this.baseUrl = (opts.baseUrl ?? envBase ?? "https://app.keeperhub.com").replace(TRAILING_SLASH, "");
|
|
102
|
+
this.fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* HMAC-signed POST/GET to any /api/agentic-wallet/* route except
|
|
106
|
+
* /provision. Path MUST start with a leading slash. Body is
|
|
107
|
+
* JSON.stringify'd (or the empty string for GET).
|
|
108
|
+
*
|
|
109
|
+
* Error mapping: non-2xx/non-202 surface as `KeeperHubError(code,
|
|
110
|
+
* message)` where `code` is the server-supplied field or the default
|
|
111
|
+
* taxonomy (`HMAC_INVALID`, `POLICY_BLOCKED`, `NOT_FOUND`,
|
|
112
|
+
* `TURNKEY_UPSTREAM`, `HTTP_<status>`). 202 ask-tier surfaces as an
|
|
113
|
+
* AskTierResponse envelope.
|
|
114
|
+
*/
|
|
115
|
+
async request(method, path, body) {
|
|
116
|
+
const bodyStr = body === void 0 ? "" : JSON.stringify(body);
|
|
117
|
+
const hmacHeaders = buildHmacHeaders(
|
|
118
|
+
this.wallet.hmacSecret,
|
|
119
|
+
method,
|
|
120
|
+
path,
|
|
121
|
+
this.wallet.subOrgId,
|
|
122
|
+
bodyStr
|
|
123
|
+
);
|
|
124
|
+
const headers = method === "POST" ? { ...hmacHeaders, "content-type": "application/json" } : { ...hmacHeaders };
|
|
125
|
+
const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
126
|
+
method,
|
|
127
|
+
headers,
|
|
128
|
+
body: method === "POST" ? bodyStr : void 0
|
|
129
|
+
});
|
|
130
|
+
if (response.status === 202) {
|
|
131
|
+
const data = await response.json();
|
|
132
|
+
return { _status: 202, approvalRequestId: data.approvalRequestId };
|
|
133
|
+
}
|
|
134
|
+
if (!response.ok) {
|
|
135
|
+
let code = "UNKNOWN";
|
|
136
|
+
let message = `HTTP ${response.status}`;
|
|
137
|
+
try {
|
|
138
|
+
const data = await response.json();
|
|
139
|
+
code = data.code ?? defaultCodeForStatus(response.status);
|
|
140
|
+
message = data.error ?? message;
|
|
141
|
+
} catch {
|
|
142
|
+
}
|
|
143
|
+
throw new KeeperHubError(code, message);
|
|
144
|
+
}
|
|
145
|
+
return await response.json();
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// src/balance.ts
|
|
150
|
+
var USDC_DECIMALS = 6;
|
|
151
|
+
async function checkBalance(wallet, opts = {}) {
|
|
152
|
+
const baseClient = opts.baseClient ?? createPublicClient({
|
|
153
|
+
chain: base,
|
|
154
|
+
transport: http()
|
|
155
|
+
});
|
|
156
|
+
const tempoClient = opts.tempoClient ?? createPublicClient({
|
|
157
|
+
chain: tempo,
|
|
158
|
+
transport: http()
|
|
159
|
+
});
|
|
160
|
+
const khClient = opts.khClient ?? new KeeperHubClient(wallet);
|
|
161
|
+
const [baseRaw, tempoRaw, credit] = await Promise.all([
|
|
162
|
+
baseClient.readContract({
|
|
163
|
+
address: BASE_USDC,
|
|
164
|
+
abi: erc20Abi,
|
|
165
|
+
functionName: "balanceOf",
|
|
166
|
+
args: [wallet.walletAddress]
|
|
167
|
+
}),
|
|
168
|
+
tempoClient.readContract({
|
|
169
|
+
address: TEMPO_USDC_E,
|
|
170
|
+
abi: erc20Abi,
|
|
171
|
+
functionName: "balanceOf",
|
|
172
|
+
args: [wallet.walletAddress]
|
|
173
|
+
}),
|
|
174
|
+
khClient.request("GET", "/api/agentic-wallet/credit")
|
|
175
|
+
]);
|
|
176
|
+
if ("_status" in credit) {
|
|
177
|
+
throw new Error("Unexpected 202 response from /api/agentic-wallet/credit");
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
base: {
|
|
181
|
+
chain: "base",
|
|
182
|
+
token: "USDC",
|
|
183
|
+
amount: formatUnits(baseRaw, USDC_DECIMALS),
|
|
184
|
+
address: wallet.walletAddress
|
|
185
|
+
},
|
|
186
|
+
tempo: {
|
|
187
|
+
chain: "tempo",
|
|
188
|
+
token: "USDC.e",
|
|
189
|
+
amount: formatUnits(tempoRaw, USDC_DECIMALS),
|
|
190
|
+
address: wallet.walletAddress
|
|
191
|
+
},
|
|
192
|
+
offChainCredit: {
|
|
193
|
+
amount: credit.amount,
|
|
194
|
+
currency: "USD"
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// src/fund.ts
|
|
200
|
+
var EVM_ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
201
|
+
var COINBASE_HOST = "pay.coinbase.com";
|
|
202
|
+
var COINBASE_PATH = "/buy/select-asset";
|
|
203
|
+
function fund(walletAddress) {
|
|
204
|
+
if (!EVM_ADDRESS_RE.test(walletAddress)) {
|
|
205
|
+
throw new Error(`Invalid EVM wallet address: ${walletAddress}`);
|
|
206
|
+
}
|
|
207
|
+
const params = new URLSearchParams({
|
|
208
|
+
defaultNetwork: "base",
|
|
209
|
+
defaultAsset: "USDC",
|
|
210
|
+
addresses: JSON.stringify({ [walletAddress]: ["base"] }),
|
|
211
|
+
presetCryptoAmount: "5"
|
|
212
|
+
});
|
|
213
|
+
const coinbaseOnrampUrl = `https://${COINBASE_HOST}${COINBASE_PATH}?${params.toString()}`;
|
|
214
|
+
const disclaimer = "If the Coinbase page does not pre-fill, paste your address manually. For Tempo USDC.e, transfer from an exchange or another wallet to the address above -- Onramp does not support Tempo directly. Coinbase sessionToken URLs are the 2025+ canonical form; legacy query-param URLs may drop prefill on some accounts.";
|
|
215
|
+
return {
|
|
216
|
+
coinbaseOnrampUrl,
|
|
217
|
+
tempoAddress: walletAddress,
|
|
218
|
+
disclaimer
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// src/skill-install.ts
|
|
223
|
+
import { chmod, copyFile, mkdir, readFile, writeFile } from "fs/promises";
|
|
224
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
225
|
+
import { fileURLToPath } from "url";
|
|
226
|
+
|
|
227
|
+
// src/agent-detect.ts
|
|
228
|
+
import { existsSync } from "fs";
|
|
229
|
+
import { homedir } from "os";
|
|
230
|
+
import { dirname, join } from "path";
|
|
231
|
+
var AGENT_SPECS = [
|
|
232
|
+
{
|
|
233
|
+
agent: "claude-code",
|
|
234
|
+
skillsRel: [".claude", "skills"],
|
|
235
|
+
settingsRel: [".claude", "settings.json"],
|
|
236
|
+
hookSupport: "claude-code"
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
agent: "cursor",
|
|
240
|
+
skillsRel: [".cursor", "skills"],
|
|
241
|
+
settingsRel: [".cursor", "settings.json"],
|
|
242
|
+
hookSupport: "notice"
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
agent: "cline",
|
|
246
|
+
skillsRel: [".cline", "skills"],
|
|
247
|
+
settingsRel: [".cline", "settings.json"],
|
|
248
|
+
hookSupport: "notice"
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
agent: "windsurf",
|
|
252
|
+
skillsRel: [".windsurf", "skills"],
|
|
253
|
+
settingsRel: [".windsurf", "settings.json"],
|
|
254
|
+
hookSupport: "notice"
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
agent: "opencode",
|
|
258
|
+
skillsRel: [".config", "opencode", "skills"],
|
|
259
|
+
settingsRel: [".config", "opencode", "settings.json"],
|
|
260
|
+
hookSupport: "notice"
|
|
261
|
+
}
|
|
262
|
+
];
|
|
263
|
+
function detectAgents(homeOverride) {
|
|
264
|
+
const home = homeOverride ?? homedir();
|
|
265
|
+
const results = [];
|
|
266
|
+
for (const spec of AGENT_SPECS) {
|
|
267
|
+
const skillsDir = join(home, ...spec.skillsRel);
|
|
268
|
+
const settingsFile = join(home, ...spec.settingsRel);
|
|
269
|
+
if (existsSync(dirname(skillsDir))) {
|
|
270
|
+
results.push({
|
|
271
|
+
agent: spec.agent,
|
|
272
|
+
skillsDir,
|
|
273
|
+
settingsFile,
|
|
274
|
+
hookSupport: spec.hookSupport
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return results;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// src/skill-install.ts
|
|
282
|
+
var HOOK_COMMAND = "keeperhub-wallet-hook";
|
|
283
|
+
var KEEPERHUB_HOOK_MARKER = "keeperhub-wallet-hook";
|
|
284
|
+
function buildKeeperhubEntry() {
|
|
285
|
+
return {
|
|
286
|
+
matcher: "*",
|
|
287
|
+
hooks: [{ type: "command", command: HOOK_COMMAND }]
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
function resolveDefaultSkillSource() {
|
|
291
|
+
const here = dirname2(fileURLToPath(import.meta.url));
|
|
292
|
+
return join2(here, "..", "skill", "keeperhub-wallet.skill.md");
|
|
293
|
+
}
|
|
294
|
+
function defaultNotice(msg) {
|
|
295
|
+
process.stderr.write(`${msg}
|
|
296
|
+
`);
|
|
297
|
+
}
|
|
298
|
+
async function registerClaudeCodeHook(settingsPath) {
|
|
299
|
+
let raw = null;
|
|
300
|
+
try {
|
|
301
|
+
raw = await readFile(settingsPath, "utf-8");
|
|
302
|
+
} catch (err) {
|
|
303
|
+
if (err.code !== "ENOENT") {
|
|
304
|
+
throw err;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
let config = {};
|
|
308
|
+
if (raw !== null) {
|
|
309
|
+
try {
|
|
310
|
+
config = JSON.parse(raw);
|
|
311
|
+
} catch {
|
|
312
|
+
throw new Error(
|
|
313
|
+
`settings.json at ${settingsPath} is not valid JSON; aborting hook registration`
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const hooks = typeof config.hooks === "object" && config.hooks !== null ? config.hooks : {};
|
|
318
|
+
const existingPreToolUse = Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse : [];
|
|
319
|
+
const filtered = [];
|
|
320
|
+
for (const entry of existingPreToolUse) {
|
|
321
|
+
const serialised = JSON.stringify(entry);
|
|
322
|
+
if (!serialised.includes(KEEPERHUB_HOOK_MARKER)) {
|
|
323
|
+
filtered.push(entry);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
filtered.push(buildKeeperhubEntry());
|
|
327
|
+
hooks.PreToolUse = filtered;
|
|
328
|
+
config.hooks = hooks;
|
|
329
|
+
await mkdir(dirname2(settingsPath), { recursive: true, mode: 448 });
|
|
330
|
+
const payload = `${JSON.stringify(config, null, 2)}
|
|
331
|
+
`;
|
|
332
|
+
await writeFile(settingsPath, payload, { mode: 384 });
|
|
333
|
+
await chmod(settingsPath, 384);
|
|
334
|
+
}
|
|
335
|
+
async function writeSkillToAgent(agent, skillSource) {
|
|
336
|
+
await mkdir(agent.skillsDir, { recursive: true, mode: 493 });
|
|
337
|
+
const target = join2(agent.skillsDir, "keeperhub-wallet.skill.md");
|
|
338
|
+
await copyFile(skillSource, target);
|
|
339
|
+
await chmod(target, 420);
|
|
340
|
+
return { agent: agent.agent, path: target, status: "written" };
|
|
341
|
+
}
|
|
342
|
+
function buildNoticeMessage(agent) {
|
|
343
|
+
return `${agent.agent} does not support auto-registered PreToolUse hooks; run \`${HOOK_COMMAND}\` on every tool use via ${agent.agent}'s settings file at ${agent.settingsFile}`;
|
|
344
|
+
}
|
|
345
|
+
async function installSkill(options = {}) {
|
|
346
|
+
const agents = detectAgents(options.homeOverride);
|
|
347
|
+
const skillSource = options.skillSourcePath ?? resolveDefaultSkillSource();
|
|
348
|
+
const onNotice = options.onNotice ?? defaultNotice;
|
|
349
|
+
const skillWrites = [];
|
|
350
|
+
const hookRegistrations = [];
|
|
351
|
+
for (const agent of agents) {
|
|
352
|
+
const write = await writeSkillToAgent(agent, skillSource);
|
|
353
|
+
skillWrites.push(write);
|
|
354
|
+
if (agent.hookSupport === "claude-code") {
|
|
355
|
+
await registerClaudeCodeHook(agent.settingsFile);
|
|
356
|
+
hookRegistrations.push({
|
|
357
|
+
agent: agent.agent,
|
|
358
|
+
status: "registered"
|
|
359
|
+
});
|
|
360
|
+
} else {
|
|
361
|
+
const message = buildNoticeMessage(agent);
|
|
362
|
+
hookRegistrations.push({
|
|
363
|
+
agent: agent.agent,
|
|
364
|
+
status: "notice",
|
|
365
|
+
message
|
|
366
|
+
});
|
|
367
|
+
onNotice(message);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return { skillWrites, hookRegistrations };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// src/storage.ts
|
|
374
|
+
import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
375
|
+
import { homedir as homedir2 } from "os";
|
|
376
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
377
|
+
async function readWalletConfig() {
|
|
378
|
+
const walletPath = join3(homedir2(), ".keeperhub", "wallet.json");
|
|
379
|
+
let raw;
|
|
380
|
+
try {
|
|
381
|
+
raw = await readFile2(walletPath, "utf-8");
|
|
382
|
+
} catch (err) {
|
|
383
|
+
if (err.code === "ENOENT") {
|
|
384
|
+
throw new WalletConfigMissingError();
|
|
385
|
+
}
|
|
386
|
+
throw err;
|
|
387
|
+
}
|
|
388
|
+
const parsed = JSON.parse(raw);
|
|
389
|
+
if (!(parsed.subOrgId && parsed.walletAddress && parsed.hmacSecret)) {
|
|
390
|
+
throw new Error(`Malformed wallet.json at ${walletPath}`);
|
|
391
|
+
}
|
|
392
|
+
return parsed;
|
|
393
|
+
}
|
|
394
|
+
async function writeWalletConfig(config) {
|
|
395
|
+
const walletPath = join3(homedir2(), ".keeperhub", "wallet.json");
|
|
396
|
+
await mkdir2(dirname3(walletPath), { recursive: true, mode: 448 });
|
|
397
|
+
await writeFile2(walletPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
398
|
+
await chmod2(walletPath, 384);
|
|
399
|
+
}
|
|
400
|
+
function getWalletConfigPath() {
|
|
401
|
+
return join3(homedir2(), ".keeperhub", "wallet.json");
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// src/cli.ts
|
|
405
|
+
var TRAILING_SLASH2 = /\/$/;
|
|
406
|
+
var WALLET_ADDRESS_PATTERN = /^0x[a-fA-F0-9]{40}$/;
|
|
407
|
+
function resolveBaseUrl(override) {
|
|
408
|
+
const candidate = override ?? process.env.KEEPERHUB_API_URL ?? "https://app.keeperhub.com";
|
|
409
|
+
return candidate.replace(TRAILING_SLASH2, "");
|
|
410
|
+
}
|
|
411
|
+
function isNonEmptyString(value) {
|
|
412
|
+
return typeof value === "string" && value.length > 0;
|
|
413
|
+
}
|
|
414
|
+
function provisionInvalidError(message) {
|
|
415
|
+
const err = new Error(message);
|
|
416
|
+
err.code = "PROVISION_RESPONSE_INVALID";
|
|
417
|
+
return err;
|
|
418
|
+
}
|
|
419
|
+
function validateProvisionResponse(data) {
|
|
420
|
+
if (typeof data !== "object" || data === null) {
|
|
421
|
+
throw provisionInvalidError("provision response is not an object");
|
|
422
|
+
}
|
|
423
|
+
const { subOrgId, walletAddress, hmacSecret } = data;
|
|
424
|
+
if (!(isNonEmptyString(subOrgId) && isNonEmptyString(walletAddress) && isNonEmptyString(hmacSecret))) {
|
|
425
|
+
throw provisionInvalidError(
|
|
426
|
+
"provision response missing subOrgId, walletAddress, or hmacSecret"
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
if (!WALLET_ADDRESS_PATTERN.test(walletAddress)) {
|
|
430
|
+
throw provisionInvalidError(
|
|
431
|
+
`provision response walletAddress is not a valid 0x-prefixed 40-hex address: ${walletAddress}`
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
return {
|
|
435
|
+
subOrgId,
|
|
436
|
+
walletAddress,
|
|
437
|
+
hmacSecret
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
async function cmdAdd(opts = {}) {
|
|
441
|
+
const baseUrl = resolveBaseUrl(opts.baseUrl);
|
|
442
|
+
const response = await fetch(`${baseUrl}/api/agentic-wallet/provision`, {
|
|
443
|
+
method: "POST",
|
|
444
|
+
headers: { "content-type": "application/json" },
|
|
445
|
+
body: "{}"
|
|
446
|
+
});
|
|
447
|
+
if (!response.ok) {
|
|
448
|
+
const text = await response.text();
|
|
449
|
+
process.stderr.write(
|
|
450
|
+
`[keeperhub-wallet] provision failed: HTTP ${response.status}: ${text}
|
|
451
|
+
`
|
|
452
|
+
);
|
|
453
|
+
process.exit(1);
|
|
454
|
+
}
|
|
455
|
+
const raw = await response.json();
|
|
456
|
+
const data = validateProvisionResponse(raw);
|
|
457
|
+
await writeWalletConfig({
|
|
458
|
+
subOrgId: data.subOrgId,
|
|
459
|
+
walletAddress: data.walletAddress,
|
|
460
|
+
hmacSecret: data.hmacSecret
|
|
461
|
+
});
|
|
462
|
+
process.stdout.write(`subOrgId: ${data.subOrgId}
|
|
463
|
+
`);
|
|
464
|
+
process.stdout.write(`walletAddress: ${data.walletAddress}
|
|
465
|
+
`);
|
|
466
|
+
process.stdout.write(`config written to ${getWalletConfigPath()}
|
|
467
|
+
`);
|
|
468
|
+
}
|
|
469
|
+
async function cmdLink(opts = {}) {
|
|
470
|
+
const wallet = await readWalletConfig();
|
|
471
|
+
const baseUrl = resolveBaseUrl(opts.baseUrl);
|
|
472
|
+
const sessionCookie = process.env.KH_SESSION_COOKIE;
|
|
473
|
+
if (!sessionCookie) {
|
|
474
|
+
process.stderr.write(
|
|
475
|
+
"[keeperhub-wallet] link requires KH_SESSION_COOKIE env var.\nSign in at app.keeperhub.com, copy the session cookie, and re-run with:\n KH_SESSION_COOKIE='<cookie>' npx @keeperhub/wallet link\n"
|
|
476
|
+
);
|
|
477
|
+
process.exit(1);
|
|
478
|
+
}
|
|
479
|
+
const body = JSON.stringify({ subOrgId: wallet.subOrgId });
|
|
480
|
+
const headers = buildHmacHeaders(
|
|
481
|
+
wallet.hmacSecret,
|
|
482
|
+
"POST",
|
|
483
|
+
"/api/agentic-wallet/link",
|
|
484
|
+
wallet.subOrgId,
|
|
485
|
+
body
|
|
486
|
+
);
|
|
487
|
+
const response = await fetch(`${baseUrl}/api/agentic-wallet/link`, {
|
|
488
|
+
method: "POST",
|
|
489
|
+
headers: {
|
|
490
|
+
...headers,
|
|
491
|
+
"content-type": "application/json",
|
|
492
|
+
cookie: sessionCookie
|
|
493
|
+
},
|
|
494
|
+
body
|
|
495
|
+
});
|
|
496
|
+
const json = await response.json().catch(() => ({}));
|
|
497
|
+
if (!response.ok) {
|
|
498
|
+
process.stderr.write(
|
|
499
|
+
`[keeperhub-wallet] link failed: ${json.code ?? response.status}: ${json.error ?? ""}
|
|
500
|
+
`
|
|
501
|
+
);
|
|
502
|
+
process.exit(1);
|
|
503
|
+
}
|
|
504
|
+
if (json.already) {
|
|
505
|
+
process.stdout.write("already linked\n");
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
process.stdout.write("linked\n");
|
|
509
|
+
}
|
|
510
|
+
async function cmdFund() {
|
|
511
|
+
const wallet = await readWalletConfig();
|
|
512
|
+
const out = fund(wallet.walletAddress);
|
|
513
|
+
process.stdout.write(`${out.coinbaseOnrampUrl}
|
|
514
|
+
`);
|
|
515
|
+
process.stdout.write(`Tempo address: ${out.tempoAddress}
|
|
516
|
+
`);
|
|
517
|
+
process.stdout.write(`${out.disclaimer}
|
|
518
|
+
`);
|
|
519
|
+
}
|
|
520
|
+
async function cmdBalance() {
|
|
521
|
+
const wallet = await readWalletConfig();
|
|
522
|
+
const snap = await checkBalance(wallet);
|
|
523
|
+
process.stdout.write(`Base USDC: ${snap.base.amount}
|
|
524
|
+
`);
|
|
525
|
+
process.stdout.write(`Tempo USDC.e: ${snap.tempo.amount}
|
|
526
|
+
`);
|
|
527
|
+
process.stdout.write(
|
|
528
|
+
`KeeperHub credit: ${snap.offChainCredit.amount} ${snap.offChainCredit.currency}
|
|
529
|
+
`
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
async function cmdInfo() {
|
|
533
|
+
const wallet = await readWalletConfig();
|
|
534
|
+
process.stdout.write(`subOrgId: ${wallet.subOrgId}
|
|
535
|
+
`);
|
|
536
|
+
process.stdout.write(`walletAddress: ${wallet.walletAddress}
|
|
537
|
+
`);
|
|
538
|
+
}
|
|
539
|
+
async function runCli(argv = process.argv) {
|
|
540
|
+
const program = new Command();
|
|
541
|
+
program.name("keeperhub-wallet").description(
|
|
542
|
+
"KeeperHub agentic wallet CLI (auto-pay x402 + MPP 402 responses)"
|
|
543
|
+
).version("0.1.0");
|
|
544
|
+
program.command("add").description("Provision a new agentic wallet (no account required)").option("--base-url <url>", "KeeperHub API base URL").action(async (opts) => {
|
|
545
|
+
await cmdAdd(opts);
|
|
546
|
+
});
|
|
547
|
+
program.command("link").description(
|
|
548
|
+
"Link the current wallet to your KeeperHub account (requires KH_SESSION_COOKIE env)"
|
|
549
|
+
).option("--base-url <url>", "KeeperHub API base URL").action(async (opts) => {
|
|
550
|
+
await cmdLink(opts);
|
|
551
|
+
});
|
|
552
|
+
program.command("fund").description(
|
|
553
|
+
"Print Coinbase Onramp URL (Base USDC) and Tempo deposit address"
|
|
554
|
+
).action(async () => {
|
|
555
|
+
await cmdFund();
|
|
556
|
+
});
|
|
557
|
+
program.command("balance").description(
|
|
558
|
+
"Print unified balance: Base USDC + Tempo USDC.e + off-chain KeeperHub credit"
|
|
559
|
+
).action(async () => {
|
|
560
|
+
await cmdBalance();
|
|
561
|
+
});
|
|
562
|
+
program.command("info").description("Print subOrgId and walletAddress from local config").action(async () => {
|
|
563
|
+
await cmdInfo();
|
|
564
|
+
});
|
|
565
|
+
program.command("skill").description(
|
|
566
|
+
"Install the KeeperHub skill file into detected agent directories"
|
|
567
|
+
).addCommand(
|
|
568
|
+
new Command("install").description(
|
|
569
|
+
"Write skill file + register PreToolUse hook in all detected agents"
|
|
570
|
+
).action(async () => {
|
|
571
|
+
const result = await installSkill();
|
|
572
|
+
for (const write of result.skillWrites) {
|
|
573
|
+
process.stdout.write(
|
|
574
|
+
`skill: ${write.agent} -> ${write.path} (${write.status})
|
|
575
|
+
`
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
for (const reg of result.hookRegistrations) {
|
|
579
|
+
if (reg.status === "registered") {
|
|
580
|
+
process.stdout.write(
|
|
581
|
+
`hook: ${reg.agent} -> PreToolUse registered
|
|
582
|
+
`
|
|
583
|
+
);
|
|
584
|
+
} else if (reg.status === "notice") {
|
|
585
|
+
process.stderr.write(
|
|
586
|
+
`notice: ${reg.agent} -> ${reg.message ?? ""}
|
|
587
|
+
`
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
if (result.skillWrites.length === 0) {
|
|
592
|
+
process.stderr.write(
|
|
593
|
+
"No supported agent skill directories detected under $HOME. Create ~/.claude/, ~/.cursor/, ~/.cline/, ~/.windsurf/, or ~/.config/opencode/ and re-run.\n"
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
})
|
|
597
|
+
);
|
|
598
|
+
try {
|
|
599
|
+
await program.parseAsync(argv);
|
|
600
|
+
} catch (err) {
|
|
601
|
+
if (err instanceof WalletConfigMissingError) {
|
|
602
|
+
process.stderr.write(`[keeperhub-wallet] ${err.message}
|
|
603
|
+
`);
|
|
604
|
+
process.exit(1);
|
|
605
|
+
}
|
|
606
|
+
process.stderr.write(
|
|
607
|
+
`[keeperhub-wallet] ${err.message ?? String(err)}
|
|
608
|
+
`
|
|
609
|
+
);
|
|
610
|
+
process.exit(1);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
export {
|
|
614
|
+
runCli
|
|
615
|
+
};
|
|
616
|
+
//# sourceMappingURL=cli.js.map
|