@fluxa-pay/planner 0.1.1 → 0.2.2

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.
Files changed (2) hide show
  1. package/package.json +2 -2
  2. package/planner.mjs +186 -9
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fluxa-pay/planner",
3
- "version": "0.1.1",
4
- "description": "FluxA Planner CLI recommend the right models/APIs/skills for a task, and run paid calls via x402.",
3
+ "version": "0.2.2",
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
@@ -181,6 +181,18 @@ async function cmdModels(vendor) {
181
181
  }
182
182
  }
183
183
 
184
+ async function cmdVendors() {
185
+ const { data } = await http(`${PLATFORM}/api/llm/vendors`);
186
+ const vendors = data.vendors || [];
187
+ if (!vendors.length) return console.log(c.dim(" no vendors"));
188
+ console.log(" " + c.dim(pad("slug", 22) + pad("name", 22) + pad("models", 9) + pad("bundles", 9) + "price (Units/Mtok)"));
189
+ for (const v of vendors) {
190
+ const pr = v.priceRange;
191
+ const price = pr && Number.isFinite(pr.min) ? (pr.min === pr.max ? `${pr.min}` : `${pr.min}-${pr.max}`) : c.dim("—");
192
+ console.log(` ${c.bold(pad(v.slug, 22))}${c.gray(pad(v.name || "", 22))}${pad(String(v.modelCount ?? 0), 9)}${pad(String(v.bundleCount ?? 0), 9)}${price}`);
193
+ }
194
+ }
195
+
184
196
  async function cmdBalance(vendor) {
185
197
  const url = vendor ? `${PROXY}/llm/wallet/balances/${vendor}` : `${PROXY}/llm/wallet/balances`;
186
198
  const { data } = await http(url, { auth: true });
@@ -323,10 +335,13 @@ async function walletJwt() {
323
335
  return (_walletJwt = jwt);
324
336
  }
325
337
  // rawCall + X-Agent-ID, with the UUID→JWT fallback for require_agent_auth configs.
338
+ // Auto-attached headers go FIRST so a caller-supplied `extra` (e.g. a user's
339
+ // `--header "X-Agent-ID: …"`) can override — collisions are surfaced upstream
340
+ // in cmdCall so the override isn't silent.
326
341
  async function agentCall(url, method, data, extra = {}) {
327
- const r = await rawCall(url, method, data, { ...extra, "X-Agent-ID": await agentId() });
342
+ const r = await rawCall(url, method, data, { "X-Agent-ID": await agentId(), ...extra });
328
343
  if (r.status === 401 && /valid JWT/i.test(r.text)) {
329
- return rawCall(url, method, data, { ...extra, "X-Agent-ID": await walletJwt() });
344
+ return rawCall(url, method, data, { "X-Agent-ID": await walletJwt(), ...extra });
330
345
  }
331
346
  return r;
332
347
  }
@@ -370,7 +385,7 @@ function descriptorEndpoints(text) {
370
385
 
371
386
  async function cmdCall(url, opts = {}) {
372
387
  if (!url) {
373
- return die("usage: planner call <url|slug> [--endpoint <path>] [--data '<json>'] [--pay credits|usdc] [--method GET|POST] [--budget <units>] [--raw]");
388
+ return die("usage: planner call <url|slug> [--endpoint <path>] [--data '<json>'] [--header 'K: V']… [--pay credits|usdc] [--method GET|POST] [--budget <units>] [--raw]");
374
389
  }
375
390
  // accept a bare AgentMarket slug (e.g. `web-search-api`) → the proxy API base.
376
391
  // full http(s) URLs are used as-is.
@@ -380,6 +395,19 @@ async function cmdCall(url, opts = {}) {
380
395
  const budget = opts.budget ? Number(opts.budget) : (pay === "usdc" ? 5000000 : 500); // ~$5 default
381
396
  const seconds = opts.seconds ? Number(opts.seconds) : 28800; // 8h
382
397
 
398
+ // user-supplied request headers (repeatable --header "K: V") are merged
399
+ // into every outbound call below. A user value wins over the auto-attached
400
+ // X-Agent-ID; X-Payment is the one exception, see the retry step. We warn
401
+ // up front when a user header would override an auto-attached one so the
402
+ // override isn't silent.
403
+ const userHeaders = opts.headers || {};
404
+ const reservedNames = ["x-agent-id", "x-payment"];
405
+ for (const k of Object.keys(userHeaders)) {
406
+ if (reservedNames.includes(k.toLowerCase())) {
407
+ console.error(c.dim(`! --header ${k} overrides the auto-attached value`));
408
+ }
409
+ }
410
+
383
411
  await ensureAgent();
384
412
 
385
413
  let targetUrl = url;
@@ -390,7 +418,7 @@ async function cmdCall(url, opts = {}) {
390
418
  // real paid sub-endpoints. Resolve it (unless --raw) so the agent doesn't have
391
419
  // to read the catalog itself. We never spend without a supplied body.
392
420
  if (!opts.raw) {
393
- const probe = await agentCall(url, "GET");
421
+ const probe = await agentCall(url, "GET", undefined, userHeaders);
394
422
  const desc = probe.status === 200 ? descriptorEndpoints(probe.text) : null;
395
423
  if (desc) {
396
424
  const eps = desc.endpoints.filter((e) => e.enabled !== false);
@@ -421,7 +449,7 @@ async function cmdCall(url, opts = {}) {
421
449
  }
422
450
 
423
451
  // call the resolved endpoint — pay if it answers 402
424
- let r = await agentCall(targetUrl, method, data);
452
+ let r = await agentCall(targetUrl, method, data, userHeaders);
425
453
  if (r.status !== 402) {
426
454
  if (r.status >= 400) die(`${r.status} — ${r.text.slice(0, 300)}`);
427
455
  return console.log(r.text); // raw body, no post-processing
@@ -435,8 +463,11 @@ async function cmdCall(url, opts = {}) {
435
463
  const xPayment = paid.xPaymentB64 || paid.xPayment;
436
464
  if (!xPayment) die("x402-v3 did not return a payment token (xPaymentB64).");
437
465
 
438
- // 3. retry with the payment token (against the resolved endpoint, with X-Agent-ID)
439
- r = await agentCall(targetUrl, method, opts.data, { "X-Payment": xPayment });
466
+ // 3. retry with the payment token (against the resolved endpoint, with
467
+ // X-Agent-ID + user headers). The freshly-minted X-Payment is appended
468
+ // LAST so it wins over any user-supplied X-Payment — preserving the x402
469
+ // settlement that the CLI just signed for this exact 402 challenge.
470
+ r = await agentCall(targetUrl, method, opts.data, { ...userHeaders, "X-Payment": xPayment });
440
471
  if (r.status >= 400) die(`paid retry failed: ${r.status} — ${r.text.slice(0, 300)}`);
441
472
  console.log(r.text); // raw body, no post-processing
442
473
  }
@@ -479,6 +510,109 @@ async function cmdTopup(vendor, opts = {}) {
479
510
  console.log(c.green("✓") + ` topped up ${vendor} · +${added.toLocaleString()} Units · balance ${Number(res.balance ?? 0).toLocaleString()} Units`);
480
511
  }
481
512
 
513
+ // --- api (manage your oneshot API configs) ----------------------------------
514
+ // `planner api <verb>` — resource-first, like `planner keys`. All verbs auth
515
+ // with auth:true (fxa_live_ key if present, else an auto-minted Agent VC) and
516
+ // are owner-scoped server-side.
517
+ function readSpec(file) {
518
+ let raw;
519
+ try {
520
+ raw = file ? readFileSync(file, "utf8") : readFileSync(0, "utf8");
521
+ } catch (e) {
522
+ return die(`could not read spec ${file ? file : "from stdin"}: ${e.message}`);
523
+ }
524
+ try {
525
+ return JSON.parse(raw);
526
+ } catch (e) {
527
+ return die(`spec is not valid JSON${file ? ` (${file})` : " (stdin)"}: ${e.message}`);
528
+ }
529
+ }
530
+
531
+ // Overlay --name/--upstream/--wallet/--private/--require-agent-auth onto a spec.
532
+ function applyOverrides(spec, opts) {
533
+ if (opts.name != null) spec.serverName = opts.name;
534
+ if (opts.upstream != null) spec.upstreamBaseUrl = opts.upstream;
535
+ if (opts.wallet != null) spec.yourWallet = opts.wallet;
536
+ if (opts.private) spec.isPrivate = true;
537
+ if (opts.requireAgentAuth) spec.requireAgentAuth = true;
538
+ return spec;
539
+ }
540
+
541
+ async function cmdApi(sub, rest, opts) {
542
+ switch (sub) {
543
+ case "create": case "new": return cmdApiCreate(opts);
544
+ case "list": case "ls": return cmdApiList();
545
+ case "get": case "show": return cmdApiGet(rest[0]);
546
+ case "edit": case "update": return cmdApiEdit(rest[0], opts);
547
+ default:
548
+ return die(
549
+ "usage: planner api <create|list|get|edit>\n" +
550
+ " api create --file <spec.json> [--publish] [--dry-run]\n" +
551
+ " api list\n" +
552
+ " api get <idOrSlug>\n" +
553
+ " api edit <idOrSlug> [--file <spec.json>] [--publish] [--dry-run]",
554
+ );
555
+ }
556
+ }
557
+
558
+ async function cmdApiCreate(opts) {
559
+ const spec = applyOverrides(readSpec(opts.file), opts);
560
+ spec.status = opts.publish ? "published" : "draft";
561
+
562
+ if (opts.dryRun) {
563
+ console.error(c.dim(`· dry-run — would POST ${spec.status} config to ${PROXY}/configs/api:`));
564
+ console.log(JSON.stringify(spec, null, 2));
565
+ return;
566
+ }
567
+
568
+ const { data } = await http(`${PROXY}/configs/api`, { method: "POST", auth: true, body: spec });
569
+ const tag = data.status === "published" ? c.green("published") : c.dim("draft");
570
+ console.log(c.green("✓") + ` created ${c.bold(data.slug || data.configId)} · ${tag}`);
571
+ if (data.proxyUrl) console.log(c.dim(` proxy: ${data.proxyUrl}`));
572
+ console.log(c.dim(
573
+ data.status === "published"
574
+ ? ` test it: planner call ${data.proxyUrl || data.slug}`
575
+ : ` publish: planner api edit ${data.slug || data.configId} --publish`,
576
+ ));
577
+ }
578
+
579
+ async function cmdApiList() {
580
+ const { data } = await http(`${PROXY}/configs/api`, { auth: true });
581
+ const rows = data.configs || [];
582
+ if (!rows.length) return console.log(c.dim(" no api configs yet — `planner api create --file <spec.json>`"));
583
+ console.log(" " + c.dim(pad("slug", 26) + pad("name", 22) + pad("status", 10) + pad("vis", 9) + pad("eps", 5) + "created"));
584
+ for (const r of rows) {
585
+ const st = r.status === "published" ? c.green(pad("published", 10)) : c.dim(pad("draft", 10));
586
+ const vis = r.isPrivate ? "private" : "public";
587
+ 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)}`);
588
+ }
589
+ }
590
+
591
+ async function cmdApiGet(idOrSlug) {
592
+ if (!idOrSlug) return die("usage: planner api get <idOrSlug>");
593
+ const { data } = await http(`${PROXY}/configs/api/${encodeURIComponent(idOrSlug)}`, { auth: true });
594
+ console.log(JSON.stringify(data, null, 2));
595
+ }
596
+
597
+ async function cmdApiEdit(idOrSlug, opts) {
598
+ if (!idOrSlug) return die("usage: planner api edit <idOrSlug> [--file <spec.json>] [--publish] [--dry-run]");
599
+ const spec = applyOverrides(opts.file ? readSpec(opts.file) : {}, opts);
600
+ if (opts.publish) spec.status = "published";
601
+ if (Object.keys(spec).length === 0) {
602
+ return die("nothing to edit — pass --file and/or flags (--name, --upstream, --publish, …)");
603
+ }
604
+
605
+ if (opts.dryRun) {
606
+ console.error(c.dim(`· dry-run — would PATCH ${PROXY}/configs/api/${idOrSlug}:`));
607
+ console.log(JSON.stringify(spec, null, 2));
608
+ return;
609
+ }
610
+
611
+ const { data } = await http(`${PROXY}/configs/api/${encodeURIComponent(idOrSlug)}`, { method: "PATCH", auth: true, body: spec });
612
+ const tag = data.status === "published" ? c.green("published") : c.dim("draft");
613
+ console.log(c.green("✓") + ` updated ${c.bold(data.slug || data.configId)} · ${tag}`);
614
+ }
615
+
482
616
  // --- plan (thin call to server-side endpoint) --------------------------------
483
617
  async function cmdPlan(task) {
484
618
  if (!task) return die('usage: planner plan "<task>"');
@@ -602,6 +736,13 @@ ${c.bold("Oneshot APIs")}
602
736
  ${c.cyan("planner call <base>")} show the paid endpoint + its required body
603
737
  ${c.cyan("planner call <base> --data '<json>'")} call it; pay the 402 if needed
604
738
  ${c.dim("--endpoint <path>")} picks when there are several; ${c.dim("--raw")} hits a URL directly.
739
+ ${c.bold("Manage your APIs")} ${c.dim("(fxa_live_ key or auto-minted Agent VC)")}
740
+ ${c.cyan("planner api create --file <spec.json> [--publish]")} create (draft unless --publish)
741
+ ${c.cyan("planner api list")} your api configs
742
+ ${c.cyan("planner api get <idOrSlug>")} print the round-trippable spec
743
+ ${c.cyan("planner api edit <idOrSlug> [--file <s>] [--publish]")} update fields / replace endpoints
744
+ ${c.dim("Spec JSON: serverName, serverDescription, upstreamBaseUrl, endpoints[]. Wallet defaults from your account.")}
745
+ ${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")}
605
746
  `,
606
747
  models: () => `
607
748
  ${c.bold("Models — merchant-centric")}
@@ -636,14 +777,18 @@ ${c.bold("Headline")}
636
777
  planner plan "<task>" recommend the models, APIs & skills for a task
637
778
  planner call <url> make a paid API call — handles the x402 mandate + payment
638
779
  [--pay credits|usdc] [--method GET|POST] [--data '<json>']
780
+ [--header "Name: Value"] (repeatable; e.g. an upstream Bearer token)
639
781
 
640
782
  ${c.bold("Primitives")} ${c.dim("(all live)")}
641
783
  planner search "<q>" [--type api,skill,model] discover resources
642
784
  planner models [--vendor <slug>] model catalog + Units rates
785
+ planner vendors list fundable merchants (slug, models, price)
643
786
  planner balance [vendor] prepaid Units per merchant
644
787
  planner topup <vendor> [--credits <N>|--bundle <slug>] prepay Units (x402, via wallet)
645
788
  planner ledger <vendor> spend/topup history
646
789
  planner keys [create|update <id>|revoke <id>] manage fxa_live_ API keys (Agent VC only)
790
+ planner api create --file <spec.json> [--publish] create a oneshot API (draft by default)
791
+ planner api list | get <idOrSlug> | edit <idOrSlug> list / inspect / update your APIs
647
792
  planner login <fxa_live_…> / planner whoami auth
648
793
  planner info [units|auth|pay|keys|apis|models|skills] explain how the platform works
649
794
 
@@ -660,9 +805,31 @@ function die(msg, ctx) {
660
805
  async function main() {
661
806
  const args = argv.slice(2);
662
807
  const cmd = args[0];
663
- const FLAGS = ["--type", "--vendor", "--pay", "--method", "--data", "--budget", "--seconds", "--endpoint", "--credits", "--bundle", "--name", "--cap"];
664
- const BOOLS = ["--raw"];
808
+ const FLAGS = ["--type", "--vendor", "--pay", "--method", "--data", "--budget", "--seconds", "--endpoint", "--credits", "--bundle", "--name", "--cap", "--file", "--upstream", "--wallet", "--header"];
809
+ const BOOLS = ["--raw", "--publish", "--private", "--require-agent-auth", "--dry-run"];
665
810
  const getFlag = (n) => { const i = args.indexOf(n); return i !== -1 ? args[i + 1] : null; };
811
+ // repeatable flag → array of all values (in order)
812
+ const getFlagAll = (n) => {
813
+ const out = [];
814
+ for (let i = 0; i < args.length - 1; i++) if (args[i] === n) out.push(args[i + 1]);
815
+ return out;
816
+ };
817
+ // parse `--header "Name: Value"` (repeatable) into a headers object. Splits
818
+ // on the FIRST colon so JWTs/URLs in the value are preserved. Malformed
819
+ // entries are warned and skipped, never thrown — a broken header shouldn't
820
+ // poison the whole call.
821
+ const parseHeaders = (specs) => {
822
+ const out = {};
823
+ for (const s of specs) {
824
+ const i = s.indexOf(":");
825
+ if (i <= 0) { console.error(c.dim(`! ignoring malformed --header "${s}" (expected "Name: Value")`)); continue; }
826
+ const name = s.slice(0, i).trim();
827
+ const value = s.slice(i + 1).trim();
828
+ if (!name) continue;
829
+ out[name] = value;
830
+ }
831
+ return out;
832
+ };
666
833
  const type = getFlag("--type");
667
834
  const vendor = getFlag("--vendor");
668
835
  const rest = [];
@@ -680,9 +847,11 @@ async function main() {
680
847
  pay: getFlag("--pay"), method: getFlag("--method"), data: getFlag("--data"),
681
848
  budget: getFlag("--budget"), seconds: getFlag("--seconds"),
682
849
  endpoint: getFlag("--endpoint"), raw: args.includes("--raw"),
850
+ headers: parseHeaders(getFlagAll("--header")),
683
851
  });
684
852
  case "search": return cmdSearch(arg, type);
685
853
  case "models": return cmdModels(vendor);
854
+ case "vendors": return cmdVendors();
686
855
  case "balance": case "bal": return cmdBalance(rest[0]);
687
856
  case "topup":
688
857
  return cmdTopup(rest[0], {
@@ -691,6 +860,14 @@ async function main() {
691
860
  });
692
861
  case "ledger": return cmdLedger(rest[0]);
693
862
  case "keys": return cmdKeys(rest[0], rest.slice(1), { name: getFlag("--name"), cap: getFlag("--cap") });
863
+ case "api":
864
+ return cmdApi(rest[0], rest.slice(1), {
865
+ file: getFlag("--file"), name: getFlag("--name"),
866
+ upstream: getFlag("--upstream"), wallet: getFlag("--wallet"),
867
+ publish: args.includes("--publish"), private: args.includes("--private"),
868
+ requireAgentAuth: args.includes("--require-agent-auth"),
869
+ dryRun: args.includes("--dry-run"),
870
+ });
694
871
  case "login": return cmdLogin(rest[0]);
695
872
  case "whoami": return cmdWhoami();
696
873
  case "info": return cmdInfo(rest[0]);