fullsendai 0.2.0 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +13 -1
  2. package/dist/cli.mjs +330 -40
  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.0" : "0.0.0-dev";
7331
+ var VERSION = true ? "0.3.0" : "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) {
@@ -7531,7 +7533,7 @@ function onPath(command, w) {
7531
7533
  const dirs = (w.env.PATH || w.env.Path || "").split(w.platform === "win32" ? ";" : delimiter).filter(Boolean);
7532
7534
  const extensions = w.platform === "win32" ? (w.env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";") : [""];
7533
7535
  for (const dir of dirs)
7534
- for (const ext of w.platform === "win32" ? ["", ...extensions] : extensions) {
7536
+ for (const ext of w.platform === "win32" ? /\.[^\\/]+$/.test(command) ? [""] : extensions : extensions) {
7535
7537
  const file = join2(dir, command + ext);
7536
7538
  try {
7537
7539
  if (statSync(file).isFile()) return file;
@@ -7550,6 +7552,87 @@ function cursorInstalled(w) {
7550
7552
  return existsSync2(join2(w.home, ".cursor"));
7551
7553
  }
7552
7554
 
7555
+ // src/shell.ts
7556
+ var SHELL_COMMANDS = [
7557
+ { name: "status", usage: "status", about: "Each tool\u2019s model and spend this month, and your balance" },
7558
+ { name: "model", usage: "model [id]", about: "Switch every tool to another model" },
7559
+ { name: "sync", usage: "sync", about: "Update every tool with the current models, abilities and prices" },
7560
+ { name: "doctor", usage: "doctor [--fix]", about: "Check every tool with a real request; --fix repairs settings" },
7561
+ { name: "connect", usage: "connect", about: "Connect more tools" },
7562
+ { name: "disconnect", usage: "disconnect", about: "Put tools back as they were and revoke their keys" },
7563
+ { name: "key", usage: "key <tool> [--show]", about: "Copy a tool\u2019s key to the clipboard" },
7564
+ { name: "home", usage: "home", about: "Show your tools, spend and balance again" },
7565
+ { name: "clear", usage: "clear", about: "Clear the screen" },
7566
+ { name: "help", usage: "help", about: "This list" },
7567
+ { name: "exit", usage: "exit", about: "Leave (or press Ctrl+D)" }
7568
+ ];
7569
+ var EXIT_WORDS = /* @__PURE__ */ new Set(["exit", "quit", "q", ":q", "bye"]);
7570
+ function splitLine(line) {
7571
+ const words = [];
7572
+ let word = "";
7573
+ let quote = null;
7574
+ let started = false;
7575
+ for (const char of line.trim()) {
7576
+ if (quote) {
7577
+ if (char === quote) quote = null;
7578
+ else word += char;
7579
+ continue;
7580
+ }
7581
+ if (char === '"' || char === "'") {
7582
+ quote = char;
7583
+ started = true;
7584
+ continue;
7585
+ }
7586
+ if (/\s/.test(char)) {
7587
+ if (started || word) words.push(word);
7588
+ word = "";
7589
+ started = false;
7590
+ continue;
7591
+ }
7592
+ word += char;
7593
+ }
7594
+ if (started || word) words.push(word);
7595
+ if (words[0] === "npx") words.shift();
7596
+ if (words[0] === "fullsendai" || words[0]?.startsWith("fullsendai@")) words.shift();
7597
+ return words;
7598
+ }
7599
+ function complete(line, context) {
7600
+ const words = line.split(/\s+/);
7601
+ const current = words.at(-1) ?? "";
7602
+ if (words.length <= 1) {
7603
+ const names = SHELL_COMMANDS.map((command) => command.name);
7604
+ const hits2 = names.filter((name) => name.startsWith(current));
7605
+ return [(hits2.length ? hits2 : names).map((name) => `${name} `), current];
7606
+ }
7607
+ const options = {
7608
+ model: context.models,
7609
+ key: [...context.tools, "--show"],
7610
+ doctor: ["--fix"],
7611
+ connect: ["--tools", "--model", "--dry-run"],
7612
+ disconnect: ["--tools", "--keep-keys"]
7613
+ };
7614
+ const choices = options[words[0]] ?? [];
7615
+ const hits = choices.filter((choice) => choice.startsWith(current));
7616
+ return [hits.map((hit) => `${hit} `), current];
7617
+ }
7618
+ function suggest(word) {
7619
+ const distance = (a, b) => {
7620
+ const row2 = Array.from({ length: b.length + 1 }, (_, index) => index);
7621
+ for (let i = 1; i <= a.length; i++) {
7622
+ let previous = row2[0];
7623
+ row2[0] = i;
7624
+ for (let j = 1; j <= b.length; j++) {
7625
+ const current = row2[j];
7626
+ row2[j] = Math.min(row2[j] + 1, row2[j - 1] + 1, previous + (a[i - 1] === b[j - 1] ? 0 : 1));
7627
+ previous = current;
7628
+ }
7629
+ }
7630
+ return row2[b.length];
7631
+ };
7632
+ const best = SHELL_COMMANDS.map((command) => ({ name: command.name, far: distance(word.toLowerCase(), command.name) })).sort((a, b) => a.far - b.far)[0];
7633
+ return best && best.far <= 2 ? best.name : void 0;
7634
+ }
7635
+
7553
7636
  // src/state.ts
7554
7637
  import { spawnSync } from "node:child_process";
7555
7638
  import { mkdirSync as mkdirSync2 } from "node:fs";
@@ -10130,7 +10213,7 @@ function found(w, binary, configured) {
10130
10213
  const command = onPath(binary, w);
10131
10214
  if (!command) return { installed: configured, how: configured ? "settings found" : void 0 };
10132
10215
  const result = run(command, ["--version"], w, 4e3);
10133
- const version = (result.stdout || result.stderr).match(/\d+\.\d+\.\d+(?:[-.][\w.]+)?/)?.[0];
10216
+ const version = (result.stdout || result.stderr || "").match(/\d+\.\d+\.\d+(?:[-.][\w.]+)?/)?.[0];
10134
10217
  return { installed: true, version, how: "command line" };
10135
10218
  }
10136
10219
  function extensionFound(w, prefix) {
@@ -10218,6 +10301,23 @@ var cyan = paint(36, 39);
10218
10301
  var volt = (text) => colour ? `\x1B[38;5;154m${text}\x1B[39m` : text;
10219
10302
  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
10303
  var jsonMode = false;
10304
+ var shellMode = false;
10305
+ function setShell(on) {
10306
+ shellMode = on;
10307
+ }
10308
+ var Cancelled = class extends Error {
10309
+ constructor() {
10310
+ super("Cancelled.");
10311
+ }
10312
+ };
10313
+ var cmd = (rest) => shellMode ? rest : `npx fullsendai ${rest}`;
10314
+ function interrupt(reject) {
10315
+ if (!shellMode) {
10316
+ restoreTerminal();
10317
+ process.exit(130);
10318
+ }
10319
+ reject(new Cancelled());
10320
+ }
10221
10321
  function setJson2(on) {
10222
10322
  jsonMode = on;
10223
10323
  }
@@ -10233,7 +10333,7 @@ function row(state, name, detail = "", width = 14) {
10233
10333
  }
10234
10334
  function header2(title, sub) {
10235
10335
  print();
10236
- print(` ${volt(bold("FullsendAI"))} ${dim(sym.dot)} ${title}`);
10336
+ print(shellMode ? ` ${bold(title.charAt(0).toUpperCase() + title.slice(1))}` : ` ${volt(bold("FullsendAI"))} ${dim(sym.dot)} ${title}`);
10237
10337
  if (sub) print(` ${dim(sub)}`);
10238
10338
  print();
10239
10339
  }
@@ -10263,24 +10363,27 @@ async function ask(question, secret = false) {
10263
10363
  return readLine(secret);
10264
10364
  }
10265
10365
  function readLine(secret = false) {
10266
- return new Promise((resolve) => {
10366
+ return new Promise((resolve, reject) => {
10267
10367
  const input = process.stdin;
10268
10368
  let value = "";
10269
10369
  input.setRawMode?.(true);
10270
10370
  input.resume();
10271
10371
  input.setEncoding("utf8");
10272
- const done = (result) => {
10372
+ const stop = () => {
10273
10373
  input.setRawMode?.(false);
10274
10374
  input.pause();
10275
10375
  input.off("data", onData);
10276
10376
  out.write("\n");
10377
+ };
10378
+ const done = (result) => {
10379
+ stop();
10277
10380
  resolve(result);
10278
10381
  };
10279
10382
  const onData = (chunk) => {
10280
10383
  for (const char of chunk) {
10281
10384
  if (char === "") {
10282
- restoreTerminal();
10283
- process.exit(130);
10385
+ stop();
10386
+ return interrupt(reject);
10284
10387
  }
10285
10388
  if (char === "\r" || char === "\n") return done(value);
10286
10389
  if (char === "\x7F" || char === "\b") {
@@ -10325,17 +10428,23 @@ async function checklist(question, choices) {
10325
10428
  process.stdin.resume();
10326
10429
  out.write("\x1B[?25l");
10327
10430
  draw(true);
10328
- return new Promise((resolve) => {
10431
+ return new Promise((resolve, reject) => {
10329
10432
  const move = (step) => {
10330
10433
  for (let tries = 0; tries < choices.length; tries++) {
10331
10434
  at = (at + step + choices.length) % choices.length;
10332
10435
  if (!choices[at].disabled) break;
10333
10436
  }
10334
10437
  };
10438
+ const stop = () => {
10439
+ process.stdin.off("keypress", onKey);
10440
+ process.stdin.setRawMode?.(false);
10441
+ process.stdin.pause();
10442
+ out.write("\x1B[?25h");
10443
+ };
10335
10444
  const onKey = (text, key2 = {}) => {
10336
10445
  if (key2.ctrl && key2.name === "c") {
10337
- restoreTerminal();
10338
- process.exit(130);
10446
+ stop();
10447
+ return interrupt(reject);
10339
10448
  }
10340
10449
  if (key2.name === "up" || text === "k") move(-1);
10341
10450
  else if (key2.name === "down" || text === "j") move(1);
@@ -10344,10 +10453,7 @@ async function checklist(question, choices) {
10344
10453
  const all = choices.filter((choice) => !choice.disabled).every((choice) => choice.checked);
10345
10454
  for (const choice of choices) if (!choice.disabled) choice.checked = !all;
10346
10455
  } 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");
10456
+ stop();
10351
10457
  return resolve(chosen());
10352
10458
  }
10353
10459
  draw(false);
@@ -10378,19 +10484,22 @@ async function select(question, options, initial) {
10378
10484
  process.stdin.resume();
10379
10485
  out.write("\x1B[?25l");
10380
10486
  draw(true);
10381
- return new Promise((resolve) => {
10487
+ return new Promise((resolve, reject) => {
10488
+ const stop = () => {
10489
+ process.stdin.off("keypress", onKey);
10490
+ process.stdin.setRawMode?.(false);
10491
+ process.stdin.pause();
10492
+ out.write("\x1B[?25h");
10493
+ };
10382
10494
  const onKey = (text, key2 = {}) => {
10383
10495
  if (key2.ctrl && key2.name === "c") {
10384
- restoreTerminal();
10385
- process.exit(130);
10496
+ stop();
10497
+ return interrupt(reject);
10386
10498
  }
10387
10499
  if (key2.name === "up" || text === "k") at = (at - 1 + options.length) % options.length;
10388
10500
  else if (key2.name === "down" || text === "j") at = (at + 1) % options.length;
10389
10501
  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");
10502
+ stop();
10394
10503
  return resolve(options[at].id);
10395
10504
  }
10396
10505
  draw(false);
@@ -10416,6 +10525,7 @@ async function main(argv) {
10416
10525
  const w = here();
10417
10526
  const origin = (flags.url ?? w.env.FULLSENDAI_URL ?? DEFAULT_ORIGIN).replace(/\/+$/, "");
10418
10527
  const api = new Api(origin);
10528
+ if (!command) return interactive && !flags.json ? shell(api, origin, flags) : connect(api, origin, flags);
10419
10529
  switch (command) {
10420
10530
  case "connect":
10421
10531
  return connect(api, origin, flags);
@@ -10462,7 +10572,7 @@ async function connect(api, origin, flags) {
10462
10572
  for (const { tool, found: found2 } of installed) row("ok", tool.name, dim([found2.version, found2.how].filter(Boolean).join(sym.dot === "\xB7" ? " \xB7 " : " - ")));
10463
10573
  print();
10464
10574
  } 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.`);
10575
+ 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
10576
  print();
10467
10577
  }
10468
10578
  const chosen = flags.tools ? flags.tools : await checklist(
@@ -10514,7 +10624,7 @@ async function connect(api, origin, flags) {
10514
10624
  if (ask2) print();
10515
10625
  const modelId = ask2 ? await select("Which model should your tools use?", modelOptions(models.value, suggested), suggested) : suggested;
10516
10626
  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)`)}`);
10627
+ print(` Model for all tools: ${bold(chosenModel?.name ?? modelId)} ${dim(`(${modelId} \xB7 change any time with: ${cmd("model")})`)}`);
10518
10628
  print();
10519
10629
  const folder = backupFolder(w);
10520
10630
  const ctx = { w, origin, base: `${origin}/v1`, model: modelId, models: models.value };
@@ -10571,7 +10681,7 @@ async function connect(api, origin, flags) {
10571
10681
  if (unpaid) {
10572
10682
  print(` ${yellow(bold("Add credit to start."))} Your FullsendAI balance is empty, so the test calls were refused.`);
10573
10683
  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")}`);
10684
+ print(` ${dim(`Then check them with: ${cmd("doctor")}`)}`);
10575
10685
  print();
10576
10686
  }
10577
10687
  if (good) {
@@ -10590,7 +10700,8 @@ async function connect(api, origin, flags) {
10590
10700
  print(` ${dim(`Cap what a tool can spend: ${origin.replace(/^https?:\/\//, "")}/dashboard/keys, then that tool's key, Monthly spend cap.`)}`);
10591
10701
  }
10592
10702
  print();
10593
- print(` ${dim(`Backups: ${short(folder, w.home)} ${sym.dot} Spend per tool: npx fullsendai status ${sym.dot} Undo: npx fullsendai disconnect`)}`);
10703
+ print(` ${dim(`Backups: ${short(folder, w.home)} ${sym.dot} Spend per tool: ${cmd("status")} ${sym.dot} Undo: ${cmd("disconnect")}`)}`);
10704
+ await offerInstall(flags);
10594
10705
  }
10595
10706
  saveState(w, { ...loadState(w, origin), catalog: fingerprint(models.value) });
10596
10707
  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 +10738,7 @@ async function showGuide(tool, ctx, secret, w) {
10627
10738
  print(` ${bold(tool.name)} ${dim("keeps its settings in the app. Three steps:")}`);
10628
10739
  guide.steps.forEach((step, index) => print(` ${dim(`${index + 1}.`)} ${step}`));
10629
10740
  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)}`);
10741
+ 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
10742
  }
10632
10743
  async function status(api, origin, flags) {
10633
10744
  const w = here();
@@ -10635,7 +10746,7 @@ async function status(api, origin, flags) {
10635
10746
  const ids = Object.keys(state.tools);
10636
10747
  header2("status", state.account?.email ? `Signed in as ${state.account.email}` : void 0);
10637
10748
  if (!ids.length) {
10638
- print(` No tools are connected yet. Run ${cyan("npx fullsendai connect")}.`);
10749
+ print(` No tools are connected yet. Run ${cyan(cmd("connect"))}.`);
10639
10750
  return output(flags, { tools: [] }, 0);
10640
10751
  }
10641
10752
  const report = await spin(
@@ -10654,7 +10765,7 @@ async function status(api, origin, flags) {
10654
10765
  for (const { tool, connection, reading, info } of report) {
10655
10766
  if (info.ok) balance = info.value.balance;
10656
10767
  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}`) : "";
10768
+ const drift = reading && !reading.connected ? yellow(` \xB7 settings changed since connect (${cmd("doctor --fix")})`) : reading?.problem ? yellow(` \xB7 ${reading.problem}`) : "";
10658
10769
  row(info.ok && !drift ? "ok" : "warn", tool?.name ?? "?", `${dim(reading?.model ?? connection.model)} ${spend}${drift}`);
10659
10770
  }
10660
10771
  const byKey = new Map(report.flatMap(({ connection, info }) => info.ok ? [[connection.key, info.value.failovers ?? 0]] : []));
@@ -10675,14 +10786,14 @@ async function model(api, origin, wanted, flags) {
10675
10786
  const w = here();
10676
10787
  const state = loadState(w, origin);
10677
10788
  const ids = Object.keys(state.tools);
10678
- if (!ids.length) return fail("No tools are connected yet. Run npx fullsendai connect first.", flags);
10789
+ if (!ids.length) return fail(`No tools are connected yet. Run ${cmd("connect")} first.`, flags);
10679
10790
  const models = await api.models(state.tools[ids[0]].key);
10680
10791
  if (!models.ok) return fail(models.error, flags);
10681
10792
  let id = wanted;
10682
10793
  if (!id) {
10683
10794
  header2("pick a model");
10684
10795
  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);
10796
+ if (!interactive) return fail(`Name the model: ${cmd("model <id>")}. Live models: ${models.value.map((option) => option.id).join(", ")}.`, flags);
10686
10797
  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
10798
  }
10688
10799
  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 +10822,191 @@ async function model(api, origin, wanted, flags) {
10711
10822
  }
10712
10823
  return output(flags, { model: id }, 0);
10713
10824
  }
10825
+ async function shell(api, origin, flags) {
10826
+ const w = here();
10827
+ setShell(true);
10828
+ if (!Object.keys(loadState(w, origin).tools).length) {
10829
+ print();
10830
+ print(` ${volt(bold("FullsendAI"))} ${dim(sym.dot)} Welcome. Let\u2019s connect your coding tools first.`);
10831
+ await guarded(() => connect(api, origin, { ...flags, json: false }));
10832
+ if (!Object.keys(loadState(w, origin).tools).length) return 0;
10833
+ }
10834
+ const context = { models: [], tools: [] };
10835
+ await home(api, origin, context);
10836
+ const history = [];
10837
+ for (; ; ) {
10838
+ context.tools = Object.keys(loadState(w, origin).tools);
10839
+ const line = await promptLine(history, context);
10840
+ if (line === null) break;
10841
+ const words = splitLine(line);
10842
+ if (!words.length) continue;
10843
+ if (history[0] !== line.trim()) history.unshift(line.trim());
10844
+ if (EXIT_WORDS.has(words[0].toLowerCase())) break;
10845
+ const parsed = parse4(words);
10846
+ const given = { ...parsed.flags, json: false, url: flags.url };
10847
+ const name = parsed.command ?? "";
10848
+ await guarded(async () => {
10849
+ switch (name) {
10850
+ case "status":
10851
+ return status(api, origin, given);
10852
+ case "model":
10853
+ return model(api, origin, parsed.args[0] ?? given.model, given);
10854
+ case "sync":
10855
+ return sync(api, origin, given);
10856
+ case "doctor":
10857
+ return doctor(api, origin, given);
10858
+ case "connect":
10859
+ await connect(api, origin, given);
10860
+ return home(api, origin, context);
10861
+ case "disconnect":
10862
+ await disconnect(api, origin, given);
10863
+ return home(api, origin, context);
10864
+ case "key":
10865
+ return key(origin, parsed.args[0], given);
10866
+ case "home":
10867
+ return home(api, origin, context);
10868
+ case "clear":
10869
+ process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
10870
+ return home(api, origin, context);
10871
+ case "help":
10872
+ return shellHelp();
10873
+ case "version":
10874
+ return print(` ${VERSION}`);
10875
+ default: {
10876
+ const near = suggest(name);
10877
+ print(` ${yellow(sym.warn)} There\u2019s no \u201C${name}\u201D command.${near ? ` Did you mean ${cyan(near)}?` : ""} Type ${cyan("help")} for the list.`);
10878
+ }
10879
+ }
10880
+ });
10881
+ }
10882
+ print(` ${dim("See you soon. Type")} ${cyan("fullsendai")} ${dim("to come back.")}`);
10883
+ return 0;
10884
+ }
10885
+ async function guarded(work) {
10886
+ try {
10887
+ await work();
10888
+ } catch (error) {
10889
+ if (error instanceof Cancelled) print(` ${dim("Cancelled. Nothing changed.")}`);
10890
+ else print(` ${red(sym.fail)} ${error.message}`);
10891
+ }
10892
+ print();
10893
+ }
10894
+ async function home(api, origin, context) {
10895
+ const w = here();
10896
+ const state = loadState(w, origin);
10897
+ const ids = Object.keys(state.tools);
10898
+ print();
10899
+ print(` ${volt(bold("FullsendAI"))}${state.account?.email ? ` ${dim(sym.dot)} ${state.account.email}` : ""}`);
10900
+ if (!ids.length) {
10901
+ print();
10902
+ print(` No tools are connected. Type ${cyan("connect")} to start, or ${cyan("exit")} to leave.`);
10903
+ print();
10904
+ return;
10905
+ }
10906
+ const keys = [...new Set(ids.map((id) => state.tools[id].key))];
10907
+ const [models, infos] = await spin("Loading your tools\u2026", () => Promise.all([api.models(keys[0]), Promise.all(keys.map((secret) => api.key(secret)))]));
10908
+ if (models.ok) context.models = models.value.map((each) => each.id);
10909
+ const info = new Map(keys.map((secret, index) => [secret, infos[index]]));
10910
+ const first = infos.find((each) => each.ok);
10911
+ const balance = first?.ok ? first.value.balance : void 0;
10912
+ const allowance = first?.ok ? first.value.plan_allowance_remaining ?? 0 : 0;
10913
+ 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`) : ""}`);
10914
+ print();
10915
+ const width = Math.max(...ids.map((id) => (toolById(id)?.name ?? id).length)) + 2;
10916
+ const nameWidth = Math.min(26, Math.max(...ids.map((id) => modelName(models, state.tools[id].model).length)) + 2);
10917
+ for (const id of ids) {
10918
+ const connection = state.tools[id];
10919
+ const result = info.get(connection.key);
10920
+ const spend = result?.ok ? `$${result.value.usage.toFixed(2)} this month${connection.given ? " (shared key)" : ""}` : red(result?.error ?? "");
10921
+ print(` ${result?.ok ? green(sym.ok) : yellow(sym.warn)} ${bold((toolById(id)?.name ?? id).padEnd(width))}${modelName(models, connection.model).padEnd(nameWidth)}${dim(spend)}`);
10922
+ }
10923
+ const failovers = keys.reduce((sum, secret) => {
10924
+ const result = info.get(secret);
10925
+ return sum + (result?.ok ? result.value.failovers ?? 0 : 0);
10926
+ }, 0);
10927
+ if (failovers) print(` ${volt(sym.ok)} ${failovers === 1 ? "1 call" : `${failovers} calls`} this month kept working when a provider failed.`);
10928
+ await noticeChanges(api, origin, { yes: false, json: false, dryRun: false, keepKeys: false, show: false, fix: false }, false);
10929
+ print();
10930
+ 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} `))}`);
10931
+ print();
10932
+ }
10933
+ var modelName = (models, id) => models.ok ? models.value.find((each) => each.id === id)?.name ?? id : id;
10934
+ function promptLine(history, context) {
10935
+ return new Promise((resolve) => {
10936
+ const rl = createInterface({
10937
+ input: process.stdin,
10938
+ output: process.stdout,
10939
+ terminal: true,
10940
+ history: [...history],
10941
+ historySize: 200,
10942
+ removeHistoryDuplicates: true,
10943
+ completer: (line) => complete(line, context)
10944
+ });
10945
+ let settled = false;
10946
+ const finish = (value) => {
10947
+ if (settled) return;
10948
+ settled = true;
10949
+ rl.close();
10950
+ if (value === null) process.stdout.write("\n");
10951
+ resolve(value);
10952
+ };
10953
+ rl.on("line", (line) => finish(line));
10954
+ rl.on("close", () => finish(null));
10955
+ rl.on("SIGINT", () => {
10956
+ if (rl.line) {
10957
+ rl.write(null, { ctrl: true, name: "u" });
10958
+ return;
10959
+ }
10960
+ process.stdout.write(`
10961
+ ${dim("To leave, type exit or press Ctrl+D.")}
10962
+ `);
10963
+ rl.prompt();
10964
+ });
10965
+ rl.setPrompt(`${volt("fullsendai")} ${dim(sym.arrow)} `);
10966
+ rl.prompt();
10967
+ });
10968
+ }
10969
+ function shellHelp() {
10970
+ const width = Math.max(...SHELL_COMMANDS.map((command) => command.usage.length)) + 3;
10971
+ print();
10972
+ for (const command of SHELL_COMMANDS) print(` ${cyan(command.usage.padEnd(width))}${command.about}`);
10973
+ print();
10974
+ print(` ${dim("Tab completes commands, model IDs and tools. \u2191 and \u2193 go through what you typed. Ctrl+C cancels a step.")}`);
10975
+ }
10976
+ async function offerInstall(flags) {
10977
+ const w = here();
10978
+ if (!interactive || flags.yes || flags.json || onPath("fullsendai", w)) return;
10979
+ print();
10980
+ if (!await confirm(`Install the ${cyan("fullsendai")} command, so you can open all this any time by typing just ${cyan("fullsendai")}?`)) {
10981
+ print(` ${dim("Any time later:")} ${cyan("npm install -g fullsendai")}`);
10982
+ return;
10983
+ }
10984
+ const result = await spin("Installing the fullsendai command\u2026", () => npmInstall(w));
10985
+ if (result.ok) {
10986
+ print(` ${green(sym.ok)} Installed. Type ${cyan(bold("fullsendai"))} in any terminal to open it.`);
10987
+ return;
10988
+ }
10989
+ const denied = /EACCES|EPERM|permission denied/i.test(result.output);
10990
+ print(` ${yellow(sym.warn)} ${denied ? "npm needs admin rights to install commands on this computer." : "That didn\u2019t install."} Run this yourself:`);
10991
+ print(` ${cyan(w.platform === "win32" || !denied ? "npm install -g fullsendai" : "sudo npm install -g fullsendai")}`);
10992
+ }
10993
+ function npmInstall(w) {
10994
+ return new Promise((resolve) => {
10995
+ 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 });
10996
+ let output2 = "";
10997
+ child.stdout.on("data", (chunk) => output2 += chunk);
10998
+ child.stderr.on("data", (chunk) => output2 += chunk);
10999
+ child.on("error", (error) => resolve({ ok: false, output: error.message }));
11000
+ child.on("close", (code) => resolve({ ok: code === 0, output: output2 }));
11001
+ });
11002
+ }
10714
11003
  async function sync(api, origin, flags, quiet = false) {
10715
11004
  const w = here();
10716
11005
  const state = loadState(w, origin);
10717
11006
  const ids = Object.keys(state.tools);
10718
11007
  if (!quiet) header2("sync", "Brings each connected tool up to date with FullsendAI's models.");
10719
11008
  if (!ids.length) {
10720
- print(` No tools are connected. Run ${cyan("npx fullsendai connect")}.`);
11009
+ print(` No tools are connected. Run ${cyan(cmd("connect"))}.`);
10721
11010
  return output(flags, { synced: [], moved: [] }, 0);
10722
11011
  }
10723
11012
  const models = await spin("Fetching the models\u2026", () => api.models(state.tools[ids[0]].key));
@@ -10764,7 +11053,7 @@ async function noticeChanges(api, origin, flags, apply) {
10764
11053
  print(` ${yellow(sym.warn)} FullsendAI's models have changed since your tools were set up ${dim("(new or renamed models, abilities or prices)")}.`);
10765
11054
  const update = apply || interactive && !flags.json && await confirm("Update your tools now?");
10766
11055
  if (update) await sync(api, origin, flags, true);
10767
- else print(` Run ${cyan("npx fullsendai sync")} to update them.`);
11056
+ else print(` Run ${cyan(cmd("sync"))} to update them.`);
10768
11057
  }
10769
11058
  var uidOf = (models, id) => models.find((model2) => model2.id === id)?.uid;
10770
11059
  function fingerprint(models) {
@@ -10777,7 +11066,7 @@ async function doctor(api, origin, flags) {
10777
11066
  header2("doctor", "Checking each connected tool's settings, key and a real request.");
10778
11067
  const ids = Object.keys(state.tools);
10779
11068
  if (!ids.length) {
10780
- print(` No tools are connected. Run ${cyan("npx fullsendai connect")}.`);
11069
+ print(` No tools are connected. Run ${cyan(cmd("connect"))}.`);
10781
11070
  return output(flags, { problems: [] }, 0);
10782
11071
  }
10783
11072
  const problems = [];
@@ -10813,7 +11102,7 @@ async function doctor(api, origin, flags) {
10813
11102
  print();
10814
11103
  if (!open.length) print(` ${green("Everything works.")}`);
10815
11104
  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.`}`);
11105
+ else print(` ${yellow(`${open.length} problem${open.length > 1 ? "s" : ""}.`)} ${flags.fix ? "" : `Run ${cyan(cmd("doctor --fix"))} to repair settings.`}`);
10817
11106
  return output(flags, { problems }, open.length ? 1 : 0);
10818
11107
  }
10819
11108
  async function disconnect(api, origin, flags) {
@@ -10858,7 +11147,7 @@ function key(origin, id, flags) {
10858
11147
  if (!id || !connection) return fail(`Name a connected tool: ${Object.keys(state.tools).join(", ") || "(none connected)"}.`, flags);
10859
11148
  if (flags.show) print(connection.key);
10860
11149
  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.`);
11150
+ else print(` No clipboard here. Run ${cyan(cmd(`key ${id} --show`))} to print it.`);
10862
11151
  return output(flags, { tool: id }, 0);
10863
11152
  }
10864
11153
  function pickModel(models, wanted) {
@@ -10903,7 +11192,7 @@ function fileFor(tool, origin) {
10903
11192
  const reading = tool.read?.({ w: here(), origin, base: `${origin}/v1` });
10904
11193
  return reading?.file ?? "";
10905
11194
  }
10906
- var short = (path, home) => path.startsWith(home) ? `~${path.slice(home.length)}` : path;
11195
+ var short = (path, home2) => path.startsWith(home2) ? `~${path.slice(home2.length)}` : path;
10907
11196
  var jsonOut;
10908
11197
  function output(flags, value, code) {
10909
11198
  if (flags.json) jsonOut = value;
@@ -10921,10 +11210,11 @@ function help() {
10921
11210
  print(`
10922
11211
  ${volt(bold("FullsendAI"))} ${dim(`v${VERSION}`)} ${dim("\xB7")} connect your coding tools in one command
10923
11212
 
10924
- ${bold("Usage")} npx fullsendai <command> [options]
11213
+ ${bold("Usage")} fullsendai Open FullsendAI: your tools, spend and every command
11214
+ fullsendai <command> Run one command (npx fullsendai <command> works too)
10925
11215
 
10926
11216
  ${bold("Commands")}
10927
- connect Find your coding tools and connect them (default)
11217
+ connect Find your coding tools and connect them
10928
11218
  status What's connected, each tool's model and spend this month
10929
11219
  model [id] Switch every connected tool to another model
10930
11220
  doctor [--fix] Check settings, keys and a real request; --fix repairs settings
@@ -10964,7 +11254,7 @@ function parse4(argv) {
10964
11254
  else if (name === "--help" || name === "-h") args.unshift("help");
10965
11255
  else args.push(arg);
10966
11256
  }
10967
- const command = args.shift() ?? "connect";
11257
+ const command = args.shift();
10968
11258
  return { command, args, flags };
10969
11259
  }
10970
11260
  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.0",
3
+ "version": "0.3.0",
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",