moneyswitch 0.5.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/dist/cli.js ADDED
@@ -0,0 +1,523 @@
1
+ #!/usr/bin/env node
2
+
3
+ // ../connect/dist/lib/args.js
4
+ function parseArgs(argv) {
5
+ let command = "connect";
6
+ const rest = [...argv];
7
+ if (rest[0] === "status" || rest[0] === "remove") {
8
+ command = rest.shift();
9
+ }
10
+ let server;
11
+ let key;
12
+ let apply = false;
13
+ let json = false;
14
+ let help = false;
15
+ for (let i = 0; i < rest.length; i++) {
16
+ const arg = rest[i];
17
+ if (arg === "--server") {
18
+ server = rest[++i];
19
+ if (server === void 0)
20
+ return { error: "--server requires a value" };
21
+ } else if (arg === "--key") {
22
+ key = rest[++i];
23
+ if (key === void 0)
24
+ return { error: "--key requires a value" };
25
+ } else if (arg === "--apply") {
26
+ apply = true;
27
+ } else if (arg === "--json") {
28
+ json = true;
29
+ } else if (arg === "--help" || arg === "-h") {
30
+ help = true;
31
+ } else {
32
+ return { error: `unknown argument: ${arg}` };
33
+ }
34
+ }
35
+ if (!help && (command === "connect" || command === "status")) {
36
+ if (!server)
37
+ return { error: "--server is required" };
38
+ if (!key)
39
+ return { error: "--key is required" };
40
+ }
41
+ return { command, server, key, apply, json, help };
42
+ }
43
+
44
+ // ../connect/dist/lib/status.js
45
+ async function fetchStatus(server, key, fetchImpl = fetch) {
46
+ const base = server.replace(/\/+$/, "");
47
+ const res = await fetchImpl(`${base}/v1/status`, {
48
+ method: "GET",
49
+ headers: { Authorization: `Bearer ${key}` }
50
+ });
51
+ let body = null;
52
+ try {
53
+ body = await res.json();
54
+ } catch {
55
+ body = null;
56
+ }
57
+ return { ok: res.ok, httpStatus: res.status, body };
58
+ }
59
+
60
+ // ../connect/dist/lib/detect.js
61
+ import fs from "node:fs";
62
+ import os from "node:os";
63
+ import path from "node:path";
64
+ function detectClaude(runner) {
65
+ const res = runner.run("claude", ["--version"]);
66
+ return res.ok;
67
+ }
68
+ function codexHomeDir(env = process.env) {
69
+ if (env.CODEX_HOME)
70
+ return env.CODEX_HOME;
71
+ return path.join(os.homedir(), ".codex");
72
+ }
73
+ function codexConfigPath(env = process.env) {
74
+ return path.join(codexHomeDir(env), "config.toml");
75
+ }
76
+ function detectCodex(runner, env = process.env) {
77
+ const inPath = runner.run("codex", ["--version"]).ok;
78
+ const dirExists = fs.existsSync(codexHomeDir(env));
79
+ return inPath || dirExists;
80
+ }
81
+
82
+ // ../connect/dist/lib/claude.js
83
+ var SERVER_NAME = "moneyswitch";
84
+ function applyClaude(runner, server, key, mcpCommand) {
85
+ const removed = runner.run("claude", ["mcp", "remove", SERVER_NAME, "-s", "user"]);
86
+ const added = runner.run("claude", [
87
+ "mcp",
88
+ "add",
89
+ SERVER_NAME,
90
+ "-s",
91
+ "user",
92
+ "-e",
93
+ `MONEY_API_BASE=${server}`,
94
+ "-e",
95
+ `MONEY_API_KEY=${key}`,
96
+ "--",
97
+ mcpCommand.command,
98
+ ...mcpCommand.args
99
+ ]);
100
+ return { removed, added };
101
+ }
102
+ function removeClaude(runner) {
103
+ return runner.run("claude", ["mcp", "remove", SERVER_NAME, "-s", "user"]);
104
+ }
105
+
106
+ // ../connect/dist/lib/codex.js
107
+ import fs2 from "node:fs";
108
+ import path2 from "node:path";
109
+ var TABLE = "mcp_servers.moneyswitch";
110
+ function tomlEscape(value) {
111
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
112
+ }
113
+ function buildMcpSection(server, key, mcpCommand) {
114
+ const argsToml = mcpCommand.args.map((a) => `"${tomlEscape(a)}"`).join(", ");
115
+ return [
116
+ `[${TABLE}]`,
117
+ `command = "${tomlEscape(mcpCommand.command)}"`,
118
+ `args = [${argsToml}]`,
119
+ "",
120
+ `[${TABLE}.env]`,
121
+ `MONEY_API_BASE = "${tomlEscape(server)}"`,
122
+ `MONEY_API_KEY = "${tomlEscape(key)}"`
123
+ ].join("\n");
124
+ }
125
+ function hasMoneySwitchSection(text) {
126
+ const headerRe = /^\[([^\]]+)\]$/;
127
+ return text.split(/\r?\n/).some((line) => {
128
+ const m = headerRe.exec(line.trim());
129
+ if (!m)
130
+ return false;
131
+ const name = m[1];
132
+ return name === TABLE || name.startsWith(`${TABLE}.`);
133
+ });
134
+ }
135
+ function removeMoneySwitchSection(text) {
136
+ const lines = text.split(/\r?\n/);
137
+ const headerRe = /^\[([^\]]+)\]$/;
138
+ const out = [];
139
+ let skipping = false;
140
+ for (const line of lines) {
141
+ const m = headerRe.exec(line.trim());
142
+ if (m) {
143
+ const name = m[1];
144
+ skipping = name === TABLE || name.startsWith(`${TABLE}.`);
145
+ }
146
+ if (!skipping)
147
+ out.push(line);
148
+ }
149
+ return out.join("\n").replace(/\n{3,}/g, "\n\n").replace(/^\n+/, "").replace(/\s+$/, "");
150
+ }
151
+ function applyCodexConfig(configPath, server, key, mcpCommand) {
152
+ fs2.mkdirSync(path2.dirname(configPath), { recursive: true });
153
+ const existed = fs2.existsSync(configPath);
154
+ const original = existed ? fs2.readFileSync(configPath, "utf8") : "";
155
+ let backupPath = null;
156
+ if (existed) {
157
+ backupPath = `${configPath}.bak-${Date.now()}`;
158
+ fs2.writeFileSync(backupPath, original, "utf8");
159
+ }
160
+ const withoutSection = removeMoneySwitchSection(original);
161
+ const section = buildMcpSection(server, key, mcpCommand);
162
+ const combined = withoutSection.length > 0 ? `${withoutSection}
163
+
164
+ ${section}
165
+ ` : `${section}
166
+ `;
167
+ fs2.writeFileSync(configPath, combined, "utf8");
168
+ return { configPath, existed, backupPath };
169
+ }
170
+ function removeCodexConfig(configPath) {
171
+ if (!fs2.existsSync(configPath)) {
172
+ return { configPath, removed: false, backupPath: null };
173
+ }
174
+ const original = fs2.readFileSync(configPath, "utf8");
175
+ if (!hasMoneySwitchSection(original)) {
176
+ return { configPath, removed: false, backupPath: null };
177
+ }
178
+ const stripped = removeMoneySwitchSection(original);
179
+ const backupPath = `${configPath}.bak-${Date.now()}`;
180
+ fs2.writeFileSync(backupPath, original, "utf8");
181
+ fs2.writeFileSync(configPath, stripped.length > 0 ? `${stripped}
182
+ ` : "", "utf8");
183
+ return { configPath, removed: true, backupPath };
184
+ }
185
+
186
+ // ../connect/dist/lib/mcp-entry.js
187
+ import { createRequire } from "node:module";
188
+ function resolveMcpCommand(fromUrl = import.meta.url) {
189
+ const require2 = createRequire(fromUrl);
190
+ try {
191
+ const entry = require2.resolve("@moneyswitch/mcp");
192
+ return { command: "node", args: [entry] };
193
+ } catch {
194
+ return { command: "npx", args: ["-y", "moneyswitch", "mcp"] };
195
+ }
196
+ }
197
+ function isNpmRegistryFallback(cmd) {
198
+ return cmd.command === "npx" && cmd.args.length === 3 && cmd.args[0] === "-y" && cmd.args[1] === "moneyswitch" && cmd.args[2] === "mcp";
199
+ }
200
+ async function resolvePortableMcpCommand(server, fetchImpl = fetch) {
201
+ const base = server.replace(/\/+$/, "");
202
+ const tarballUrl = `${base}/dl/moneyswitch.tgz`;
203
+ try {
204
+ const res = await fetchImpl(tarballUrl, { method: "HEAD" });
205
+ if (res.ok)
206
+ return { command: "npx", args: ["-y", `--package=${tarballUrl}`, "moneyswitch", "mcp"] };
207
+ } catch {
208
+ }
209
+ return { command: "npx", args: ["-y", "moneyswitch", "mcp"] };
210
+ }
211
+
212
+ // ../connect/dist/lib/runner.js
213
+ import { spawnSync } from "node:child_process";
214
+ function quoteWinArg(arg) {
215
+ if (arg.length > 0 && !/[\s"&|<>^%()!,;=]/.test(arg))
216
+ return arg;
217
+ return `"${arg.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, "$1$1")}"`;
218
+ }
219
+ var RealCommandRunner = class {
220
+ opts;
221
+ constructor(opts = {}) {
222
+ this.opts = opts;
223
+ }
224
+ run(cmd, args) {
225
+ const win = process.platform === "win32";
226
+ try {
227
+ const res = win ? spawnSync([cmd, ...args].map(quoteWinArg).join(" "), {
228
+ encoding: "utf8",
229
+ shell: true,
230
+ env: this.opts.env ?? process.env,
231
+ cwd: this.opts.cwd,
232
+ timeout: this.opts.timeoutMs
233
+ }) : spawnSync(cmd, args, {
234
+ encoding: "utf8",
235
+ env: this.opts.env ?? process.env,
236
+ cwd: this.opts.cwd,
237
+ timeout: this.opts.timeoutMs
238
+ });
239
+ if (res.error) {
240
+ return { ok: false, code: null, stdout: "", stderr: String(res.error.message ?? res.error) };
241
+ }
242
+ return {
243
+ ok: (res.status ?? 1) === 0,
244
+ code: res.status,
245
+ stdout: res.stdout ?? "",
246
+ stderr: res.stderr ?? ""
247
+ };
248
+ } catch (e) {
249
+ return { ok: false, code: null, stdout: "", stderr: e.message };
250
+ }
251
+ }
252
+ };
253
+
254
+ // ../connect/dist/lib/cli.js
255
+ function maskKey(key) {
256
+ if (key.length <= 12)
257
+ return `${key}\u2022\u2022\u2022\u2022`;
258
+ return `${key.slice(0, 12)}\u2022\u2022\u2022\u2022`;
259
+ }
260
+ var HELP_TEXT = `moneyswitch-connect - one-command local Agent setup for MoneySwitch (SPEC-v0.3-employee.md \xA7B)
261
+
262
+ Usage:
263
+ moneyswitch-connect --server <url> --key <mk_live_...> [--apply] [--json]
264
+ moneyswitch-connect status --server <url> --key <mk_live_...> [--json]
265
+ moneyswitch-connect remove [--apply] [--json]
266
+
267
+ Without --apply, changes are only listed (dry-run). Exit codes: 0 ok, 1 failure, 2 bad args.`;
268
+ async function runCli(argv, deps = {}) {
269
+ const stdout = deps.stdout ?? ((line) => process.stdout.write(line + "\n"));
270
+ const stderr = deps.stderr ?? ((line) => process.stderr.write(line + "\n"));
271
+ const env = deps.env ?? process.env;
272
+ const runner = deps.runner ?? new RealCommandRunner();
273
+ const fetchImpl = deps.fetchImpl;
274
+ const isTTY = deps.isTTY ?? Boolean(process.stdout.isTTY);
275
+ const jsonMode = argv.includes("--json") || !isTTY;
276
+ const parsed = parseArgs(argv);
277
+ if ("error" in parsed) {
278
+ if (jsonMode)
279
+ stdout(JSON.stringify({ ok: false, error: parsed.error }));
280
+ else
281
+ stderr(`error: ${parsed.error}
282
+
283
+ ${HELP_TEXT}`);
284
+ return 2;
285
+ }
286
+ if (parsed.help) {
287
+ if (jsonMode)
288
+ stdout(JSON.stringify({ ok: true, help: HELP_TEXT }));
289
+ else
290
+ stdout(HELP_TEXT);
291
+ return 0;
292
+ }
293
+ let mcpCommand = deps.mcpCommand ?? resolveMcpCommand();
294
+ if (parsed.command === "status") {
295
+ const server2 = parsed.server;
296
+ const key2 = parsed.key;
297
+ let result;
298
+ try {
299
+ result = await fetchStatus(server2, key2, fetchImpl);
300
+ } catch (e) {
301
+ const msg = e.message;
302
+ if (jsonMode)
303
+ stdout(JSON.stringify({ ok: false, error: "REQUEST_FAILED", message: msg }));
304
+ else
305
+ stderr(`failed to reach ${server2}: ${msg}`);
306
+ return 1;
307
+ }
308
+ if (!result.ok) {
309
+ if (jsonMode)
310
+ stdout(JSON.stringify({ ok: false, http_status: result.httpStatus, body: result.body }));
311
+ else
312
+ stderr(`GET /v1/status -> ${result.httpStatus}: ${JSON.stringify(result.body)}`);
313
+ return 1;
314
+ }
315
+ if (jsonMode)
316
+ stdout(JSON.stringify({ ok: true, http_status: result.httpStatus, status: result.body }));
317
+ else {
318
+ const body = result.body;
319
+ stdout(`Key: ${body.key_name ?? "(unnamed)"} (${body.key_prefix ?? maskKey(key2)})`);
320
+ stdout(`Remaining today: ${body.remaining_today} / ${body.daily_budget} ${body.currency}`);
321
+ stdout(`Remaining total: ${body.remaining_total} / ${body.total_budget} ${body.currency}`);
322
+ stdout(`Network: ${body.network}`);
323
+ }
324
+ return 0;
325
+ }
326
+ if (parsed.command === "remove") {
327
+ const agents2 = [];
328
+ const claudePresent2 = detectClaude(runner);
329
+ if (parsed.apply) {
330
+ const res = removeClaude(runner);
331
+ agents2.push({ name: "claude", detected: claudePresent2, action: res.ok ? "removed" : "remove_failed", detail: res });
332
+ } else {
333
+ agents2.push({ name: "claude", detected: claudePresent2, action: claudePresent2 ? "would_remove" : "skip" });
334
+ }
335
+ const cfgPath2 = codexConfigPath(env);
336
+ if (parsed.apply) {
337
+ const res = removeCodexConfig(cfgPath2);
338
+ agents2.push({ name: "codex", detected: res.removed, action: res.removed ? "removed" : "skip", detail: res });
339
+ } else {
340
+ agents2.push({ name: "codex", detected: null, action: "would_remove", detail: { configPath: cfgPath2 } });
341
+ }
342
+ if (jsonMode) {
343
+ stdout(JSON.stringify({ ok: true, applied: parsed.apply, agents: agents2 }));
344
+ } else {
345
+ stdout(parsed.apply ? "Removed MoneySwitch MCP configuration:" : "Would remove MoneySwitch MCP configuration (dry-run, pass --apply):");
346
+ for (const a of agents2)
347
+ stdout(` - ${a.name}: ${a.action}`);
348
+ }
349
+ return 0;
350
+ }
351
+ const server = parsed.server;
352
+ const key = parsed.key;
353
+ let statusResult;
354
+ try {
355
+ statusResult = await fetchStatus(server, key, fetchImpl);
356
+ } catch (e) {
357
+ const msg = e.message;
358
+ if (jsonMode)
359
+ stdout(JSON.stringify({ ok: false, error: "REQUEST_FAILED", message: msg }));
360
+ else
361
+ stderr(`failed to reach ${server}: ${msg}`);
362
+ return 1;
363
+ }
364
+ if (!statusResult.ok) {
365
+ if (jsonMode)
366
+ stdout(JSON.stringify({ ok: false, http_status: statusResult.httpStatus, body: statusResult.body }));
367
+ else
368
+ stderr(`key rejected by ${server}: HTTP ${statusResult.httpStatus}: ${JSON.stringify(statusResult.body)}`);
369
+ return 1;
370
+ }
371
+ const statusBody = statusResult.body;
372
+ if (!deps.mcpCommand && isNpmRegistryFallback(mcpCommand)) {
373
+ mcpCommand = await resolvePortableMcpCommand(server, fetchImpl);
374
+ }
375
+ if (!jsonMode) {
376
+ stdout(`Key: ${statusBody.key_name ?? "(unnamed)"} (${statusBody.key_prefix ?? maskKey(key)})`);
377
+ stdout(`Remaining today: ${statusBody.remaining_today} ${statusBody.currency}`);
378
+ stdout(`Network: ${statusBody.network}`);
379
+ }
380
+ const claudePresent = detectClaude(runner);
381
+ const codexPresent = detectCodex(runner, env);
382
+ const cfgPath = codexConfigPath(env);
383
+ const agents = [];
384
+ if (parsed.apply) {
385
+ if (claudePresent) {
386
+ const res = applyClaude(runner, server, key, mcpCommand);
387
+ agents.push({
388
+ name: "claude",
389
+ detected: true,
390
+ action: res.added.ok ? "added" : "add_failed",
391
+ detail: res
392
+ });
393
+ } else {
394
+ agents.push({ name: "claude", detected: false, action: "not_found" });
395
+ }
396
+ if (codexPresent) {
397
+ const res = applyCodexConfig(cfgPath, server, key, mcpCommand);
398
+ agents.push({ name: "codex", detected: true, action: "added", detail: res });
399
+ } else {
400
+ agents.push({ name: "codex", detected: false, action: "not_found" });
401
+ }
402
+ } else {
403
+ const mcpCommandStr = [mcpCommand.command, ...mcpCommand.args].join(" ");
404
+ agents.push({
405
+ name: "claude",
406
+ detected: claudePresent,
407
+ action: claudePresent ? `would run: claude mcp add moneyswitch -s user -e MONEY_API_BASE=${server} -e MONEY_API_KEY=**** -- ${mcpCommandStr}` : "not_found"
408
+ });
409
+ agents.push({
410
+ name: "codex",
411
+ detected: codexPresent,
412
+ action: codexPresent ? `would write [mcp_servers.moneyswitch] to ${cfgPath} (backing up existing file)` : "not_found"
413
+ });
414
+ }
415
+ agents.push({
416
+ name: "cherry_studio",
417
+ detected: null,
418
+ action: `manual: set Base URL = ${server}/v1, API Key = <your key>`
419
+ });
420
+ agents.push({
421
+ name: "open_webui",
422
+ detected: null,
423
+ action: `manual: set Base URL = ${server}/v1, API Key = <your key>`
424
+ });
425
+ const revoke = "moneyswitch-connect remove --apply";
426
+ if (jsonMode) {
427
+ stdout(JSON.stringify({
428
+ ok: true,
429
+ applied: parsed.apply,
430
+ server,
431
+ key_prefix: statusBody.key_prefix ?? maskKey(key),
432
+ status: statusBody,
433
+ agents,
434
+ revoke
435
+ }));
436
+ } else {
437
+ stdout(parsed.apply ? "\nApplied:" : "\nDry-run (pass --apply to make these changes):");
438
+ for (const a of agents)
439
+ stdout(` - ${a.name}: ${a.action}`);
440
+ stdout(`
441
+ To revoke later: ${revoke}`);
442
+ }
443
+ const failed = agents.some((a) => a.action === "add_failed");
444
+ return failed ? 1 : 0;
445
+ }
446
+
447
+ // src/cli.ts
448
+ import { pathToFileURL } from "node:url";
449
+ var TOP_HELP = `moneyswitch - client CLI for MoneySwitch (x402 + USDC on Monad)
450
+
451
+ Usage:
452
+ moneyswitch connect --server <url> --key <mk_live_...> [--apply] [--json]
453
+ moneyswitch status --server <url> --key <mk_live_...> [--json]
454
+ moneyswitch remove [--apply] [--json]
455
+ moneyswitch ui [--port 4318] [--no-open]
456
+ moneyswitch sell --upstream <url> --pay-to <0x\u2026> [--price 0.01] [--route "POST /path=0.01"]...
457
+ moneyswitch mcp
458
+
459
+ "sell" puts a toll booth in front of your own API: AI agents pay USDC per call
460
+ (x402) straight to your PUBLIC receiving address; no MoneySwitch server needed.
461
+ Run "moneyswitch sell --help" for all options.
462
+ "ui" opens the local desktop console (127.0.0.1 only): give each agent a
463
+ model key and a MoneyKey, preview the config diff, then enable.
464
+ Without --apply, "connect"/"remove" only print planned changes (dry-run).
465
+ "mcp" starts a stdio MCP server; it reads MONEY_API_BASE and MONEY_API_KEY
466
+ from the environment and never touches this process's argv/stdout for
467
+ anything other than the MCP protocol itself.
468
+
469
+ Exit codes: 0 ok, 1 failure, 2 bad args.`;
470
+ function parseTopArgv(argv) {
471
+ const [sub, ...rest] = argv;
472
+ if (sub === "mcp") return { kind: "mcp" };
473
+ if (sub === "ui") return { kind: "ui", args: rest };
474
+ if (sub === "sell") return { kind: "sell", args: rest };
475
+ if (sub === "connect") return { kind: "connect-lib", args: rest };
476
+ if (sub === "status" || sub === "remove") return { kind: "connect-lib", args: argv };
477
+ if (sub === void 0 || sub === "--help" || sub === "-h") return { kind: "help" };
478
+ return { kind: "unknown", command: sub };
479
+ }
480
+ async function main() {
481
+ const dispatch = parseTopArgv(process.argv.slice(2));
482
+ if (dispatch.kind === "mcp") {
483
+ await import("./mcp.js");
484
+ return;
485
+ }
486
+ if (dispatch.kind === "ui") {
487
+ const { runUi } = await import("./desktop.js");
488
+ process.exit(await runUi(dispatch.args));
489
+ return;
490
+ }
491
+ if (dispatch.kind === "sell") {
492
+ const { runSell } = await import("./sell.js");
493
+ process.exit(await runSell(dispatch.args));
494
+ return;
495
+ }
496
+ if (dispatch.kind === "connect-lib") {
497
+ const code = await runCli(dispatch.args);
498
+ process.exit(code);
499
+ return;
500
+ }
501
+ if (dispatch.kind === "help") {
502
+ process.stdout.write(TOP_HELP + "\n");
503
+ process.exit(0);
504
+ return;
505
+ }
506
+ process.stderr.write(`moneyswitch: unknown command "${dispatch.command}"
507
+
508
+ ${TOP_HELP}
509
+ `);
510
+ process.exit(2);
511
+ }
512
+ var isMain = Boolean(process.argv[1]) && import.meta.url === pathToFileURL(process.argv[1]).href;
513
+ if (isMain) {
514
+ main().catch((err) => {
515
+ process.stderr.write(`moneyswitch: unexpected error: ${err?.message ?? err}
516
+ `);
517
+ process.exit(1);
518
+ });
519
+ }
520
+ export {
521
+ TOP_HELP,
522
+ parseTopArgv
523
+ };