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.
- package/README.md +13 -1
- package/dist/cli.mjs +332 -38
- 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.
|
|
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
|
|
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
|
-
|
|
10283
|
-
|
|
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
|
-
|
|
10338
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10385
|
-
|
|
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
|
-
|
|
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("
|
|
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:
|
|
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(
|
|
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:
|
|
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(`
|
|
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("
|
|
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(
|
|
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(
|
|
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:
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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(`
|
|
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,
|
|
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")}
|
|
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
|
|
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()
|
|
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.
|
|
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",
|