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