fullsendai 0.2.1 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +13 -1
  2. package/dist/cli.mjs +332 -38
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -39,10 +39,22 @@ added, renamed or given new abilities or prices, `status` and `doctor` notice an
39
39
  `npx fullsendai sync` does it. A tool whose model was renamed follows it to its new ID; one whose model was removed
40
40
  moves to the recommended model, and says so.
41
41
 
42
+ ## Open FullsendAI
43
+
44
+ ```sh
45
+ fullsendai
46
+ ```
47
+
48
+ In a terminal, `fullsendai` on its own opens FullsendAI: your connected tools with their model and spend this month,
49
+ your balance, and a prompt that takes every command below without the `fullsendai` in front. Tab completes commands,
50
+ model IDs and tools; ↑ and ↓ go through what you typed; Ctrl+C cancels a step; `exit` or Ctrl+D leaves. The first
51
+ time, it connects your tools first, then offers to install the `fullsendai` command so it's one word from then on
52
+ (or run `npm install -g fullsendai` yourself). `npx fullsendai` works too, without installing anything.
53
+
42
54
  ## Commands
43
55
 
44
56
  ```sh
45
- npx fullsendai connect # find tools, sign in, connect (the default)
57
+ npx fullsendai connect # find tools, sign in, connect (the default in scripts)
46
58
  npx fullsendai status # each tool's model and spend this month, and calls a backup provider answered
47
59
  npx fullsendai model # switch every connected tool to another model (or name it: model sulfuras)
48
60
  npx fullsendai doctor --fix # check settings, keys and a real request; repair settings
package/dist/cli.mjs CHANGED
@@ -7323,10 +7323,12 @@ var require_dist = __commonJS({
7323
7323
  });
7324
7324
 
7325
7325
  // src/main.ts
7326
+ import { spawn as spawn2 } from "node:child_process";
7326
7327
  import { createHash as createHash2 } from "node:crypto";
7328
+ import { createInterface } from "node:readline";
7327
7329
 
7328
7330
  // src/api.ts
7329
- var VERSION = true ? "0.2.1" : "0.0.0-dev";
7331
+ var VERSION = true ? "0.3.1" : "0.0.0-dev";
7330
7332
  var agent = `fullsendai-cli/${VERSION} (${process.platform}; node ${process.versions.node})`;
7331
7333
  var Api = class {
7332
7334
  constructor(origin, timeoutMs = 6e4) {
@@ -7540,6 +7542,10 @@ function onPath(command, w) {
7540
7542
  }
7541
7543
  return void 0;
7542
7544
  }
7545
+ function fullsendaiInstalled(w) {
7546
+ const found2 = onPath("fullsendai", w);
7547
+ return !!found2 && !/[\\/]_npx[\\/]/.test(found2);
7548
+ }
7543
7549
  function editorExtensions(w) {
7544
7550
  return [".vscode", ".vscode-insiders", ".vscode-oss", ".cursor", ".windsurf"].map((dir) => join2(w.home, dir, "extensions"));
7545
7551
  }
@@ -7550,6 +7556,87 @@ function cursorInstalled(w) {
7550
7556
  return existsSync2(join2(w.home, ".cursor"));
7551
7557
  }
7552
7558
 
7559
+ // src/shell.ts
7560
+ var SHELL_COMMANDS = [
7561
+ { name: "status", usage: "status", about: "Each tool\u2019s model and spend this month, and your balance" },
7562
+ { name: "model", usage: "model [id]", about: "Switch every tool to another model" },
7563
+ { name: "sync", usage: "sync", about: "Update every tool with the current models, abilities and prices" },
7564
+ { name: "doctor", usage: "doctor [--fix]", about: "Check every tool with a real request; --fix repairs settings" },
7565
+ { name: "connect", usage: "connect", about: "Connect more tools" },
7566
+ { name: "disconnect", usage: "disconnect", about: "Put tools back as they were and revoke their keys" },
7567
+ { name: "key", usage: "key <tool> [--show]", about: "Copy a tool\u2019s key to the clipboard" },
7568
+ { name: "home", usage: "home", about: "Show your tools, spend and balance again" },
7569
+ { name: "clear", usage: "clear", about: "Clear the screen" },
7570
+ { name: "help", usage: "help", about: "This list" },
7571
+ { name: "exit", usage: "exit", about: "Leave (or press Ctrl+D)" }
7572
+ ];
7573
+ var EXIT_WORDS = /* @__PURE__ */ new Set(["exit", "quit", "q", ":q", "bye"]);
7574
+ function splitLine(line) {
7575
+ const words = [];
7576
+ let word = "";
7577
+ let quote = null;
7578
+ let started = false;
7579
+ for (const char of line.trim()) {
7580
+ if (quote) {
7581
+ if (char === quote) quote = null;
7582
+ else word += char;
7583
+ continue;
7584
+ }
7585
+ if (char === '"' || char === "'") {
7586
+ quote = char;
7587
+ started = true;
7588
+ continue;
7589
+ }
7590
+ if (/\s/.test(char)) {
7591
+ if (started || word) words.push(word);
7592
+ word = "";
7593
+ started = false;
7594
+ continue;
7595
+ }
7596
+ word += char;
7597
+ }
7598
+ if (started || word) words.push(word);
7599
+ if (words[0] === "npx") words.shift();
7600
+ if (words[0] === "fullsendai" || words[0]?.startsWith("fullsendai@")) words.shift();
7601
+ return words;
7602
+ }
7603
+ function complete(line, context) {
7604
+ const words = line.split(/\s+/);
7605
+ const current = words.at(-1) ?? "";
7606
+ if (words.length <= 1) {
7607
+ const names = SHELL_COMMANDS.map((command) => command.name);
7608
+ const hits2 = names.filter((name) => name.startsWith(current));
7609
+ return [(hits2.length ? hits2 : names).map((name) => `${name} `), current];
7610
+ }
7611
+ const options = {
7612
+ model: context.models,
7613
+ key: [...context.tools, "--show"],
7614
+ doctor: ["--fix"],
7615
+ connect: ["--tools", "--model", "--dry-run"],
7616
+ disconnect: ["--tools", "--keep-keys"]
7617
+ };
7618
+ const choices = options[words[0]] ?? [];
7619
+ const hits = choices.filter((choice) => choice.startsWith(current));
7620
+ return [hits.map((hit) => `${hit} `), current];
7621
+ }
7622
+ function suggest(word) {
7623
+ const distance = (a, b) => {
7624
+ const row2 = Array.from({ length: b.length + 1 }, (_, index) => index);
7625
+ for (let i = 1; i <= a.length; i++) {
7626
+ let previous = row2[0];
7627
+ row2[0] = i;
7628
+ for (let j = 1; j <= b.length; j++) {
7629
+ const current = row2[j];
7630
+ row2[j] = Math.min(row2[j] + 1, row2[j - 1] + 1, previous + (a[i - 1] === b[j - 1] ? 0 : 1));
7631
+ previous = current;
7632
+ }
7633
+ }
7634
+ return row2[b.length];
7635
+ };
7636
+ const best = SHELL_COMMANDS.map((command) => ({ name: command.name, far: distance(word.toLowerCase(), command.name) })).sort((a, b) => a.far - b.far)[0];
7637
+ return best && best.far <= 2 ? best.name : void 0;
7638
+ }
7639
+
7553
7640
  // src/state.ts
7554
7641
  import { spawnSync } from "node:child_process";
7555
7642
  import { mkdirSync as mkdirSync2 } from "node:fs";
@@ -10218,6 +10305,23 @@ var cyan = paint(36, 39);
10218
10305
  var volt = (text) => colour ? `\x1B[38;5;154m${text}\x1B[39m` : text;
10219
10306
  var sym = unicode ? { ok: "\u2713", fail: "\u2717", warn: "!", dot: "\xB7", arrow: "\u203A", on: "\u25C9", off: "\u25CB", pointer: "\u276F", line: "\u2500", skip: "\u2013" } : { ok: "+", fail: "x", warn: "!", dot: "-", arrow: ">", on: "(*)", off: "( )", pointer: ">", line: "-", skip: "-" };
10220
10307
  var jsonMode = false;
10308
+ var shellMode = false;
10309
+ function setShell(on) {
10310
+ shellMode = on;
10311
+ }
10312
+ var Cancelled = class extends Error {
10313
+ constructor() {
10314
+ super("Cancelled.");
10315
+ }
10316
+ };
10317
+ var cmd = (rest) => shellMode ? rest : `npx fullsendai ${rest}`;
10318
+ function interrupt(reject) {
10319
+ if (!shellMode) {
10320
+ restoreTerminal();
10321
+ process.exit(130);
10322
+ }
10323
+ reject(new Cancelled());
10324
+ }
10221
10325
  function setJson2(on) {
10222
10326
  jsonMode = on;
10223
10327
  }
@@ -10233,7 +10337,7 @@ function row(state, name, detail = "", width = 14) {
10233
10337
  }
10234
10338
  function header2(title, sub) {
10235
10339
  print();
10236
- print(` ${volt(bold("FullsendAI"))} ${dim(sym.dot)} ${title}`);
10340
+ print(shellMode ? ` ${bold(title.charAt(0).toUpperCase() + title.slice(1))}` : ` ${volt(bold("FullsendAI"))} ${dim(sym.dot)} ${title}`);
10237
10341
  if (sub) print(` ${dim(sub)}`);
10238
10342
  print();
10239
10343
  }
@@ -10263,24 +10367,27 @@ async function ask(question, secret = false) {
10263
10367
  return readLine(secret);
10264
10368
  }
10265
10369
  function readLine(secret = false) {
10266
- return new Promise((resolve) => {
10370
+ return new Promise((resolve, reject) => {
10267
10371
  const input = process.stdin;
10268
10372
  let value = "";
10269
10373
  input.setRawMode?.(true);
10270
10374
  input.resume();
10271
10375
  input.setEncoding("utf8");
10272
- const done = (result) => {
10376
+ const stop = () => {
10273
10377
  input.setRawMode?.(false);
10274
10378
  input.pause();
10275
10379
  input.off("data", onData);
10276
10380
  out.write("\n");
10381
+ };
10382
+ const done = (result) => {
10383
+ stop();
10277
10384
  resolve(result);
10278
10385
  };
10279
10386
  const onData = (chunk) => {
10280
10387
  for (const char of chunk) {
10281
10388
  if (char === "") {
10282
- restoreTerminal();
10283
- process.exit(130);
10389
+ stop();
10390
+ return interrupt(reject);
10284
10391
  }
10285
10392
  if (char === "\r" || char === "\n") return done(value);
10286
10393
  if (char === "\x7F" || char === "\b") {
@@ -10325,17 +10432,23 @@ async function checklist(question, choices) {
10325
10432
  process.stdin.resume();
10326
10433
  out.write("\x1B[?25l");
10327
10434
  draw(true);
10328
- return new Promise((resolve) => {
10435
+ return new Promise((resolve, reject) => {
10329
10436
  const move = (step) => {
10330
10437
  for (let tries = 0; tries < choices.length; tries++) {
10331
10438
  at = (at + step + choices.length) % choices.length;
10332
10439
  if (!choices[at].disabled) break;
10333
10440
  }
10334
10441
  };
10442
+ const stop = () => {
10443
+ process.stdin.off("keypress", onKey);
10444
+ process.stdin.setRawMode?.(false);
10445
+ process.stdin.pause();
10446
+ out.write("\x1B[?25h");
10447
+ };
10335
10448
  const onKey = (text, key2 = {}) => {
10336
10449
  if (key2.ctrl && key2.name === "c") {
10337
- restoreTerminal();
10338
- process.exit(130);
10450
+ stop();
10451
+ return interrupt(reject);
10339
10452
  }
10340
10453
  if (key2.name === "up" || text === "k") move(-1);
10341
10454
  else if (key2.name === "down" || text === "j") move(1);
@@ -10344,10 +10457,7 @@ async function checklist(question, choices) {
10344
10457
  const all = choices.filter((choice) => !choice.disabled).every((choice) => choice.checked);
10345
10458
  for (const choice of choices) if (!choice.disabled) choice.checked = !all;
10346
10459
  } else if (key2.name === "return" || key2.name === "enter") {
10347
- process.stdin.off("keypress", onKey);
10348
- process.stdin.setRawMode?.(false);
10349
- process.stdin.pause();
10350
- out.write("\x1B[?25h");
10460
+ stop();
10351
10461
  return resolve(chosen());
10352
10462
  }
10353
10463
  draw(false);
@@ -10378,19 +10488,22 @@ async function select(question, options, initial) {
10378
10488
  process.stdin.resume();
10379
10489
  out.write("\x1B[?25l");
10380
10490
  draw(true);
10381
- return new Promise((resolve) => {
10491
+ return new Promise((resolve, reject) => {
10492
+ const stop = () => {
10493
+ process.stdin.off("keypress", onKey);
10494
+ process.stdin.setRawMode?.(false);
10495
+ process.stdin.pause();
10496
+ out.write("\x1B[?25h");
10497
+ };
10382
10498
  const onKey = (text, key2 = {}) => {
10383
10499
  if (key2.ctrl && key2.name === "c") {
10384
- restoreTerminal();
10385
- process.exit(130);
10500
+ stop();
10501
+ return interrupt(reject);
10386
10502
  }
10387
10503
  if (key2.name === "up" || text === "k") at = (at - 1 + options.length) % options.length;
10388
10504
  else if (key2.name === "down" || text === "j") at = (at + 1) % options.length;
10389
10505
  else if (key2.name === "return" || key2.name === "enter") {
10390
- process.stdin.off("keypress", onKey);
10391
- process.stdin.setRawMode?.(false);
10392
- process.stdin.pause();
10393
- out.write("\x1B[?25h");
10506
+ stop();
10394
10507
  return resolve(options[at].id);
10395
10508
  }
10396
10509
  draw(false);
@@ -10416,6 +10529,7 @@ async function main(argv) {
10416
10529
  const w = here();
10417
10530
  const origin = (flags.url ?? w.env.FULLSENDAI_URL ?? DEFAULT_ORIGIN).replace(/\/+$/, "");
10418
10531
  const api = new Api(origin);
10532
+ if (!command) return interactive && !flags.json ? shell(api, origin, flags) : connect(api, origin, flags);
10419
10533
  switch (command) {
10420
10534
  case "connect":
10421
10535
  return connect(api, origin, flags);
@@ -10462,7 +10576,7 @@ async function connect(api, origin, flags) {
10462
10576
  for (const { tool, found: found2 } of installed) row("ok", tool.name, dim([found2.version, found2.how].filter(Boolean).join(sym.dot === "\xB7" ? " \xB7 " : " - ")));
10463
10577
  print();
10464
10578
  } else {
10465
- print(` Found ${installed.length === 1 ? "1 coding tool" : `${installed.length} coding tools`}. What each uses now is backed up, and ${cyan("npx fullsendai disconnect")} brings it back.`);
10579
+ print(` Found ${installed.length === 1 ? "1 coding tool" : `${installed.length} coding tools`}. What each uses now is backed up, and ${cyan(cmd("disconnect"))} brings it back.`);
10466
10580
  print();
10467
10581
  }
10468
10582
  const chosen = flags.tools ? flags.tools : await checklist(
@@ -10514,7 +10628,7 @@ async function connect(api, origin, flags) {
10514
10628
  if (ask2) print();
10515
10629
  const modelId = ask2 ? await select("Which model should your tools use?", modelOptions(models.value, suggested), suggested) : suggested;
10516
10630
  const chosenModel = models.value.find((model2) => model2.id === modelId);
10517
- print(` Model for all tools: ${bold(chosenModel?.name ?? modelId)} ${dim(`(${modelId} \xB7 change any time with: npx fullsendai model)`)}`);
10631
+ print(` Model for all tools: ${bold(chosenModel?.name ?? modelId)} ${dim(`(${modelId} \xB7 change any time with: ${cmd("model")})`)}`);
10518
10632
  print();
10519
10633
  const folder = backupFolder(w);
10520
10634
  const ctx = { w, origin, base: `${origin}/v1`, model: modelId, models: models.value };
@@ -10571,7 +10685,7 @@ async function connect(api, origin, flags) {
10571
10685
  if (unpaid) {
10572
10686
  print(` ${yellow(bold("Add credit to start."))} Your FullsendAI balance is empty, so the test calls were refused.`);
10573
10687
  print(` Your tools are set up and work as soon as you add credit at ${cyan(`${origin}/dashboard/billing`)}.`);
10574
- print(` ${dim("Then check them with: npx fullsendai doctor")}`);
10688
+ print(` ${dim(`Then check them with: ${cmd("doctor")}`)}`);
10575
10689
  print();
10576
10690
  }
10577
10691
  if (good) {
@@ -10590,7 +10704,8 @@ async function connect(api, origin, flags) {
10590
10704
  print(` ${dim(`Cap what a tool can spend: ${origin.replace(/^https?:\/\//, "")}/dashboard/keys, then that tool's key, Monthly spend cap.`)}`);
10591
10705
  }
10592
10706
  print();
10593
- print(` ${dim(`Backups: ${short(folder, w.home)} ${sym.dot} Spend per tool: npx fullsendai status ${sym.dot} Undo: npx fullsendai disconnect`)}`);
10707
+ print(` ${dim(`Backups: ${short(folder, w.home)} ${sym.dot} Spend per tool: ${cmd("status")} ${sym.dot} Undo: ${cmd("disconnect")}`)}`);
10708
+ await offerInstall(flags);
10594
10709
  }
10595
10710
  saveState(w, { ...loadState(w, origin), catalog: fingerprint(models.value) });
10596
10711
  return output(flags, { connected: results.filter((result) => result.ok).map((result) => result.id), guided: guided.map((tool) => tool.id), failed: results.filter((result) => !result.ok), model: modelId, backups: folder }, results.some((result) => !result.ok) ? 1 : 0);
@@ -10627,7 +10742,7 @@ async function showGuide(tool, ctx, secret, w) {
10627
10742
  print(` ${bold(tool.name)} ${dim("keeps its settings in the app. Three steps:")}`);
10628
10743
  guide.steps.forEach((step, index) => print(` ${dim(`${index + 1}.`)} ${step}`));
10629
10744
  const copied = interactive && await confirm(`Copy ${tool.name}'s key to the clipboard now?`) && copy(secret, w);
10630
- for (const [label, value] of guide.values) print(` ${dim(label.padEnd(26))} ${value.includes("clipboard") ? copied ? green("copied to your clipboard") : `run ${cyan(`npx fullsendai key ${tool.id}`)} to copy it` : cyan(value)}`);
10745
+ for (const [label, value] of guide.values) print(` ${dim(label.padEnd(26))} ${value.includes("clipboard") ? copied ? green("copied to your clipboard") : `run ${cyan(cmd(`key ${tool.id}`))} to copy it` : cyan(value)}`);
10631
10746
  }
10632
10747
  async function status(api, origin, flags) {
10633
10748
  const w = here();
@@ -10635,7 +10750,7 @@ async function status(api, origin, flags) {
10635
10750
  const ids = Object.keys(state.tools);
10636
10751
  header2("status", state.account?.email ? `Signed in as ${state.account.email}` : void 0);
10637
10752
  if (!ids.length) {
10638
- print(` No tools are connected yet. Run ${cyan("npx fullsendai connect")}.`);
10753
+ print(` No tools are connected yet. Run ${cyan(cmd("connect"))}.`);
10639
10754
  return output(flags, { tools: [] }, 0);
10640
10755
  }
10641
10756
  const report = await spin(
@@ -10654,7 +10769,7 @@ async function status(api, origin, flags) {
10654
10769
  for (const { tool, connection, reading, info } of report) {
10655
10770
  if (info.ok) balance = info.value.balance;
10656
10771
  const spend = info.ok ? `$${info.value.usage.toFixed(2)} this month${connection.given ? dim(" on your shared key") : ""}` : red(info.error);
10657
- const drift = reading && !reading.connected ? yellow(" \xB7 settings changed since connect (npx fullsendai doctor --fix)") : reading?.problem ? yellow(` \xB7 ${reading.problem}`) : "";
10772
+ const drift = reading && !reading.connected ? yellow(` \xB7 settings changed since connect (${cmd("doctor --fix")})`) : reading?.problem ? yellow(` \xB7 ${reading.problem}`) : "";
10658
10773
  row(info.ok && !drift ? "ok" : "warn", tool?.name ?? "?", `${dim(reading?.model ?? connection.model)} ${spend}${drift}`);
10659
10774
  }
10660
10775
  const byKey = new Map(report.flatMap(({ connection, info }) => info.ok ? [[connection.key, info.value.failovers ?? 0]] : []));
@@ -10675,14 +10790,14 @@ async function model(api, origin, wanted, flags) {
10675
10790
  const w = here();
10676
10791
  const state = loadState(w, origin);
10677
10792
  const ids = Object.keys(state.tools);
10678
- if (!ids.length) return fail("No tools are connected yet. Run npx fullsendai connect first.", flags);
10793
+ if (!ids.length) return fail(`No tools are connected yet. Run ${cmd("connect")} first.`, flags);
10679
10794
  const models = await api.models(state.tools[ids[0]].key);
10680
10795
  if (!models.ok) return fail(models.error, flags);
10681
10796
  let id = wanted;
10682
10797
  if (!id) {
10683
10798
  header2("pick a model");
10684
10799
  const current = state.tools[ids[0]].model;
10685
- if (!interactive) return fail(`Name the model: npx fullsendai model <id>. Live models: ${models.value.map((option) => option.id).join(", ")}.`, flags);
10800
+ if (!interactive) return fail(`Name the model: ${cmd("model <id>")}. Live models: ${models.value.map((option) => option.id).join(", ")}.`, flags);
10686
10801
  id = await select("Switch every tool to:", modelOptions(models.value, pickModel(models.value, void 0) ?? current).map((option) => option.id === current ? { ...option, tag: "current" } : option), current);
10687
10802
  }
10688
10803
  if (!id || !models.value.some((option) => option.id === id)) return fail(`There's no model called ${id ?? "(none)"}. Live models: ${models.value.map((option) => option.id).join(", ")}.`, flags);
@@ -10711,13 +10826,191 @@ async function model(api, origin, wanted, flags) {
10711
10826
  }
10712
10827
  return output(flags, { model: id }, 0);
10713
10828
  }
10829
+ async function shell(api, origin, flags) {
10830
+ const w = here();
10831
+ setShell(true);
10832
+ if (!Object.keys(loadState(w, origin).tools).length) {
10833
+ print();
10834
+ print(` ${volt(bold("FullsendAI"))} ${dim(sym.dot)} Welcome. Let\u2019s connect your coding tools first.`);
10835
+ await guarded(() => connect(api, origin, { ...flags, json: false }));
10836
+ if (!Object.keys(loadState(w, origin).tools).length) return 0;
10837
+ }
10838
+ const context = { models: [], tools: [] };
10839
+ await home(api, origin, context);
10840
+ const history = [];
10841
+ for (; ; ) {
10842
+ context.tools = Object.keys(loadState(w, origin).tools);
10843
+ const line = await promptLine(history, context);
10844
+ if (line === null) break;
10845
+ const words = splitLine(line);
10846
+ if (!words.length) continue;
10847
+ if (history[0] !== line.trim()) history.unshift(line.trim());
10848
+ if (EXIT_WORDS.has(words[0].toLowerCase())) break;
10849
+ const parsed = parse4(words);
10850
+ const given = { ...parsed.flags, json: false, url: flags.url };
10851
+ const name = parsed.command ?? "";
10852
+ await guarded(async () => {
10853
+ switch (name) {
10854
+ case "status":
10855
+ return status(api, origin, given);
10856
+ case "model":
10857
+ return model(api, origin, parsed.args[0] ?? given.model, given);
10858
+ case "sync":
10859
+ return sync(api, origin, given);
10860
+ case "doctor":
10861
+ return doctor(api, origin, given);
10862
+ case "connect":
10863
+ await connect(api, origin, given);
10864
+ return home(api, origin, context);
10865
+ case "disconnect":
10866
+ await disconnect(api, origin, given);
10867
+ return home(api, origin, context);
10868
+ case "key":
10869
+ return key(origin, parsed.args[0], given);
10870
+ case "home":
10871
+ return home(api, origin, context);
10872
+ case "clear":
10873
+ process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
10874
+ return home(api, origin, context);
10875
+ case "help":
10876
+ return shellHelp();
10877
+ case "version":
10878
+ return print(` ${VERSION}`);
10879
+ default: {
10880
+ const near = suggest(name);
10881
+ print(` ${yellow(sym.warn)} There\u2019s no \u201C${name}\u201D command.${near ? ` Did you mean ${cyan(near)}?` : ""} Type ${cyan("help")} for the list.`);
10882
+ }
10883
+ }
10884
+ });
10885
+ }
10886
+ print(` ${dim("See you soon. Type")} ${cyan("fullsendai")} ${dim("to come back.")}`);
10887
+ return 0;
10888
+ }
10889
+ async function guarded(work) {
10890
+ try {
10891
+ await work();
10892
+ } catch (error) {
10893
+ if (error instanceof Cancelled) print(` ${dim("Cancelled. Nothing changed.")}`);
10894
+ else print(` ${red(sym.fail)} ${error.message}`);
10895
+ }
10896
+ print();
10897
+ }
10898
+ async function home(api, origin, context) {
10899
+ const w = here();
10900
+ const state = loadState(w, origin);
10901
+ const ids = Object.keys(state.tools);
10902
+ print();
10903
+ print(` ${volt(bold("FullsendAI"))}${state.account?.email ? ` ${dim(sym.dot)} ${state.account.email}` : ""}`);
10904
+ if (!ids.length) {
10905
+ print();
10906
+ print(` No tools are connected. Type ${cyan("connect")} to start, or ${cyan("exit")} to leave.`);
10907
+ print();
10908
+ return;
10909
+ }
10910
+ const keys = [...new Set(ids.map((id) => state.tools[id].key))];
10911
+ const [models, infos] = await spin("Loading your tools\u2026", () => Promise.all([api.models(keys[0]), Promise.all(keys.map((secret) => api.key(secret)))]));
10912
+ if (models.ok) context.models = models.value.map((each) => each.id);
10913
+ const info = new Map(keys.map((secret, index) => [secret, infos[index]]));
10914
+ const first = infos.find((each) => each.ok);
10915
+ const balance = first?.ok ? first.value.balance : void 0;
10916
+ const allowance = first?.ok ? first.value.plan_allowance_remaining ?? 0 : 0;
10917
+ if (balance !== void 0) print(` ${dim("Balance")} ${balance <= 0 && allowance <= 0 ? yellow("$0.00, add credit to keep your tools working") : bold(`$${balance.toFixed(2)}`)}${allowance > 0 ? dim(` + $${allowance.toFixed(2)} left in this month\u2019s plan`) : ""}`);
10918
+ print();
10919
+ const width = Math.max(...ids.map((id) => (toolById(id)?.name ?? id).length)) + 2;
10920
+ const nameWidth = Math.min(26, Math.max(...ids.map((id) => modelName(models, state.tools[id].model).length)) + 2);
10921
+ for (const id of ids) {
10922
+ const connection = state.tools[id];
10923
+ const result = info.get(connection.key);
10924
+ const spend = result?.ok ? `$${result.value.usage.toFixed(2)} this month${connection.given ? " (shared key)" : ""}` : red(result?.error ?? "");
10925
+ print(` ${result?.ok ? green(sym.ok) : yellow(sym.warn)} ${bold((toolById(id)?.name ?? id).padEnd(width))}${modelName(models, connection.model).padEnd(nameWidth)}${dim(spend)}`);
10926
+ }
10927
+ const failovers = keys.reduce((sum, secret) => {
10928
+ const result = info.get(secret);
10929
+ return sum + (result?.ok ? result.value.failovers ?? 0 : 0);
10930
+ }, 0);
10931
+ if (failovers) print(` ${volt(sym.ok)} ${failovers === 1 ? "1 call" : `${failovers} calls`} this month kept working when a provider failed.`);
10932
+ await noticeChanges(api, origin, { yes: false, json: false, dryRun: false, keepKeys: false, show: false, fix: false }, false);
10933
+ print();
10934
+ print(` ${dim("Type a command, or press Tab:")} ${["status", "model", "sync", "doctor", "connect", "disconnect", "key", "help", "exit"].map((name) => cyan(name)).join(dim(` ${sym.dot} `))}`);
10935
+ print();
10936
+ }
10937
+ var modelName = (models, id) => models.ok ? models.value.find((each) => each.id === id)?.name ?? id : id;
10938
+ function promptLine(history, context) {
10939
+ return new Promise((resolve) => {
10940
+ const rl = createInterface({
10941
+ input: process.stdin,
10942
+ output: process.stdout,
10943
+ terminal: true,
10944
+ history: [...history],
10945
+ historySize: 200,
10946
+ removeHistoryDuplicates: true,
10947
+ completer: (line) => complete(line, context)
10948
+ });
10949
+ let settled = false;
10950
+ const finish = (value) => {
10951
+ if (settled) return;
10952
+ settled = true;
10953
+ rl.close();
10954
+ if (value === null) process.stdout.write("\n");
10955
+ resolve(value);
10956
+ };
10957
+ rl.on("line", (line) => finish(line));
10958
+ rl.on("close", () => finish(null));
10959
+ rl.on("SIGINT", () => {
10960
+ if (rl.line) {
10961
+ rl.write(null, { ctrl: true, name: "u" });
10962
+ return;
10963
+ }
10964
+ process.stdout.write(`
10965
+ ${dim("To leave, type exit or press Ctrl+D.")}
10966
+ `);
10967
+ rl.prompt();
10968
+ });
10969
+ rl.setPrompt(`${volt("fullsendai")} ${dim(sym.arrow)} `);
10970
+ rl.prompt();
10971
+ });
10972
+ }
10973
+ function shellHelp() {
10974
+ const width = Math.max(...SHELL_COMMANDS.map((command) => command.usage.length)) + 3;
10975
+ print();
10976
+ for (const command of SHELL_COMMANDS) print(` ${cyan(command.usage.padEnd(width))}${command.about}`);
10977
+ print();
10978
+ print(` ${dim("Tab completes commands, model IDs and tools. \u2191 and \u2193 go through what you typed. Ctrl+C cancels a step.")}`);
10979
+ }
10980
+ async function offerInstall(flags) {
10981
+ const w = here();
10982
+ if (!interactive || flags.yes || flags.json || fullsendaiInstalled(w)) return;
10983
+ print();
10984
+ if (!await confirm(`Install the ${cyan("fullsendai")} command, so you can open all this any time by typing just ${cyan("fullsendai")}?`)) {
10985
+ print(` ${dim("Any time later:")} ${cyan("npm install -g fullsendai")}`);
10986
+ return;
10987
+ }
10988
+ const result = await spin("Installing the fullsendai command\u2026", () => npmInstall(w));
10989
+ if (result.ok) {
10990
+ print(` ${green(sym.ok)} Installed. Type ${cyan(bold("fullsendai"))} in any terminal to open it.`);
10991
+ return;
10992
+ }
10993
+ const denied = /EACCES|EPERM|permission denied/i.test(result.output);
10994
+ print(` ${yellow(sym.warn)} ${denied ? "npm needs admin rights to install commands on this computer." : "That didn\u2019t install."} Run this yourself:`);
10995
+ print(` ${cyan(w.platform === "win32" || !denied ? "npm install -g fullsendai" : "sudo npm install -g fullsendai")}`);
10996
+ }
10997
+ function npmInstall(w) {
10998
+ return new Promise((resolve) => {
10999
+ const child = spawn2(w.platform === "win32" ? "npm.cmd" : "npm", ["install", "-g", `fullsendai@${VERSION}`, "--no-fund", "--no-audit", "--loglevel=error"], { shell: w.platform === "win32", windowsHide: true });
11000
+ let output2 = "";
11001
+ child.stdout.on("data", (chunk) => output2 += chunk);
11002
+ child.stderr.on("data", (chunk) => output2 += chunk);
11003
+ child.on("error", (error) => resolve({ ok: false, output: error.message }));
11004
+ child.on("close", (code) => resolve({ ok: code === 0, output: output2 }));
11005
+ });
11006
+ }
10714
11007
  async function sync(api, origin, flags, quiet = false) {
10715
11008
  const w = here();
10716
11009
  const state = loadState(w, origin);
10717
11010
  const ids = Object.keys(state.tools);
10718
11011
  if (!quiet) header2("sync", "Brings each connected tool up to date with FullsendAI's models.");
10719
11012
  if (!ids.length) {
10720
- print(` No tools are connected. Run ${cyan("npx fullsendai connect")}.`);
11013
+ print(` No tools are connected. Run ${cyan(cmd("connect"))}.`);
10721
11014
  return output(flags, { synced: [], moved: [] }, 0);
10722
11015
  }
10723
11016
  const models = await spin("Fetching the models\u2026", () => api.models(state.tools[ids[0]].key));
@@ -10764,7 +11057,7 @@ async function noticeChanges(api, origin, flags, apply) {
10764
11057
  print(` ${yellow(sym.warn)} FullsendAI's models have changed since your tools were set up ${dim("(new or renamed models, abilities or prices)")}.`);
10765
11058
  const update = apply || interactive && !flags.json && await confirm("Update your tools now?");
10766
11059
  if (update) await sync(api, origin, flags, true);
10767
- else print(` Run ${cyan("npx fullsendai sync")} to update them.`);
11060
+ else print(` Run ${cyan(cmd("sync"))} to update them.`);
10768
11061
  }
10769
11062
  var uidOf = (models, id) => models.find((model2) => model2.id === id)?.uid;
10770
11063
  function fingerprint(models) {
@@ -10777,7 +11070,7 @@ async function doctor(api, origin, flags) {
10777
11070
  header2("doctor", "Checking each connected tool's settings, key and a real request.");
10778
11071
  const ids = Object.keys(state.tools);
10779
11072
  if (!ids.length) {
10780
- print(` No tools are connected. Run ${cyan("npx fullsendai connect")}.`);
11073
+ print(` No tools are connected. Run ${cyan(cmd("connect"))}.`);
10781
11074
  return output(flags, { problems: [] }, 0);
10782
11075
  }
10783
11076
  const problems = [];
@@ -10813,7 +11106,7 @@ async function doctor(api, origin, flags) {
10813
11106
  print();
10814
11107
  if (!open.length) print(` ${green("Everything works.")}`);
10815
11108
  else if (!settings.length) print(` ${yellow("Your balance is empty.")} Add credit at ${cyan(billing)}; the settings themselves are fine.`);
10816
- else print(` ${yellow(`${open.length} problem${open.length > 1 ? "s" : ""}.`)} ${flags.fix ? "" : `Run ${cyan("npx fullsendai doctor --fix")} to repair settings.`}`);
11109
+ else print(` ${yellow(`${open.length} problem${open.length > 1 ? "s" : ""}.`)} ${flags.fix ? "" : `Run ${cyan(cmd("doctor --fix"))} to repair settings.`}`);
10817
11110
  return output(flags, { problems }, open.length ? 1 : 0);
10818
11111
  }
10819
11112
  async function disconnect(api, origin, flags) {
@@ -10858,7 +11151,7 @@ function key(origin, id, flags) {
10858
11151
  if (!id || !connection) return fail(`Name a connected tool: ${Object.keys(state.tools).join(", ") || "(none connected)"}.`, flags);
10859
11152
  if (flags.show) print(connection.key);
10860
11153
  else if (copy(connection.key, w)) print(` ${green(sym.ok)} ${toolById(id)?.name}'s key is on your clipboard.`);
10861
- else print(` No clipboard here. Run ${cyan(`npx fullsendai key ${id} --show`)} to print it.`);
11154
+ else print(` No clipboard here. Run ${cyan(cmd(`key ${id} --show`))} to print it.`);
10862
11155
  return output(flags, { tool: id }, 0);
10863
11156
  }
10864
11157
  function pickModel(models, wanted) {
@@ -10903,7 +11196,7 @@ function fileFor(tool, origin) {
10903
11196
  const reading = tool.read?.({ w: here(), origin, base: `${origin}/v1` });
10904
11197
  return reading?.file ?? "";
10905
11198
  }
10906
- var short = (path, home) => path.startsWith(home) ? `~${path.slice(home.length)}` : path;
11199
+ var short = (path, home2) => path.startsWith(home2) ? `~${path.slice(home2.length)}` : path;
10907
11200
  var jsonOut;
10908
11201
  function output(flags, value, code) {
10909
11202
  if (flags.json) jsonOut = value;
@@ -10921,10 +11214,11 @@ function help() {
10921
11214
  print(`
10922
11215
  ${volt(bold("FullsendAI"))} ${dim(`v${VERSION}`)} ${dim("\xB7")} connect your coding tools in one command
10923
11216
 
10924
- ${bold("Usage")} npx fullsendai <command> [options]
11217
+ ${bold("Usage")} fullsendai Open FullsendAI: your tools, spend and every command
11218
+ fullsendai <command> Run one command (npx fullsendai <command> works too)
10925
11219
 
10926
11220
  ${bold("Commands")}
10927
- connect Find your coding tools and connect them (default)
11221
+ connect Find your coding tools and connect them
10928
11222
  status What's connected, each tool's model and spend this month
10929
11223
  model [id] Switch every connected tool to another model
10930
11224
  doctor [--fix] Check settings, keys and a real request; --fix repairs settings
@@ -10964,7 +11258,7 @@ function parse4(argv) {
10964
11258
  else if (name === "--help" || name === "-h") args.unshift("help");
10965
11259
  else args.push(arg);
10966
11260
  }
10967
- const command = args.shift() ?? "connect";
11261
+ const command = args.shift();
10968
11262
  return { command, args, flags };
10969
11263
  }
10970
11264
  main(process.argv.slice(2)).then((code) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullsendai",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "Connect Claude Code, Codex, OpenCode, Aider, Continue and more to FullsendAI in one command.",
5
5
  "keywords": ["fullsendai", "claude-code", "codex", "opencode", "aider", "continue", "cline", "cursor", "llm", "cli"],
6
6
  "homepage": "https://fullsendai.com/docs/agents",