@fluxa-pay/planner 0.1.0 → 0.2.1
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/package.json +2 -2
- package/planner.mjs +204 -8
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fluxa-pay/planner",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "FluxA Planner CLI
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "FluxA Planner CLI. Recommend the right models, APIs and skills for a task, run paid calls via x402, and manage your oneshot APIs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"planner": "planner.mjs"
|
package/planner.mjs
CHANGED
|
@@ -301,6 +301,39 @@ async function rawCall(url, method, data, extraHeaders = {}, auth = false) {
|
|
|
301
301
|
return { status: res.status, text };
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
+
// The proxy's API/MCP lanes bill the caller identified by the X-Agent-ID header
|
|
305
|
+
// and reject every non-discovery request without it. Default to the agent_id
|
|
306
|
+
// (UUID) from `fluxa-wallet status`; configs with require_agent_auth need a JWT
|
|
307
|
+
// instead, so agentCall() retries with a freshly minted wallet JWT on the
|
|
308
|
+
// "valid JWT" 401. Both values are cached for the process.
|
|
309
|
+
let _agentId = null;
|
|
310
|
+
async function agentId() {
|
|
311
|
+
if (_agentId) return _agentId;
|
|
312
|
+
const s = unwrap(await walletJson(["status"]).catch(() => ({})));
|
|
313
|
+
const id = s.agent_id || s.agentId || s.id;
|
|
314
|
+
if (!id) die("could not read agent_id from `fluxa-wallet status` — register once with `fluxa-wallet init`.");
|
|
315
|
+
return (_agentId = id);
|
|
316
|
+
}
|
|
317
|
+
let _walletJwt = null;
|
|
318
|
+
async function walletJwt() {
|
|
319
|
+
if (_walletJwt) return _walletJwt;
|
|
320
|
+
const j = unwrap(await walletJson(["refreshJWT"]).catch(() => ({})));
|
|
321
|
+
const jwt = j.jwt || j.token;
|
|
322
|
+
if (!jwt) die("could not mint a wallet JWT (`fluxa-wallet refreshJWT`).");
|
|
323
|
+
return (_walletJwt = jwt);
|
|
324
|
+
}
|
|
325
|
+
// rawCall + X-Agent-ID, with the UUID→JWT fallback for require_agent_auth configs.
|
|
326
|
+
// Auto-attached headers go FIRST so a caller-supplied `extra` (e.g. a user's
|
|
327
|
+
// `--header "X-Agent-ID: …"`) can override — collisions are surfaced upstream
|
|
328
|
+
// in cmdCall so the override isn't silent.
|
|
329
|
+
async function agentCall(url, method, data, extra = {}) {
|
|
330
|
+
const r = await rawCall(url, method, data, { "X-Agent-ID": await agentId(), ...extra });
|
|
331
|
+
if (r.status === 401 && /valid JWT/i.test(r.text)) {
|
|
332
|
+
return rawCall(url, method, data, { "X-Agent-ID": await walletJwt(), ...extra });
|
|
333
|
+
}
|
|
334
|
+
return r;
|
|
335
|
+
}
|
|
336
|
+
|
|
304
337
|
async function ensureMandate(currency, budget, seconds, url) {
|
|
305
338
|
const cfg = loadConfig();
|
|
306
339
|
// only reuse a signed mandate of the SAME currency with budget left
|
|
@@ -339,14 +372,30 @@ function descriptorEndpoints(text) {
|
|
|
339
372
|
}
|
|
340
373
|
|
|
341
374
|
async function cmdCall(url, opts = {}) {
|
|
342
|
-
if (!url
|
|
343
|
-
return die("usage: planner call <url> [--endpoint <path>] [--data '<json>'] [--pay credits|usdc] [--method GET|POST] [--budget <units>] [--raw]");
|
|
375
|
+
if (!url) {
|
|
376
|
+
return die("usage: planner call <url|slug> [--endpoint <path>] [--data '<json>'] [--header 'K: V']… [--pay credits|usdc] [--method GET|POST] [--budget <units>] [--raw]");
|
|
344
377
|
}
|
|
378
|
+
// accept a bare AgentMarket slug (e.g. `web-search-api`) → the proxy API base.
|
|
379
|
+
// full http(s) URLs are used as-is.
|
|
380
|
+
if (!/^https?:\/\//.test(url)) url = `${PROXY}/api/${encodeURIComponent(url)}`;
|
|
345
381
|
const pay = (opts.pay || loadConfig().pay || "credits").toLowerCase();
|
|
346
382
|
const currency = pay === "usdc" ? "USDC" : "FLUXA_MONETIZE_CREDITS";
|
|
347
383
|
const budget = opts.budget ? Number(opts.budget) : (pay === "usdc" ? 5000000 : 500); // ~$5 default
|
|
348
384
|
const seconds = opts.seconds ? Number(opts.seconds) : 28800; // 8h
|
|
349
385
|
|
|
386
|
+
// user-supplied request headers (repeatable --header "K: V") are merged
|
|
387
|
+
// into every outbound call below. A user value wins over the auto-attached
|
|
388
|
+
// X-Agent-ID; X-Payment is the one exception, see the retry step. We warn
|
|
389
|
+
// up front when a user header would override an auto-attached one so the
|
|
390
|
+
// override isn't silent.
|
|
391
|
+
const userHeaders = opts.headers || {};
|
|
392
|
+
const reservedNames = ["x-agent-id", "x-payment"];
|
|
393
|
+
for (const k of Object.keys(userHeaders)) {
|
|
394
|
+
if (reservedNames.includes(k.toLowerCase())) {
|
|
395
|
+
console.error(c.dim(`! --header ${k} overrides the auto-attached value`));
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
350
399
|
await ensureAgent();
|
|
351
400
|
|
|
352
401
|
let targetUrl = url;
|
|
@@ -357,7 +406,7 @@ async function cmdCall(url, opts = {}) {
|
|
|
357
406
|
// real paid sub-endpoints. Resolve it (unless --raw) so the agent doesn't have
|
|
358
407
|
// to read the catalog itself. We never spend without a supplied body.
|
|
359
408
|
if (!opts.raw) {
|
|
360
|
-
const probe = await
|
|
409
|
+
const probe = await agentCall(url, "GET", undefined, userHeaders);
|
|
361
410
|
const desc = probe.status === 200 ? descriptorEndpoints(probe.text) : null;
|
|
362
411
|
if (desc) {
|
|
363
412
|
const eps = desc.endpoints.filter((e) => e.enabled !== false);
|
|
@@ -388,7 +437,7 @@ async function cmdCall(url, opts = {}) {
|
|
|
388
437
|
}
|
|
389
438
|
|
|
390
439
|
// call the resolved endpoint — pay if it answers 402
|
|
391
|
-
let r = await
|
|
440
|
+
let r = await agentCall(targetUrl, method, data, userHeaders);
|
|
392
441
|
if (r.status !== 402) {
|
|
393
442
|
if (r.status >= 400) die(`${r.status} — ${r.text.slice(0, 300)}`);
|
|
394
443
|
return console.log(r.text); // raw body, no post-processing
|
|
@@ -402,8 +451,11 @@ async function cmdCall(url, opts = {}) {
|
|
|
402
451
|
const xPayment = paid.xPaymentB64 || paid.xPayment;
|
|
403
452
|
if (!xPayment) die("x402-v3 did not return a payment token (xPaymentB64).");
|
|
404
453
|
|
|
405
|
-
// 3. retry with the payment token
|
|
406
|
-
|
|
454
|
+
// 3. retry with the payment token (against the resolved endpoint, with
|
|
455
|
+
// X-Agent-ID + user headers). The freshly-minted X-Payment is appended
|
|
456
|
+
// LAST so it wins over any user-supplied X-Payment — preserving the x402
|
|
457
|
+
// settlement that the CLI just signed for this exact 402 challenge.
|
|
458
|
+
r = await agentCall(targetUrl, method, opts.data, { ...userHeaders, "X-Payment": xPayment });
|
|
407
459
|
if (r.status >= 400) die(`paid retry failed: ${r.status} — ${r.text.slice(0, 300)}`);
|
|
408
460
|
console.log(r.text); // raw body, no post-processing
|
|
409
461
|
}
|
|
@@ -446,6 +498,109 @@ async function cmdTopup(vendor, opts = {}) {
|
|
|
446
498
|
console.log(c.green("✓") + ` topped up ${vendor} · +${added.toLocaleString()} Units · balance ${Number(res.balance ?? 0).toLocaleString()} Units`);
|
|
447
499
|
}
|
|
448
500
|
|
|
501
|
+
// --- api (manage your oneshot API configs) ----------------------------------
|
|
502
|
+
// `planner api <verb>` — resource-first, like `planner keys`. All verbs auth
|
|
503
|
+
// with auth:true (fxa_live_ key if present, else an auto-minted Agent VC) and
|
|
504
|
+
// are owner-scoped server-side.
|
|
505
|
+
function readSpec(file) {
|
|
506
|
+
let raw;
|
|
507
|
+
try {
|
|
508
|
+
raw = file ? readFileSync(file, "utf8") : readFileSync(0, "utf8");
|
|
509
|
+
} catch (e) {
|
|
510
|
+
return die(`could not read spec ${file ? file : "from stdin"}: ${e.message}`);
|
|
511
|
+
}
|
|
512
|
+
try {
|
|
513
|
+
return JSON.parse(raw);
|
|
514
|
+
} catch (e) {
|
|
515
|
+
return die(`spec is not valid JSON${file ? ` (${file})` : " (stdin)"}: ${e.message}`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Overlay --name/--upstream/--wallet/--private/--require-agent-auth onto a spec.
|
|
520
|
+
function applyOverrides(spec, opts) {
|
|
521
|
+
if (opts.name != null) spec.serverName = opts.name;
|
|
522
|
+
if (opts.upstream != null) spec.upstreamBaseUrl = opts.upstream;
|
|
523
|
+
if (opts.wallet != null) spec.yourWallet = opts.wallet;
|
|
524
|
+
if (opts.private) spec.isPrivate = true;
|
|
525
|
+
if (opts.requireAgentAuth) spec.requireAgentAuth = true;
|
|
526
|
+
return spec;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async function cmdApi(sub, rest, opts) {
|
|
530
|
+
switch (sub) {
|
|
531
|
+
case "create": case "new": return cmdApiCreate(opts);
|
|
532
|
+
case "list": case "ls": return cmdApiList();
|
|
533
|
+
case "get": case "show": return cmdApiGet(rest[0]);
|
|
534
|
+
case "edit": case "update": return cmdApiEdit(rest[0], opts);
|
|
535
|
+
default:
|
|
536
|
+
return die(
|
|
537
|
+
"usage: planner api <create|list|get|edit>\n" +
|
|
538
|
+
" api create --file <spec.json> [--publish] [--dry-run]\n" +
|
|
539
|
+
" api list\n" +
|
|
540
|
+
" api get <idOrSlug>\n" +
|
|
541
|
+
" api edit <idOrSlug> [--file <spec.json>] [--publish] [--dry-run]",
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
async function cmdApiCreate(opts) {
|
|
547
|
+
const spec = applyOverrides(readSpec(opts.file), opts);
|
|
548
|
+
spec.status = opts.publish ? "published" : "draft";
|
|
549
|
+
|
|
550
|
+
if (opts.dryRun) {
|
|
551
|
+
console.error(c.dim(`· dry-run — would POST ${spec.status} config to ${PROXY}/configs/api:`));
|
|
552
|
+
console.log(JSON.stringify(spec, null, 2));
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const { data } = await http(`${PROXY}/configs/api`, { method: "POST", auth: true, body: spec });
|
|
557
|
+
const tag = data.status === "published" ? c.green("published") : c.dim("draft");
|
|
558
|
+
console.log(c.green("✓") + ` created ${c.bold(data.slug || data.configId)} · ${tag}`);
|
|
559
|
+
if (data.proxyUrl) console.log(c.dim(` proxy: ${data.proxyUrl}`));
|
|
560
|
+
console.log(c.dim(
|
|
561
|
+
data.status === "published"
|
|
562
|
+
? ` test it: planner call ${data.proxyUrl || data.slug}`
|
|
563
|
+
: ` publish: planner api edit ${data.slug || data.configId} --publish`,
|
|
564
|
+
));
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
async function cmdApiList() {
|
|
568
|
+
const { data } = await http(`${PROXY}/configs/api`, { auth: true });
|
|
569
|
+
const rows = data.configs || [];
|
|
570
|
+
if (!rows.length) return console.log(c.dim(" no api configs yet — `planner api create --file <spec.json>`"));
|
|
571
|
+
console.log(" " + c.dim(pad("slug", 26) + pad("name", 22) + pad("status", 10) + pad("vis", 9) + pad("eps", 5) + "created"));
|
|
572
|
+
for (const r of rows) {
|
|
573
|
+
const st = r.status === "published" ? c.green(pad("published", 10)) : c.dim(pad("draft", 10));
|
|
574
|
+
const vis = r.isPrivate ? "private" : "public";
|
|
575
|
+
console.log(` ${pad(r.slug || r.configId, 26)}${pad(r.serverName || "", 22)}${st}${pad(vis, 9)}${pad(String(r.endpointCount ?? 0), 5)}${(r.createdAt || "").slice(0, 10)}`);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
async function cmdApiGet(idOrSlug) {
|
|
580
|
+
if (!idOrSlug) return die("usage: planner api get <idOrSlug>");
|
|
581
|
+
const { data } = await http(`${PROXY}/configs/api/${encodeURIComponent(idOrSlug)}`, { auth: true });
|
|
582
|
+
console.log(JSON.stringify(data, null, 2));
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
async function cmdApiEdit(idOrSlug, opts) {
|
|
586
|
+
if (!idOrSlug) return die("usage: planner api edit <idOrSlug> [--file <spec.json>] [--publish] [--dry-run]");
|
|
587
|
+
const spec = applyOverrides(opts.file ? readSpec(opts.file) : {}, opts);
|
|
588
|
+
if (opts.publish) spec.status = "published";
|
|
589
|
+
if (Object.keys(spec).length === 0) {
|
|
590
|
+
return die("nothing to edit — pass --file and/or flags (--name, --upstream, --publish, …)");
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (opts.dryRun) {
|
|
594
|
+
console.error(c.dim(`· dry-run — would PATCH ${PROXY}/configs/api/${idOrSlug}:`));
|
|
595
|
+
console.log(JSON.stringify(spec, null, 2));
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const { data } = await http(`${PROXY}/configs/api/${encodeURIComponent(idOrSlug)}`, { method: "PATCH", auth: true, body: spec });
|
|
600
|
+
const tag = data.status === "published" ? c.green("published") : c.dim("draft");
|
|
601
|
+
console.log(c.green("✓") + ` updated ${c.bold(data.slug || data.configId)} · ${tag}`);
|
|
602
|
+
}
|
|
603
|
+
|
|
449
604
|
// --- plan (thin call to server-side endpoint) --------------------------------
|
|
450
605
|
async function cmdPlan(task) {
|
|
451
606
|
if (!task) return die('usage: planner plan "<task>"');
|
|
@@ -569,6 +724,13 @@ ${c.bold("Oneshot APIs")}
|
|
|
569
724
|
${c.cyan("planner call <base>")} show the paid endpoint + its required body
|
|
570
725
|
${c.cyan("planner call <base> --data '<json>'")} call it; pay the 402 if needed
|
|
571
726
|
${c.dim("--endpoint <path>")} picks when there are several; ${c.dim("--raw")} hits a URL directly.
|
|
727
|
+
${c.bold("Manage your APIs")} ${c.dim("(fxa_live_ key or auto-minted Agent VC)")}
|
|
728
|
+
${c.cyan("planner api create --file <spec.json> [--publish]")} create (draft unless --publish)
|
|
729
|
+
${c.cyan("planner api list")} your api configs
|
|
730
|
+
${c.cyan("planner api get <idOrSlug>")} print the round-trippable spec
|
|
731
|
+
${c.cyan("planner api edit <idOrSlug> [--file <s>] [--publish]")} update fields / replace endpoints
|
|
732
|
+
${c.dim("Spec JSON: serverName, serverDescription, upstreamBaseUrl, endpoints[]. Wallet defaults from your account.")}
|
|
733
|
+
${c.dim("get emits a spec you can edit and feed back: planner api get foo > s.json; planner api edit foo --file s.json")}
|
|
572
734
|
`,
|
|
573
735
|
models: () => `
|
|
574
736
|
${c.bold("Models — merchant-centric")}
|
|
@@ -603,6 +765,7 @@ ${c.bold("Headline")}
|
|
|
603
765
|
planner plan "<task>" recommend the models, APIs & skills for a task
|
|
604
766
|
planner call <url> make a paid API call — handles the x402 mandate + payment
|
|
605
767
|
[--pay credits|usdc] [--method GET|POST] [--data '<json>']
|
|
768
|
+
[--header "Name: Value"] (repeatable; e.g. an upstream Bearer token)
|
|
606
769
|
|
|
607
770
|
${c.bold("Primitives")} ${c.dim("(all live)")}
|
|
608
771
|
planner search "<q>" [--type api,skill,model] discover resources
|
|
@@ -611,6 +774,8 @@ ${c.bold("Primitives")} ${c.dim("(all live)")}
|
|
|
611
774
|
planner topup <vendor> [--credits <N>|--bundle <slug>] prepay Units (x402, via wallet)
|
|
612
775
|
planner ledger <vendor> spend/topup history
|
|
613
776
|
planner keys [create|update <id>|revoke <id>] manage fxa_live_ API keys (Agent VC only)
|
|
777
|
+
planner api create --file <spec.json> [--publish] create a oneshot API (draft by default)
|
|
778
|
+
planner api list | get <idOrSlug> | edit <idOrSlug> list / inspect / update your APIs
|
|
614
779
|
planner login <fxa_live_…> / planner whoami auth
|
|
615
780
|
planner info [units|auth|pay|keys|apis|models|skills] explain how the platform works
|
|
616
781
|
|
|
@@ -627,9 +792,31 @@ function die(msg, ctx) {
|
|
|
627
792
|
async function main() {
|
|
628
793
|
const args = argv.slice(2);
|
|
629
794
|
const cmd = args[0];
|
|
630
|
-
const FLAGS = ["--type", "--vendor", "--pay", "--method", "--data", "--budget", "--seconds", "--endpoint", "--credits", "--bundle", "--name", "--cap"];
|
|
631
|
-
const BOOLS = ["--raw"];
|
|
795
|
+
const FLAGS = ["--type", "--vendor", "--pay", "--method", "--data", "--budget", "--seconds", "--endpoint", "--credits", "--bundle", "--name", "--cap", "--file", "--upstream", "--wallet", "--header"];
|
|
796
|
+
const BOOLS = ["--raw", "--publish", "--private", "--require-agent-auth", "--dry-run"];
|
|
632
797
|
const getFlag = (n) => { const i = args.indexOf(n); return i !== -1 ? args[i + 1] : null; };
|
|
798
|
+
// repeatable flag → array of all values (in order)
|
|
799
|
+
const getFlagAll = (n) => {
|
|
800
|
+
const out = [];
|
|
801
|
+
for (let i = 0; i < args.length - 1; i++) if (args[i] === n) out.push(args[i + 1]);
|
|
802
|
+
return out;
|
|
803
|
+
};
|
|
804
|
+
// parse `--header "Name: Value"` (repeatable) into a headers object. Splits
|
|
805
|
+
// on the FIRST colon so JWTs/URLs in the value are preserved. Malformed
|
|
806
|
+
// entries are warned and skipped, never thrown — a broken header shouldn't
|
|
807
|
+
// poison the whole call.
|
|
808
|
+
const parseHeaders = (specs) => {
|
|
809
|
+
const out = {};
|
|
810
|
+
for (const s of specs) {
|
|
811
|
+
const i = s.indexOf(":");
|
|
812
|
+
if (i <= 0) { console.error(c.dim(`! ignoring malformed --header "${s}" (expected "Name: Value")`)); continue; }
|
|
813
|
+
const name = s.slice(0, i).trim();
|
|
814
|
+
const value = s.slice(i + 1).trim();
|
|
815
|
+
if (!name) continue;
|
|
816
|
+
out[name] = value;
|
|
817
|
+
}
|
|
818
|
+
return out;
|
|
819
|
+
};
|
|
633
820
|
const type = getFlag("--type");
|
|
634
821
|
const vendor = getFlag("--vendor");
|
|
635
822
|
const rest = [];
|
|
@@ -647,6 +834,7 @@ async function main() {
|
|
|
647
834
|
pay: getFlag("--pay"), method: getFlag("--method"), data: getFlag("--data"),
|
|
648
835
|
budget: getFlag("--budget"), seconds: getFlag("--seconds"),
|
|
649
836
|
endpoint: getFlag("--endpoint"), raw: args.includes("--raw"),
|
|
837
|
+
headers: parseHeaders(getFlagAll("--header")),
|
|
650
838
|
});
|
|
651
839
|
case "search": return cmdSearch(arg, type);
|
|
652
840
|
case "models": return cmdModels(vendor);
|
|
@@ -658,6 +846,14 @@ async function main() {
|
|
|
658
846
|
});
|
|
659
847
|
case "ledger": return cmdLedger(rest[0]);
|
|
660
848
|
case "keys": return cmdKeys(rest[0], rest.slice(1), { name: getFlag("--name"), cap: getFlag("--cap") });
|
|
849
|
+
case "api":
|
|
850
|
+
return cmdApi(rest[0], rest.slice(1), {
|
|
851
|
+
file: getFlag("--file"), name: getFlag("--name"),
|
|
852
|
+
upstream: getFlag("--upstream"), wallet: getFlag("--wallet"),
|
|
853
|
+
publish: args.includes("--publish"), private: args.includes("--private"),
|
|
854
|
+
requireAgentAuth: args.includes("--require-agent-auth"),
|
|
855
|
+
dryRun: args.includes("--dry-run"),
|
|
856
|
+
});
|
|
661
857
|
case "login": return cmdLogin(rest[0]);
|
|
662
858
|
case "whoami": return cmdWhoami();
|
|
663
859
|
case "info": return cmdInfo(rest[0]);
|