@tiens.nguyen/gu-cli 1.0.796 → 1.0.798
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/gonext_agent_chat.py +204 -23
- package/gu-cli.mjs +37 -2
- package/gu-repl.mjs +202 -178
- package/log-level.mjs +193 -0
- package/package.json +2 -1
- package/terminal-layout.mjs +49 -0
package/gu-repl.mjs
CHANGED
|
@@ -81,6 +81,8 @@ import {
|
|
|
81
81
|
updateRestartNote,
|
|
82
82
|
describeInstallFailure,
|
|
83
83
|
CLI_UPDATE_SHELL_COMMAND,
|
|
84
|
+
cliUpdatePlan,
|
|
85
|
+
installedVersionShortfall,
|
|
84
86
|
isFreshPublishMiss,
|
|
85
87
|
freshPublishNote,
|
|
86
88
|
lazyModuleNames,
|
|
@@ -139,6 +141,7 @@ import { readHooks } from "./hooks-config.mjs";
|
|
|
139
141
|
import { readSkillSource, sourceLabel, SKILL_SOURCE_KEY } from "./skill-source.mjs";
|
|
140
142
|
import { codingBannerLabel, isOllamaCloudUrl, SECRET_KEY_HINT } from "./coding-key-prompt.mjs";
|
|
141
143
|
import { questionEchoText, thoughtPrintSeq } from "./terminal-layout.mjs";
|
|
144
|
+
import { globalPrefixOf } from "./install-path.mjs";
|
|
142
145
|
import { embedNotice, embedState } from "./embed-model-command.mjs";
|
|
143
146
|
import {
|
|
144
147
|
MODELFALLBACK_PREFIX, fallbackAfterTurn, fallbackHistoryLine, fallbackNotice, keyMissingNotice,
|
|
@@ -588,13 +591,15 @@ if (cliVerdict.kind === "run") {
|
|
|
588
591
|
} else if (argv.includes("--help") || argv.includes("-h")) {
|
|
589
592
|
console.log(
|
|
590
593
|
"gu — terminal agent REPL for the current folder\n\n" +
|
|
591
|
-
"Usage: gu [-v|--verbose] [--license <pro token>]\n" +
|
|
594
|
+
"Usage: gu [-v|--verbose] [--log] [--license <pro token>]\n" +
|
|
592
595
|
" gu status # worker + a table of the running model servers\n" +
|
|
593
596
|
" gu start|stop|restart|logs # manage the background worker\n" +
|
|
594
597
|
" gu logs <port|pid> [-f] # tail a running model server's log\n" +
|
|
595
598
|
" gu doctor|login|set # models / account (same as `gu-cli …`)\n" +
|
|
596
599
|
" gu rag # is RAG working? tests the embedder end-to-end\n\n" +
|
|
597
600
|
" -v, --verbose show RAG index-file activity in the bottom bar (each shard written)\n" +
|
|
601
|
+
" --log log this run in detail (~/.gonext/worker.log). Without it only starts,\n" +
|
|
602
|
+
" stops and errors are logged. Ends when this gu closes.\n" +
|
|
598
603
|
" --license <token> pro license: a first install ASKS how this machine runs (hosted\n" +
|
|
599
604
|
" client / MLX host / private). Without it, a first install is\n" +
|
|
600
605
|
" private by default — the whole stack on this machine.\n" +
|
|
@@ -614,6 +619,17 @@ if (cliVerdict.kind === "run") {
|
|
|
614
619
|
// could not deliver, because agent LOG events stay on the worker's console and never enter
|
|
615
620
|
// the job stream. It now drives a real, streamable signal.
|
|
616
621
|
const REPL_VERBOSE = argv.includes("-v") || argv.includes("--verbose");
|
|
622
|
+
// --log (task #352): THIS RUN logs in detail. By default the worker writes only starts, stops and
|
|
623
|
+
// errors; a run started with --log takes a lease (its pid, in ~/.gonext/log-detail.json) that the
|
|
624
|
+
// worker reads, and gives it back on exit. Per run by the owner's rule — nothing is saved, and a
|
|
625
|
+
// run that is killed leaves a lease for a dead process, which is no lease.
|
|
626
|
+
const REPL_LOG = argv.includes("--log");
|
|
627
|
+
if (REPL_LOG) {
|
|
628
|
+
const ll = await import("./log-level.mjs");
|
|
629
|
+
await ll.takeLease().catch(() => {});
|
|
630
|
+
process.on("exit", () => ll.releaseLeaseSync());
|
|
631
|
+
console.log(dim(" --log: detailed logging is on for this run (~/.gonext/worker.log); back to minimal when gu closes."));
|
|
632
|
+
}
|
|
617
633
|
if (REPL_VERBOSE) {
|
|
618
634
|
process.env.GONEXT_REPL_VERBOSE = "1";
|
|
619
635
|
}
|
|
@@ -2446,10 +2462,10 @@ async function runUpdate() {
|
|
|
2446
2462
|
if (cli) rows.push({ id: "cli", label: `Update gu to ${cli}`, note: "takes effect after you restart gu" });
|
|
2447
2463
|
if (!rows.length) return;
|
|
2448
2464
|
console.log("");
|
|
2449
|
-
console.log(dim(`
|
|
2465
|
+
console.log(dim(` Update (${PICK_HINT}):`));
|
|
2450
2466
|
const idx = await pickFromList(rows.map((r) => r.label + dim(` · ${r.note}`)), 0);
|
|
2451
2467
|
if (idx < 0) {
|
|
2452
|
-
console.log(dim("
|
|
2468
|
+
console.log(dim(" (cancelled)\n"));
|
|
2453
2469
|
paintBar();
|
|
2454
2470
|
return;
|
|
2455
2471
|
}
|
|
@@ -2476,7 +2492,7 @@ async function runUpdate() {
|
|
|
2476
2492
|
r.stderr ? ` — ${String(r.stderr).trim().split("\n").slice(-1)[0]}` : ""}`);
|
|
2477
2493
|
}
|
|
2478
2494
|
});
|
|
2479
|
-
console.log(accent(`
|
|
2495
|
+
console.log(accent(` ✓ API updated to ${api}\n`));
|
|
2480
2496
|
availableUpdates = { ...availableUpdates, api: "", any: Boolean(availableUpdates.cli) };
|
|
2481
2497
|
} else {
|
|
2482
2498
|
await withPendingStatus(`updating gu to ${cli}`, async () => {
|
|
@@ -2490,8 +2506,11 @@ async function runUpdate() {
|
|
|
2490
2506
|
// stderr is captured rather than discarded so the reason can be shown. npm puts the
|
|
2491
2507
|
// useful line (EPERM, EACCES, ETARGET) there, and "it failed" without it leaves the
|
|
2492
2508
|
// user with nothing to act on — sudo and "close gu first" are different fixes.
|
|
2493
|
-
|
|
2494
|
-
|
|
2509
|
+
// Install into the prefix of the gu that is RUNNING, with the npm that matches this node —
|
|
2510
|
+
// not whichever npm happens to be first on PATH (that one may own a different copy).
|
|
2511
|
+
const plan = cliUpdatePlan({ prefix: globalPrefixOf(DIR), execPath: process.execPath, env: process.env });
|
|
2512
|
+
const install = () => spawnSync(plan.command,
|
|
2513
|
+
{ stdio: ["ignore", "ignore", "pipe"], shell: true, encoding: "utf8", env: plan.env });
|
|
2495
2514
|
let r = install();
|
|
2496
2515
|
// JUST PUBLISHED: the update check sees a new version the moment it lands, npm's cache and
|
|
2497
2516
|
// CDN a few minutes later — ETARGET in between. Wait once and retry before calling it a
|
|
@@ -2503,15 +2522,20 @@ async function runUpdate() {
|
|
|
2503
2522
|
}
|
|
2504
2523
|
const failed = describeInstallFailure(r);
|
|
2505
2524
|
if (failed) throw new Error(failed);
|
|
2525
|
+
// npm exiting 0 is not the outcome: read the version now on disk in THIS install.
|
|
2526
|
+
let onDisk = "";
|
|
2527
|
+
try { onDisk = JSON.parse(readFileSync(join(DIR, "package.json"), "utf8")).version ?? ""; } catch { /* unreadable: skip the check */ }
|
|
2528
|
+
const short = installedVersionShortfall(onDisk, cli);
|
|
2529
|
+
if (short) throw new Error(short);
|
|
2506
2530
|
});
|
|
2507
2531
|
// NOT "updated" — the files changed, THIS process did not. Claiming otherwise is the one
|
|
2508
2532
|
// way this feature could lie to someone.
|
|
2509
|
-
console.log(accent(`
|
|
2533
|
+
console.log(accent(` ✓ gu ${cli} installed`) +
|
|
2510
2534
|
dim(` — ${updateRestartNote()}\n`));
|
|
2511
2535
|
availableUpdates = { ...availableUpdates, cli: "", any: Boolean(availableUpdates.api) };
|
|
2512
2536
|
}
|
|
2513
2537
|
} catch (e) {
|
|
2514
|
-
console.log(red(`
|
|
2538
|
+
console.log(red(` update failed: ${e?.message ?? e}\n`));
|
|
2515
2539
|
}
|
|
2516
2540
|
paintBar();
|
|
2517
2541
|
}
|
|
@@ -4304,8 +4328,8 @@ if (process.stdin.isTTY) {
|
|
|
4304
4328
|
// PROCESS (reported: "PromptCancelled: cancelled … Node.js v22"). Cancel is caught here; any
|
|
4305
4329
|
// other failure is reported and the session goes on — a keypress must not kill the REPL.
|
|
4306
4330
|
replaceCodingKey().catch((e) => {
|
|
4307
|
-
if (wasPromptCancelled(e)) console.log(dim("
|
|
4308
|
-
else console.log(red(`
|
|
4331
|
+
if (wasPromptCancelled(e)) console.log(dim(" (cancelled)\n"));
|
|
4332
|
+
else console.log(red(` ctrl+k failed: ${e?.message ?? e}\n`));
|
|
4309
4333
|
paintBar();
|
|
4310
4334
|
});
|
|
4311
4335
|
return;
|
|
@@ -5653,10 +5677,10 @@ async function runModelDoctor() {
|
|
|
5653
5677
|
const dc = await import("./doctor-check.mjs");
|
|
5654
5678
|
const settings = await withPendingStatus("reading your model settings", fetchWorkerSettings);
|
|
5655
5679
|
if (!settings) {
|
|
5656
|
-
console.log(red("
|
|
5680
|
+
console.log(red(" couldn't reach the API to read your model settings.\n"));
|
|
5657
5681
|
return;
|
|
5658
5682
|
}
|
|
5659
|
-
console.log(dim("
|
|
5683
|
+
console.log(dim(" Asking each model to answer — the real request, not just a ping."));
|
|
5660
5684
|
const roles = dc.rolesFromSettings(settings);
|
|
5661
5685
|
const results = [];
|
|
5662
5686
|
for (const r of roles) {
|
|
@@ -5699,7 +5723,7 @@ async function reconfigureOllamaServer() {
|
|
|
5699
5723
|
|
|
5700
5724
|
const settings = await withPendingStatus("reading your model settings", fetchWorkerSettings);
|
|
5701
5725
|
if (!settings) {
|
|
5702
|
-
console.log(red("
|
|
5726
|
+
console.log(red(" couldn't reach the API to read your current settings.\n"));
|
|
5703
5727
|
return;
|
|
5704
5728
|
}
|
|
5705
5729
|
|
|
@@ -5728,7 +5752,7 @@ async function reconfigureOllamaServer() {
|
|
|
5728
5752
|
console.log(dim(` ${hint()}`));
|
|
5729
5753
|
// askSecret: masked, kept out of history; Esc throws PromptCancelled → /model is cancelled
|
|
5730
5754
|
// with nothing changed, which is what "skip all" means here.
|
|
5731
|
-
const k = (await askSecret(`
|
|
5755
|
+
const k = (await askSecret(` API key for ${host} (Enter for none): `)).trim();
|
|
5732
5756
|
typedKeys.set(host, k);
|
|
5733
5757
|
console.log(GUTTER + dim(ui.keyConfirmation(host, k)));
|
|
5734
5758
|
return k;
|
|
@@ -5744,7 +5768,7 @@ async function reconfigureOllamaServer() {
|
|
|
5744
5768
|
// instruction, leaving the reader unsure whether their own address needs it. It does not
|
|
5745
5769
|
// — normalizeOllamaUrl adds it — and displayUrl round-trips, so what is shown can be fed
|
|
5746
5770
|
// straight back as the default.
|
|
5747
|
-
console.log(dim(`
|
|
5771
|
+
console.log(dim(` ${label} — Enter keeps ${os.displayUrl(dflt)}`));
|
|
5748
5772
|
console.log(dim(` ${hint()}`));
|
|
5749
5773
|
// THE ROW MUST SAY WHICH SERVER IT IS ASKING FOR. It said a bare "server:" — chosen to
|
|
5750
5774
|
// fit the frozen row, which traded away the one thing the label was for. Two questions
|
|
@@ -5760,7 +5784,7 @@ async function reconfigureOllamaServer() {
|
|
|
5760
5784
|
// other. See urlCandidates; the load decides, not the heuristic.
|
|
5761
5785
|
const candidates = os.urlCandidates(typed || dflt);
|
|
5762
5786
|
if (!candidates.length) {
|
|
5763
|
-
console.log(dim("
|
|
5787
|
+
console.log(dim(" that is not a server address — try 192.168.1.40 or a full URL"));
|
|
5764
5788
|
continue;
|
|
5765
5789
|
}
|
|
5766
5790
|
let n = null;
|
|
@@ -5775,7 +5799,7 @@ async function reconfigureOllamaServer() {
|
|
|
5775
5799
|
try {
|
|
5776
5800
|
models = await load(cand, key);
|
|
5777
5801
|
n = cand;
|
|
5778
|
-
if (candidates.length > 1) console.log(dim(`
|
|
5802
|
+
if (candidates.length > 1) console.log(dim(` using ${os.displayUrl(cand.url)}`));
|
|
5779
5803
|
return;
|
|
5780
5804
|
} catch (err) {
|
|
5781
5805
|
lastErr = err;
|
|
@@ -5785,31 +5809,31 @@ async function reconfigureOllamaServer() {
|
|
|
5785
5809
|
await tryAll();
|
|
5786
5810
|
// Refused for want of credentials: ask now, whatever the address looked like, and list again.
|
|
5787
5811
|
if (!n && ui.needsKeyAfterStatus(lastErr?.httpStatus) && !key) {
|
|
5788
|
-
console.log(yellow(`
|
|
5812
|
+
console.log(yellow(` ${candidates[0].host} needs an API key to list its models.`));
|
|
5789
5813
|
typedKeys.delete(candidates[0].host);
|
|
5790
5814
|
key = await askKeyFor(candidates[0].host);
|
|
5791
5815
|
if (key) await tryAll();
|
|
5792
5816
|
}
|
|
5793
5817
|
if (!n && ui.needsKeyAfterStatus(lastErr?.httpStatus)) {
|
|
5794
|
-
console.log(red(`
|
|
5818
|
+
console.log(red(` ${candidates[0].host} refused the request (HTTP ${lastErr.httpStatus})${key ? " — that key was not accepted" : ""}.`));
|
|
5795
5819
|
continue;
|
|
5796
5820
|
}
|
|
5797
5821
|
if (!n) {
|
|
5798
5822
|
if (lastErr?.httpStatus === 404) {
|
|
5799
5823
|
// IT ANSWERED — it is simply not an Ollama (#43). "Couldn't reach … is Ollama listening"
|
|
5800
5824
|
// sent people to debug a server that was working.
|
|
5801
|
-
for (const l of am.describeNotOllama(candidates[0].host, role)) console.log(yellow(`
|
|
5825
|
+
for (const l of am.describeNotOllama(candidates[0].host, role)) console.log(yellow(` ${l}`));
|
|
5802
5826
|
continue;
|
|
5803
5827
|
}
|
|
5804
5828
|
// One address typed, one failure reported — naming every candidate would be two errors
|
|
5805
5829
|
// for one mistake, and neither would be the address they gave.
|
|
5806
|
-
console.log(red(`
|
|
5807
|
-
console.log(dim("
|
|
5830
|
+
console.log(red(` couldn't reach ${candidates[0].host}: ${os.networkReason(lastErr)}`));
|
|
5831
|
+
console.log(dim(" check the address, that Ollama is listening on the network, and /proxy."));
|
|
5808
5832
|
continue;
|
|
5809
5833
|
}
|
|
5810
5834
|
const usable = os.usableModelList(models);
|
|
5811
5835
|
if (!usable.ok) {
|
|
5812
|
-
for (const l of os.describeUnusableList(usable.why, n.host)) console.log(dim(`
|
|
5836
|
+
for (const l of os.describeUnusableList(usable.why, n.host)) console.log(dim(` ${l}`));
|
|
5813
5837
|
continue;
|
|
5814
5838
|
}
|
|
5815
5839
|
if (require) {
|
|
@@ -5818,13 +5842,13 @@ async function reconfigureOllamaServer() {
|
|
|
5818
5842
|
// rather than the server.
|
|
5819
5843
|
const need = os.checkRequired(models);
|
|
5820
5844
|
if (!need.ok) {
|
|
5821
|
-
for (const l of os.describeNotReady(n.host, need.missing)) console.log(yellow(`
|
|
5822
|
-
console.log(dim("
|
|
5845
|
+
for (const l of os.describeNotReady(n.host, need.missing)) console.log(yellow(` ${l}`));
|
|
5846
|
+
console.log(dim(" (or type s to skip — you can set it later with /model)"));
|
|
5823
5847
|
continue;
|
|
5824
5848
|
}
|
|
5825
|
-
console.log(dim(`
|
|
5849
|
+
console.log(dim(` ✓ ${n.host} has the ${os.REQUIRED_AGENT_PREFIX} family and the embedder`));
|
|
5826
5850
|
}
|
|
5827
|
-
console.log(dim(`
|
|
5851
|
+
console.log(dim(` found ${models.length} model${models.length === 1 ? "" : "s"} on ${n.host}`));
|
|
5828
5852
|
return { normalized: n, models };
|
|
5829
5853
|
}
|
|
5830
5854
|
return null;
|
|
@@ -5841,11 +5865,11 @@ async function reconfigureOllamaServer() {
|
|
|
5841
5865
|
let agent = null;
|
|
5842
5866
|
if (agentOnProvider) {
|
|
5843
5867
|
const host = hostOf(String(settings.agentModelUrl ?? ""));
|
|
5844
|
-
console.log(dim(`
|
|
5845
|
-
console.log(dim("
|
|
5868
|
+
console.log(dim(` The agent model is on ${host} (OpenAI-compatible) — kept as it is; change it with /agent-model.`));
|
|
5869
|
+
console.log(dim(" Enter keeps the current address."));
|
|
5846
5870
|
} else {
|
|
5847
|
-
console.log(dim("
|
|
5848
|
-
console.log(dim("
|
|
5871
|
+
console.log(dim(" TWO servers may be used: the agent + embedder on one, the coder on another."));
|
|
5872
|
+
console.log(dim(" Enter keeps the current address."));
|
|
5849
5873
|
agent = await askServer("Agent + embedding server", curAgent, {
|
|
5850
5874
|
require: true,
|
|
5851
5875
|
short: "agent + embed server",
|
|
@@ -5865,7 +5889,7 @@ async function reconfigureOllamaServer() {
|
|
|
5865
5889
|
}
|
|
5866
5890
|
|
|
5867
5891
|
if (!agent && !coder) {
|
|
5868
|
-
console.log(dim("
|
|
5892
|
+
console.log(dim(" nothing set — models stay as they are.\n"));
|
|
5869
5893
|
return;
|
|
5870
5894
|
}
|
|
5871
5895
|
|
|
@@ -5888,13 +5912,13 @@ async function reconfigureOllamaServer() {
|
|
|
5888
5912
|
const size = am.formatSize(m.sizeBytes);
|
|
5889
5913
|
return `${m.model}${size ? dim(` ${size}`) : ""}${m.model === agentDefault ? dim(" (current)") : ""}`;
|
|
5890
5914
|
});
|
|
5891
|
-
console.log(dim(`
|
|
5915
|
+
console.log(dim(` AGENT model — routes every turn, writes the plain replies (${PICK_HINT}):`));
|
|
5892
5916
|
const pick = await pickFromList(
|
|
5893
5917
|
labels,
|
|
5894
5918
|
Math.max(0, cands.findIndex((m) => m.model === agentDefault))
|
|
5895
5919
|
);
|
|
5896
5920
|
if (pick < 0) {
|
|
5897
|
-
console.log(dim("
|
|
5921
|
+
console.log(dim(" (cancelled — nothing changed)\n"));
|
|
5898
5922
|
return;
|
|
5899
5923
|
}
|
|
5900
5924
|
chat = cands[pick].model;
|
|
@@ -5913,10 +5937,10 @@ async function reconfigureOllamaServer() {
|
|
|
5913
5937
|
const kind = am.isLikelyEmbedder(m) ? dim(" · embedder") : "";
|
|
5914
5938
|
return `${m.model}${size ? dim(` ${size}`) : ""}${kind}${m.model === roles.code ? dim(" (current)") : ""}`;
|
|
5915
5939
|
});
|
|
5916
|
-
console.log(dim(`
|
|
5940
|
+
console.log(dim(` CODING model — writes the code; worth waiting for (${PICK_HINT}):`));
|
|
5917
5941
|
const idx = await pickFromList(labels, Math.max(0, coder.models.findIndex((m) => m.model === roles.code)));
|
|
5918
5942
|
if (idx < 0) {
|
|
5919
|
-
console.log(dim("
|
|
5943
|
+
console.log(dim(" (cancelled — nothing changed)\n"));
|
|
5920
5944
|
return;
|
|
5921
5945
|
}
|
|
5922
5946
|
code = coder.models[idx].model;
|
|
@@ -5931,7 +5955,7 @@ async function reconfigureOllamaServer() {
|
|
|
5931
5955
|
.flatMap((b) => (Array.isArray(b.models) ? b.models : []))
|
|
5932
5956
|
);
|
|
5933
5957
|
console.log(
|
|
5934
|
-
dim(`
|
|
5958
|
+
dim(` Also offer these in /model? (up to ${MAX_ALT_CODERS} · ${PICK_MANY_HINT}):`)
|
|
5935
5959
|
);
|
|
5936
5960
|
const ticks = await pickManyFromList(
|
|
5937
5961
|
rest.map((m) => `${m.model}${am.formatSize(m.sizeBytes) ? dim(` ${am.formatSize(m.sizeBytes)}`) : ""}`),
|
|
@@ -5940,7 +5964,7 @@ async function reconfigureOllamaServer() {
|
|
|
5940
5964
|
);
|
|
5941
5965
|
// null is Escape and cancels the command; an EMPTY list is a legitimate "offer none".
|
|
5942
5966
|
if (ticks === null) {
|
|
5943
|
-
console.log(dim("
|
|
5967
|
+
console.log(dim(" (cancelled — nothing changed)\n"));
|
|
5944
5968
|
return;
|
|
5945
5969
|
}
|
|
5946
5970
|
extras = ticks.map((i) => rest[i].model);
|
|
@@ -5986,7 +6010,7 @@ async function reconfigureOllamaServer() {
|
|
|
5986
6010
|
md.saveWorkerSettings(apiBase, workerKey, patch)
|
|
5987
6011
|
);
|
|
5988
6012
|
if (!saved) {
|
|
5989
|
-
console.log(red("
|
|
6013
|
+
console.log(red(" couldn't save that to this machine — nothing changed."));
|
|
5990
6014
|
console.log(GUTTER + dim("if this machine's local API is old, restart it with `gu api restart`.\n"));
|
|
5991
6015
|
return;
|
|
5992
6016
|
}
|
|
@@ -6006,13 +6030,13 @@ async function reconfigureOllamaServer() {
|
|
|
6006
6030
|
const url = used.find((u) => hostnameOf(u) === host);
|
|
6007
6031
|
const found = ps.providerRowForUrl(ps.existingBackendRows(fresh ?? {}), url);
|
|
6008
6032
|
if (!found) {
|
|
6009
|
-
console.log(yellow(`
|
|
6033
|
+
console.log(yellow(` no server row holds ${host} yet — add its key with ctrl+k in /model.`));
|
|
6010
6034
|
continue;
|
|
6011
6035
|
}
|
|
6012
6036
|
const ok = await withPendingStatus("saving the key", () => addCodingKey(host, k, found.id));
|
|
6013
6037
|
console.log(ok
|
|
6014
6038
|
? GUTTER + dim(`✓ key stored for ${host} — the server's default key`)
|
|
6015
|
-
: red(`
|
|
6039
|
+
: red(` couldn't store the key for ${host} — add it with ctrl+k.`));
|
|
6016
6040
|
}
|
|
6017
6041
|
}
|
|
6018
6042
|
}
|
|
@@ -6030,11 +6054,11 @@ async function reconfigureOllamaServer() {
|
|
|
6030
6054
|
for (const u of cloudUrls) {
|
|
6031
6055
|
const found = ps.providerRowForUrl(ps.existingBackendRows(fresh ?? {}), u);
|
|
6032
6056
|
if (found && !am.boundKeyFor(keys, found.id)) {
|
|
6033
|
-
console.log(yellow(`
|
|
6057
|
+
console.log(yellow(` ${hostOf(u)} is the Ollama cloud — it needs its own API key (the model list worked without one).`));
|
|
6034
6058
|
// The shared key prompt (task #245); the server's first key becomes its default.
|
|
6035
6059
|
const { name, secret } = await askServerKey({ host: hostOf(u) });
|
|
6036
6060
|
if (secret && !(await withPendingStatus("saving the key", () => addCodingKey(name, secret, found.id)))) {
|
|
6037
|
-
console.log(red("
|
|
6061
|
+
console.log(red(" couldn't save the key — add it with ctrl+k.\n"));
|
|
6038
6062
|
}
|
|
6039
6063
|
}
|
|
6040
6064
|
}
|
|
@@ -6070,7 +6094,7 @@ async function reconfigureOllamaServer() {
|
|
|
6070
6094
|
if (g !== null) globalOutputTokens = g;
|
|
6071
6095
|
}
|
|
6072
6096
|
paintBar(); // …and show all of it now, not at the next keystroke
|
|
6073
|
-
console.log(accent("
|
|
6097
|
+
console.log(accent(" ✓ models saved"));
|
|
6074
6098
|
if (agent) {
|
|
6075
6099
|
console.log(dim(` chat ${chat} @ ${agent.normalized.host}`));
|
|
6076
6100
|
console.log(dim(` RAG ${os.defaultEmbedModel?.(agent.models) ?? os.REQUIRED_EMBED_MODEL} @ ${agent.normalized.host}`));
|
|
@@ -6088,9 +6112,9 @@ async function reconfigureOllamaServer() {
|
|
|
6088
6112
|
// falsely scream "changed" while the seeded value is exactly what settings now hold.
|
|
6089
6113
|
const seededEmbed = agent ? (os.defaultEmbedModel?.(agent.models) ?? os.REQUIRED_EMBED_MODEL) : "";
|
|
6090
6114
|
if (agent && String(settings.ragEmbedModel ?? "") !== seededEmbed) {
|
|
6091
|
-
console.log(dim("
|
|
6115
|
+
console.log(dim(" the embedder changed — existing RAG indexes need rebuilding (/rag)."));
|
|
6092
6116
|
}
|
|
6093
|
-
console.log(dim("
|
|
6117
|
+
console.log(dim(" takes effect on your next question.\n"));
|
|
6094
6118
|
}
|
|
6095
6119
|
|
|
6096
6120
|
// ---------- fetch the ready-to-run agent payload from user settings ----------
|
|
@@ -6141,7 +6165,7 @@ async function reconfigureOllamaServer() {
|
|
|
6141
6165
|
*/
|
|
6142
6166
|
async function pickExtraModels(rest, preChecked = []) {
|
|
6143
6167
|
if (rest.length === 0) return [];
|
|
6144
|
-
console.log(dim(`
|
|
6168
|
+
console.log(dim(` Also offer these in /model? (up to ${MAX_ALT_CODERS} · ${PICK_MANY_HINT}):`));
|
|
6145
6169
|
const already = new Set(preChecked);
|
|
6146
6170
|
const ticks = await pickManyFromList(
|
|
6147
6171
|
rest,
|
|
@@ -6187,7 +6211,7 @@ async function chooseCodingModelFrom(ps, all, question, current) {
|
|
|
6187
6211
|
const idx = await pickFromList(rows, at >= 0 ? at : 0);
|
|
6188
6212
|
if (idx < 0) return null;
|
|
6189
6213
|
if (idx === list.length) {
|
|
6190
|
-
const typed = (await ask("
|
|
6214
|
+
const typed = (await ask(" model name: ")).trim();
|
|
6191
6215
|
return typed || null;
|
|
6192
6216
|
}
|
|
6193
6217
|
if (idx === list.length + 1) {
|
|
@@ -6215,11 +6239,11 @@ async function chooseCodingModelFrom(ps, all, question, current) {
|
|
|
6215
6239
|
async function chooseFromModelList(all, question, current = 0) {
|
|
6216
6240
|
const rows = [...all, "type a model name…"];
|
|
6217
6241
|
const at = typeof current === "string" ? all.indexOf(current) : Number(current) || 0;
|
|
6218
|
-
console.log(dim(`
|
|
6242
|
+
console.log(dim(` ${question} (${PICK_HINT})`));
|
|
6219
6243
|
const idx = await pickFromList(rows, at >= 0 ? at : 0);
|
|
6220
6244
|
if (idx < 0) return null;
|
|
6221
6245
|
if (idx === all.length) {
|
|
6222
|
-
const typed = (await ask("
|
|
6246
|
+
const typed = (await ask(" model name: ")).trim();
|
|
6223
6247
|
return typed || null;
|
|
6224
6248
|
}
|
|
6225
6249
|
return all[idx];
|
|
@@ -6229,7 +6253,7 @@ async function manageCodingProviders() {
|
|
|
6229
6253
|
const ps = await import("./coding-provider-setup.mjs");
|
|
6230
6254
|
const settings = await withPendingStatus("reading your providers", fetchWorkerSettings);
|
|
6231
6255
|
if (!settings) {
|
|
6232
|
-
console.log(red("
|
|
6256
|
+
console.log(red(" couldn't reach the API to read your current settings.\n"));
|
|
6233
6257
|
return;
|
|
6234
6258
|
}
|
|
6235
6259
|
const rows = ps.existingBackendRows(settings);
|
|
@@ -6246,10 +6270,10 @@ async function manageCodingProviders() {
|
|
|
6246
6270
|
);
|
|
6247
6271
|
const ADD = labels.length;
|
|
6248
6272
|
labels.push("add another provider — Claude API, OpenAI, Kimi…");
|
|
6249
|
-
console.log(dim(`
|
|
6273
|
+
console.log(dim(` Providers — pick one to change it, or add another (${PICK_HINT}):`));
|
|
6250
6274
|
const idx = await pickFromList(labels, 0);
|
|
6251
6275
|
if (idx < 0) {
|
|
6252
|
-
console.log(dim("
|
|
6276
|
+
console.log(dim(" (cancelled)\n"));
|
|
6253
6277
|
return;
|
|
6254
6278
|
}
|
|
6255
6279
|
if (idx === ADD) {
|
|
@@ -6264,7 +6288,7 @@ async function manageCodingProviders() {
|
|
|
6264
6288
|
1
|
|
6265
6289
|
);
|
|
6266
6290
|
if (action <= 0) {
|
|
6267
|
-
console.log(dim("
|
|
6291
|
+
console.log(dim(" (nothing changed)\n"));
|
|
6268
6292
|
return;
|
|
6269
6293
|
}
|
|
6270
6294
|
if (action === 1) await changeProviderModel(ps, rows, p);
|
|
@@ -6323,12 +6347,12 @@ async function changeProviderModel(ps, rows, p) {
|
|
|
6323
6347
|
if (models.length) {
|
|
6324
6348
|
model = await chooseCodingModelFrom(ps, models, `Which model should ${p.host} use?`, p.model);
|
|
6325
6349
|
if (model === null) {
|
|
6326
|
-
console.log(dim("
|
|
6350
|
+
console.log(dim(" (nothing changed)\n"));
|
|
6327
6351
|
return;
|
|
6328
6352
|
}
|
|
6329
6353
|
} else {
|
|
6330
6354
|
console.log(GUTTER + dim(`Enter keeps ${p.model || "it unset"}`));
|
|
6331
|
-
model = (await ask("
|
|
6355
|
+
model = (await ask(" model name: ")).trim();
|
|
6332
6356
|
}
|
|
6333
6357
|
if (!model) {
|
|
6334
6358
|
console.log(GUTTER + dim("(nothing changed)\n"));
|
|
@@ -6352,7 +6376,7 @@ async function changeProviderModel(ps, rows, p) {
|
|
|
6352
6376
|
})
|
|
6353
6377
|
);
|
|
6354
6378
|
if (!saved) {
|
|
6355
|
-
console.log(red("
|
|
6379
|
+
console.log(red(" couldn't save that — nothing changed.\n"));
|
|
6356
6380
|
return;
|
|
6357
6381
|
}
|
|
6358
6382
|
// THE FOLDER'S PICK NAMES THE OLD MODEL. Leaving it would send a name this provider no longer
|
|
@@ -6366,14 +6390,14 @@ async function changeProviderModel(ps, rows, p) {
|
|
|
6366
6390
|
await syncActiveCoderWhere();
|
|
6367
6391
|
console.log(GUTTER + dim("this folder's pick named the old model — cleared it."));
|
|
6368
6392
|
}
|
|
6369
|
-
console.log(accent(`
|
|
6393
|
+
console.log(accent(` ✓ ${p.host} → ${model}`) + dim(" · pick it in /model\n"));
|
|
6370
6394
|
}
|
|
6371
6395
|
|
|
6372
6396
|
/** Point a provider at a different URL, keeping its id so saved picks still resolve. */
|
|
6373
6397
|
async function changeProviderUrl(ps, rows, p) {
|
|
6374
6398
|
const md = await import("./model-doctor.mjs");
|
|
6375
6399
|
console.log("\n" + GUTTER + dim(`Enter keeps ${p.url}`));
|
|
6376
|
-
const typed = (await ask("
|
|
6400
|
+
const typed = (await ask(" provider URL: ")).trim();
|
|
6377
6401
|
if (!typed || typed === p.url) {
|
|
6378
6402
|
console.log(GUTTER + dim("(nothing changed)\n"));
|
|
6379
6403
|
return;
|
|
@@ -6398,12 +6422,12 @@ async function changeProviderUrl(ps, rows, p) {
|
|
|
6398
6422
|
})
|
|
6399
6423
|
);
|
|
6400
6424
|
if (!saved) {
|
|
6401
|
-
console.log(red("
|
|
6425
|
+
console.log(red(" couldn't save that — nothing changed.\n"));
|
|
6402
6426
|
return;
|
|
6403
6427
|
}
|
|
6404
6428
|
// The KEY still belongs to this row, because the row id did not change — which is the whole
|
|
6405
6429
|
// reason an id is not derived from the URL.
|
|
6406
|
-
console.log(accent(`
|
|
6430
|
+
console.log(accent(` ✓ ${p.host} → ${r.host}`) + dim(" · its key still applies\n"));
|
|
6407
6431
|
}
|
|
6408
6432
|
|
|
6409
6433
|
/** Remove a provider, and the keys that would be stranded by its removal. */
|
|
@@ -6436,7 +6460,7 @@ async function removeCodingProvider(ps, rows, p) {
|
|
|
6436
6460
|
})
|
|
6437
6461
|
);
|
|
6438
6462
|
if (!saved) {
|
|
6439
|
-
console.log(red("
|
|
6463
|
+
console.log(red(" couldn't save that — nothing was removed.\n"));
|
|
6440
6464
|
return;
|
|
6441
6465
|
}
|
|
6442
6466
|
// THE ROW GOES FIRST. If a key delete fails afterwards the key is merely orphaned, which is
|
|
@@ -6460,7 +6484,7 @@ async function removeCodingProvider(ps, rows, p) {
|
|
|
6460
6484
|
console.log(GUTTER + dim("this folder was using it — cleared the pick.\n"));
|
|
6461
6485
|
}
|
|
6462
6486
|
console.log(
|
|
6463
|
-
accent(`
|
|
6487
|
+
accent(` ✓ removed ${p.host}`) +
|
|
6464
6488
|
(stranded.length ? dim(` · ${deleted} key${deleted === 1 ? "" : "s"} deleted`) : "") +
|
|
6465
6489
|
"\n"
|
|
6466
6490
|
);
|
|
@@ -6501,7 +6525,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6501
6525
|
|
|
6502
6526
|
const settings = await withPendingStatus("reading your model settings", fetchWorkerSettings);
|
|
6503
6527
|
if (!settings) {
|
|
6504
|
-
console.log(red("
|
|
6528
|
+
console.log(red(" couldn't reach the API to read your current settings.\n"));
|
|
6505
6529
|
return;
|
|
6506
6530
|
}
|
|
6507
6531
|
// EVERY ROW THAT IS ALREADY THERE, including the synthesized one a pre-#123 account keeps in
|
|
@@ -6527,10 +6551,10 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6527
6551
|
// without Claude, which serves no embeddings).
|
|
6528
6552
|
const presets = ps.providerPresets(role);
|
|
6529
6553
|
const rowsShown = [...presets.map((p) => `${p.name}${dim(` · ${p.url}`)}`), "another URL…"];
|
|
6530
|
-
console.log(dim(`
|
|
6554
|
+
console.log(dim(` Which provider? (${PICK_HINT})`));
|
|
6531
6555
|
const pi = await pickFromList(rowsShown, 0);
|
|
6532
6556
|
if (pi < 0) {
|
|
6533
|
-
console.log(dim("
|
|
6557
|
+
console.log(dim(" (nothing saved)\n"));
|
|
6534
6558
|
return;
|
|
6535
6559
|
}
|
|
6536
6560
|
if (pi < presets.length) target = as.serverTarget("openai", presets[pi].url);
|
|
@@ -6538,12 +6562,12 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6538
6562
|
if (!target && !presetUrl) {
|
|
6539
6563
|
const dflt = as.defaultServerUrl(role, dialect, settings, existing);
|
|
6540
6564
|
console.log(dim(dialect === "ollama"
|
|
6541
|
-
? "
|
|
6542
|
-
: "
|
|
6565
|
+
? " An Ollama server: this machine, a box on your network, or https://ollama.com (needs a key)."
|
|
6566
|
+
: " Any OpenAI-compatible endpoint: OpenRouter, Together, vLLM, LM Studio, …"));
|
|
6543
6567
|
// The address goes ABOVE, in full, and Enter's meaning is said out loud (46d1524e).
|
|
6544
6568
|
console.log(dim(dflt ? ` Enter uses ${dflt} · Esc cancels\n` : " Esc cancels.\n"));
|
|
6545
6569
|
for (let attempt = 0; attempt < 3 && !target; attempt++) {
|
|
6546
|
-
const typed = await ask("
|
|
6570
|
+
const typed = await ask(" server URL: ");
|
|
6547
6571
|
if (typed === null || as.isCancelWord(typed)) break;
|
|
6548
6572
|
const raw = typed.trim() || dflt;
|
|
6549
6573
|
if (!raw) break;
|
|
@@ -6560,7 +6584,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6560
6584
|
}
|
|
6561
6585
|
}
|
|
6562
6586
|
if (!target) {
|
|
6563
|
-
console.log(dim("
|
|
6587
|
+
console.log(dim(" (nothing saved)\n"));
|
|
6564
6588
|
return;
|
|
6565
6589
|
}
|
|
6566
6590
|
const host = target.host;
|
|
@@ -6582,7 +6606,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6582
6606
|
// THE ROW CAP BEFORE ANY QUESTION about a new server: exceeding it is a silent 200 with no row,
|
|
6583
6607
|
// and asking for a key and a model first would be the worst order to discover that in.
|
|
6584
6608
|
if (!found && existing.length >= ps.MAX_BACKENDS) {
|
|
6585
|
-
console.log(yellow(`
|
|
6609
|
+
console.log(yellow(` ${ps.MAX_BACKENDS} servers is the limit — remove one in /model → manage providers first.\n`));
|
|
6586
6610
|
return;
|
|
6587
6611
|
}
|
|
6588
6612
|
const need = as.keyRequirement(target);
|
|
@@ -6627,7 +6651,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6627
6651
|
let probe = await list();
|
|
6628
6652
|
// THE STORED KEY WAS REFUSED — ask for the right one once, rather than saving nothing.
|
|
6629
6653
|
if ((probe.status === 401 || probe.status === 403) && !typedKey && key) {
|
|
6630
|
-
console.log(yellow(`
|
|
6654
|
+
console.log(yellow(` ${host} refused the stored key (HTTP ${probe.status}) — enter the right one.`));
|
|
6631
6655
|
if (!(await askKey()) || !key) return;
|
|
6632
6656
|
probe = await list();
|
|
6633
6657
|
}
|
|
@@ -6662,11 +6686,11 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6662
6686
|
? await chooseCodingModelFrom(ps, models, R.question, 0)
|
|
6663
6687
|
: await chooseFromModelList(models, R.question, Math.max(0, models.indexOf(current)));
|
|
6664
6688
|
if (model === null) {
|
|
6665
|
-
console.log(dim("
|
|
6689
|
+
console.log(dim(" (cancelled — nothing was saved)\n"));
|
|
6666
6690
|
return;
|
|
6667
6691
|
}
|
|
6668
6692
|
} else {
|
|
6669
|
-
model = String((await ask("
|
|
6693
|
+
model = String((await ask(" model name: ")) ?? "").trim();
|
|
6670
6694
|
}
|
|
6671
6695
|
if (!model) {
|
|
6672
6696
|
console.log(GUTTER + dim("(cancelled — nothing was saved)\n"));
|
|
@@ -6683,7 +6707,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6683
6707
|
if (!sw.changed) return;
|
|
6684
6708
|
plan = sw.plan;
|
|
6685
6709
|
} else if (as.serverTarget(target.dialect, String(settings.ragEmbedUrl ?? "")).root === target.root && !typedKey) {
|
|
6686
|
-
console.log(dim(`
|
|
6710
|
+
console.log(dim(` already embedding with ${model} on ${host} — nothing changed.\n`));
|
|
6687
6711
|
return;
|
|
6688
6712
|
}
|
|
6689
6713
|
}
|
|
@@ -6712,7 +6736,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6712
6736
|
} else {
|
|
6713
6737
|
const r = as.rowForSave(role, { existing: found.row, model });
|
|
6714
6738
|
if (r.full) {
|
|
6715
|
-
console.log(yellow(`
|
|
6739
|
+
console.log(yellow(` ${host} already offers ${ps.MAX_MODELS_PER_SERVER} models — remove one in /model → manage providers first.\n`));
|
|
6716
6740
|
return;
|
|
6717
6741
|
}
|
|
6718
6742
|
if (r.changed) rows = existing.map((b) => (b === found.row ? r.row : b));
|
|
@@ -6720,7 +6744,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6720
6744
|
if (rows !== existing) {
|
|
6721
6745
|
const ok = await withPendingStatus(`saving ${host}`, () => md.saveWorkerSettings(apiBase, workerKey, { agentCodingBackends: rows }));
|
|
6722
6746
|
if (!ok) {
|
|
6723
|
-
console.log(red(`
|
|
6747
|
+
console.log(red(` couldn't save ${host} — nothing changed.`));
|
|
6724
6748
|
console.log(GUTTER + dim("if this machine's local API is old, restart it with `gu api restart`.\n"));
|
|
6725
6749
|
return;
|
|
6726
6750
|
}
|
|
@@ -6729,7 +6753,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6729
6753
|
if (typedKey) {
|
|
6730
6754
|
const id = await withPendingStatus("saving the key", () => addCodingKey(keyName, key, rowId));
|
|
6731
6755
|
if (!id) {
|
|
6732
|
-
console.log(yellow(`
|
|
6756
|
+
console.log(yellow(` ${host} was saved, but its key was not — nothing else changed.`));
|
|
6733
6757
|
console.log(GUTTER + dim("try again, or add the key with ctrl+k.\n"));
|
|
6734
6758
|
return;
|
|
6735
6759
|
}
|
|
@@ -6747,7 +6771,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6747
6771
|
console.log(GUTTER + dim("the banner shows the new model next time gu starts.\n"));
|
|
6748
6772
|
} else if (rows !== existing || typedKey) {
|
|
6749
6773
|
// The SERVER landed — say so, or the user re-enters a key that is already stored.
|
|
6750
|
-
console.log(yellow(`
|
|
6774
|
+
console.log(yellow(` ${host} was saved, but the agent model was not pointed at it.`));
|
|
6751
6775
|
console.log(GUTTER + dim("run /agent-model to finish, or check the API is reachable.\n"));
|
|
6752
6776
|
}
|
|
6753
6777
|
return;
|
|
@@ -6760,7 +6784,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6760
6784
|
if (!(await saveEmbedSettings(fields))) return;
|
|
6761
6785
|
if (plan) announceEmbedSaved({ chosen: model, plan, url: target.roleUrl("embed") });
|
|
6762
6786
|
else {
|
|
6763
|
-
console.log(accent(`
|
|
6787
|
+
console.log(accent(` ✓ embedding server → ${host}`));
|
|
6764
6788
|
console.log(dim(` ${model} did not change, so no knowledge base rebuilds — the same model just answers from ${host}.\n`));
|
|
6765
6789
|
}
|
|
6766
6790
|
return;
|
|
@@ -6788,7 +6812,7 @@ async function useCoderInThisFolder({ ps, rows, beforeChoices, url, host, locali
|
|
|
6788
6812
|
(c) => c.model === model && (c.backendId ? c.backendId === backendId : sameHost(c.url, url))
|
|
6789
6813
|
);
|
|
6790
6814
|
if (!offered) {
|
|
6791
|
-
console.log(yellow(`
|
|
6815
|
+
console.log(yellow(` ${host} was saved, but this API is not offering ${model} yet.`));
|
|
6792
6816
|
console.log(GUTTER + dim("run /model — if it is missing there too, the API may be older than"));
|
|
6793
6817
|
console.log(GUTTER + dim("this feature; restart it with `gu api restart`.\n"));
|
|
6794
6818
|
return;
|
|
@@ -6802,7 +6826,7 @@ async function useCoderInThisFolder({ ps, rows, beforeChoices, url, host, locali
|
|
|
6802
6826
|
])
|
|
6803
6827
|
);
|
|
6804
6828
|
if (lost.length) {
|
|
6805
|
-
console.log(yellow(`
|
|
6829
|
+
console.log(yellow(` ⚠ this API dropped ${lost.length === 1 ? "a provider" : `${lost.length} providers`} when saving:`));
|
|
6806
6830
|
for (const u of lost) console.log(GUTTER + dim(` ${u}`));
|
|
6807
6831
|
if (ps.apiKeysBackendsByKind(after?.codingChoices)) {
|
|
6808
6832
|
console.log(GUTTER + dim("this API identifies a backend by its KIND, so it holds ONE"));
|
|
@@ -6826,8 +6850,8 @@ async function useCoderInThisFolder({ ps, rows, beforeChoices, url, host, locali
|
|
|
6826
6850
|
// The key question for this coder is answered — leaving the flag set would warn about a key
|
|
6827
6851
|
// that was just entered, which reads as the save having failed.
|
|
6828
6852
|
codingKeyMissingKnown = false;
|
|
6829
|
-
console.log(accent(`
|
|
6830
|
-
if (key) console.log(dim(`
|
|
6853
|
+
console.log(accent(` ✓ coding model → ${model}`) + dim(` · ${host}`));
|
|
6854
|
+
if (key) console.log(dim(` API key ${keyName || "saved"} ····${key.slice(-4)} · ${host}'s key\n`));
|
|
6831
6855
|
// A HOSTED SERVER NEEDS ITS WINDOW SET: with nothing set gu assumes 24,000 prompt tokens and
|
|
6832
6856
|
// compacts there — on a large hosted model that throws history away every turn.
|
|
6833
6857
|
if (ps.shouldMentionContextWindow(locality)) {
|
|
@@ -6847,7 +6871,7 @@ async function chooseModel() {
|
|
|
6847
6871
|
fetchAgentPayload([{ role: "user", content: "ping" }])
|
|
6848
6872
|
);
|
|
6849
6873
|
} catch (err) {
|
|
6850
|
-
console.log(red(`
|
|
6874
|
+
console.log(red(` ${describeApiFailure(err, "couldn't load models")}\n`));
|
|
6851
6875
|
return;
|
|
6852
6876
|
}
|
|
6853
6877
|
const allowed = Array.isArray(probe?.codingAllowed) ? probe.codingAllowed : [];
|
|
@@ -6868,7 +6892,7 @@ async function chooseModel() {
|
|
|
6868
6892
|
// choices while still occupying its URL, so sending this branch straight to the ADD flow
|
|
6869
6893
|
// would meet "already configured here" and dead-end. The manager fixes such a row, and falls
|
|
6870
6894
|
// through to add when there is genuinely nothing there.
|
|
6871
|
-
console.log(dim("
|
|
6895
|
+
console.log(dim(" No coding model configured."));
|
|
6872
6896
|
const go = await pickFromList(
|
|
6873
6897
|
[dim("leave it"), "add or change a provider — Claude API, OpenAI, Kimi…", "change the Ollama server this machine uses…"],
|
|
6874
6898
|
1
|
|
@@ -6886,7 +6910,7 @@ async function chooseModel() {
|
|
|
6886
6910
|
// symptom of being pointed at the wrong box, and the old message sent people to a web app a
|
|
6887
6911
|
// Client Expert machine does not have.
|
|
6888
6912
|
if (choices.length === 1) {
|
|
6889
|
-
console.log(dim(`
|
|
6913
|
+
console.log(dim(` Only one coding model is available: ${choices[0].model}.`));
|
|
6890
6914
|
// APPENDED AFTER the existing entry, here and below, so that entry keeps the index anyone
|
|
6891
6915
|
// who has used it already reaches for.
|
|
6892
6916
|
const go = await pickFromList(
|
|
@@ -6934,10 +6958,10 @@ async function chooseModel() {
|
|
|
6934
6958
|
// enough in name to have already been mixed up. The fix is to say which one THIS is — not to
|
|
6935
6959
|
// print the other two here: the agent model and the embedder are independent settings with
|
|
6936
6960
|
// their own commands, and listing them together implies a relationship that does not exist.
|
|
6937
|
-
console.log(dim(`
|
|
6961
|
+
console.log(dim(` Choose the CODING model — the one that writes code (${PICK_HINT}):`));
|
|
6938
6962
|
const idx = await pickFromList(labels, activeIdx >= 0 ? activeIdx : 0);
|
|
6939
6963
|
if (idx < 0) {
|
|
6940
|
-
console.log(dim("
|
|
6964
|
+
console.log(dim(" (cancelled)\n"));
|
|
6941
6965
|
return;
|
|
6942
6966
|
}
|
|
6943
6967
|
// The appended entries sit past the last model, so they are the only indices without a choice.
|
|
@@ -6967,7 +6991,7 @@ async function chooseModel() {
|
|
|
6967
6991
|
const where = showTag && !activeCoderWhere ? ` (${tags[idx]})` : "";
|
|
6968
6992
|
console.log(
|
|
6969
6993
|
accent(
|
|
6970
|
-
`
|
|
6994
|
+
` ✓ coding model → ${coderDisplayName(chosen.model, activeCoderWhere)}${where}${chosen.id === def ? " (default)" : ""}\n`
|
|
6971
6995
|
)
|
|
6972
6996
|
);
|
|
6973
6997
|
// DOES THIS ONE NEED AN API KEY? (task #181) Reported live on Windows 11 / Client Expert:
|
|
@@ -7020,7 +7044,7 @@ async function chooseModel() {
|
|
|
7020
7044
|
host: ks.provider,
|
|
7021
7045
|
});
|
|
7022
7046
|
console.log(
|
|
7023
|
-
dim("
|
|
7047
|
+
dim(" API key ") + accent(line.text) +
|
|
7024
7048
|
(line.note ? orange(` · ${line.note}`) : "") +
|
|
7025
7049
|
dim(` · ctrl+k to ${line.verb} it`)
|
|
7026
7050
|
);
|
|
@@ -7029,7 +7053,7 @@ async function chooseModel() {
|
|
|
7029
7053
|
// the more common one — "how do I change the model" — with no visible answer at all. Reported
|
|
7030
7054
|
// three times from exactly this view, twice after being told where the picker entry was.
|
|
7031
7055
|
// /provider is hidden (see HIDDEN_COMMANDS): point at the row of /model that does the same.
|
|
7032
|
-
console.log(dim("
|
|
7056
|
+
console.log(dim(" to change this provider's model or URL, or remove it: /model → add or change a provider\n"));
|
|
7033
7057
|
}
|
|
7034
7058
|
// Task #125: the GLOBAL lifetime total is per-backend, so re-fetch it for the newly
|
|
7035
7059
|
// selected backend NOW — otherwise the footer would keep showing the previous backend's
|
|
@@ -7383,13 +7407,13 @@ async function runSwitchSkillCommand(line) {
|
|
|
7383
7407
|
}
|
|
7384
7408
|
} else {
|
|
7385
7409
|
const options = ["claude", "gu"];
|
|
7386
|
-
console.log(dim(`
|
|
7410
|
+
console.log(dim(` Whose skills, MCP servers and hooks this folder uses (${PICK_HINT}):`));
|
|
7387
7411
|
const idx = await pickFromList(
|
|
7388
7412
|
options.map((o) => sourceLabel(o) + dim(` · ${where(o)}`) + (o === current ? dim(" · in use") : "")),
|
|
7389
7413
|
Math.max(0, options.indexOf(current))
|
|
7390
7414
|
);
|
|
7391
7415
|
if (idx < 0) {
|
|
7392
|
-
console.log(dim("
|
|
7416
|
+
console.log(dim(" (cancelled)\n"));
|
|
7393
7417
|
paintBar();
|
|
7394
7418
|
return;
|
|
7395
7419
|
}
|
|
@@ -7407,7 +7431,7 @@ async function runSwitchSkillCommand(line) {
|
|
|
7407
7431
|
_skillScan = { at: 0, skills: [] }; // the "/" menu and Tab follow at once
|
|
7408
7432
|
const say = (l) => console.log(GUTTER + l);
|
|
7409
7433
|
say(accent(`✓ ${pick === current ? "this folder uses" : "this folder now uses"} ${sourceLabel(pick)}`));
|
|
7410
|
-
say(dim(`
|
|
7434
|
+
say(dim(` skills, MCP servers and hooks are read from ${where(pick)}`));
|
|
7411
7435
|
// What was found there — and what was not, said rather than shown as an empty list.
|
|
7412
7436
|
try {
|
|
7413
7437
|
const sk = discoverSkills({ cwd: process.cwd(), reserved: COMMAND_NAMES });
|
|
@@ -7419,20 +7443,20 @@ async function runSwitchSkillCommand(line) {
|
|
|
7419
7443
|
say(dim(` ${count(sk.skills.length, "skill")}${sk.skills.length ? ` (${sk.skills.map((x) => "/" + x.name).join(", ")})` : ""} · ${count(running, "MCP server")} · ${count(nHooks(hk.user) + (hk.projectApproved ? nHooks(hk.project) : 0), "hook")}`));
|
|
7420
7444
|
// Nothing there at all is a normal state, not a fault: say so, and that gu needs none of it.
|
|
7421
7445
|
if (!sk.skills.length && !mcp.servers.length && !nHooks(hk.user) && !nHooks(hk.project) && !hk.notes.length) {
|
|
7422
|
-
say(dim("
|
|
7446
|
+
say(dim(" Nothing is set up there yet — gu works the same without any of them."));
|
|
7423
7447
|
}
|
|
7424
7448
|
for (const w of [...sk.warnings, ...mcp.warnings, ...hk.warnings]) say(yellow(" " + w));
|
|
7425
7449
|
const unsupported = mcp.servers.filter((x) => x.error);
|
|
7426
7450
|
if (unsupported.length) say(yellow(` ${count(unsupported.length, "MCP server")} cannot run here: ${unsupported.map((x) => `${x.name} (${x.error})`).join("; ")}`));
|
|
7427
7451
|
const waiting = mcp.servers.filter((x) => x.scope === "project" && !x.approved && !x.error);
|
|
7428
7452
|
if (waiting.length) say(yellow(` ${count(waiting.length, "project MCP server")} waiting for your approval: ${waiting.map((x) => x.name).join(", ")} — /mcp approve <name>`));
|
|
7429
|
-
if (hk.projectPending) say(yellow(`
|
|
7453
|
+
if (hk.projectPending) say(yellow(` this folder's ${hk.projectFile.replace(/^\.\//, "")} has hooks that will not run until you approve them — /hooks approve`));
|
|
7430
7454
|
if (hk.notes.length) say(yellow(` ${count(hk.notes.length, "hook")} in Claude's settings ${hk.notes.length === 1 ? "is" : "are"} NOT run by gu — /hooks says which and why`));
|
|
7431
|
-
if (pick === "claude" && (nHooks(hk.user) || nHooks(hk.project))) say(dim("
|
|
7455
|
+
if (pick === "claude" && (nHooks(hk.user) || nHooks(hk.project))) say(dim(" Claude's hooks get Claude's event and tool names from gu, so they record here as they do there."));
|
|
7432
7456
|
} catch (err) {
|
|
7433
|
-
say(yellow(`
|
|
7457
|
+
say(yellow(` (could not read it back: ${err.message})`));
|
|
7434
7458
|
}
|
|
7435
|
-
say(dim(`
|
|
7459
|
+
say(dim(` It holds from your next question. ${pick === "claude" ? "/switch-skill gu" : "/switch-skill claude"} switches back.`));
|
|
7436
7460
|
console.log("");
|
|
7437
7461
|
if (barPinned()) paintBar();
|
|
7438
7462
|
}
|
|
@@ -7581,7 +7605,7 @@ async function runUsePresetsCommand(line) {
|
|
|
7581
7605
|
console.log(GUTTER + dim("/save-preset pastes one in — copy it from another computer's /open-presets.") + "\n");
|
|
7582
7606
|
return;
|
|
7583
7607
|
}
|
|
7584
|
-
console.log(dim(`
|
|
7608
|
+
console.log(dim(` Presets (${ed.PICKER_HINT}):`));
|
|
7585
7609
|
const pick = await pickFromList(found, 0, { keys: ed.PICKER_KEYS });
|
|
7586
7610
|
if (!pick || pick.index < 0) {
|
|
7587
7611
|
console.log(GUTTER + dim("nothing changed.") + "\n");
|
|
@@ -7660,7 +7684,7 @@ async function runSaveSettingsCommand(line) {
|
|
|
7660
7684
|
// have to type is a name — and Enter keeps gu-settings.json, as before.
|
|
7661
7685
|
let typed = settingsArg(line, "/save-settings");
|
|
7662
7686
|
if (!typed) {
|
|
7663
|
-
const answer = await ask(`
|
|
7687
|
+
const answer = await ask(` file name in ${home} [${st.DEFAULT_SETTINGS_NAME}]: `);
|
|
7664
7688
|
if (answer === null) {
|
|
7665
7689
|
console.log(GUTTER + dim(" (cancelled — nothing was written)\n"));
|
|
7666
7690
|
return;
|
|
@@ -7720,7 +7744,7 @@ async function runLoadSettingsCommand(line) {
|
|
|
7720
7744
|
if (!typed) {
|
|
7721
7745
|
const found = st.settingsFilesIn(listJsonFiles(home));
|
|
7722
7746
|
if (found.length > 1) {
|
|
7723
|
-
console.log(dim(`
|
|
7747
|
+
console.log(dim(` Settings files in ${home} (${PICK_HINT}):`));
|
|
7724
7748
|
const i = await pickFromList(found, 0);
|
|
7725
7749
|
if (i < 0) {
|
|
7726
7750
|
console.log(GUTTER + dim(" (cancelled — nothing changed)\n"));
|
|
@@ -8070,11 +8094,11 @@ async function runClearLogs() {
|
|
|
8070
8094
|
const question = (q) => {
|
|
8071
8095
|
const hint = `(${PICK_HINT}):`;
|
|
8072
8096
|
console.log("");
|
|
8073
|
-
if (2 + q.length + 2 + hint.length < width) console.log(dim(`
|
|
8074
|
-
else console.log(dim(`
|
|
8097
|
+
if (2 + q.length + 2 + hint.length < width) console.log(dim(` ${q} ${hint}`));
|
|
8098
|
+
else console.log(dim(` ${q}\n ${hint}`));
|
|
8075
8099
|
};
|
|
8076
8100
|
const cancelled = () => {
|
|
8077
|
-
console.log(dim(`
|
|
8101
|
+
console.log(dim(` (stopped — nothing ${freed.length ? "further " : ""}removed)\n`));
|
|
8078
8102
|
paintBar();
|
|
8079
8103
|
};
|
|
8080
8104
|
|
|
@@ -8175,7 +8199,7 @@ async function saveEmbedSettings(fields) {
|
|
|
8175
8199
|
const saved = await withPendingStatus("saving your embedder", () =>
|
|
8176
8200
|
md.saveWorkerSettings(apiBase, workerKey, fields)
|
|
8177
8201
|
);
|
|
8178
|
-
if (!saved) console.log(red("
|
|
8202
|
+
if (!saved) console.log(red(" couldn't save that to your account — nothing changed.\n"));
|
|
8179
8203
|
return saved;
|
|
8180
8204
|
}
|
|
8181
8205
|
|
|
@@ -8193,7 +8217,7 @@ async function confirmEmbedSwitch(ec, current, chosen) {
|
|
|
8193
8217
|
}
|
|
8194
8218
|
const plan = ec.embedChangePlan({ current, chosen, indexes });
|
|
8195
8219
|
if (!plan.changed) {
|
|
8196
|
-
console.log(dim(`
|
|
8220
|
+
console.log(dim(` already using ${current} — nothing changed.\n`));
|
|
8197
8221
|
return { changed: false, plan: null };
|
|
8198
8222
|
}
|
|
8199
8223
|
// "Are you sure?" with nothing behind it is a prompt people learn to dismiss; the four projects
|
|
@@ -8201,7 +8225,7 @@ async function confirmEmbedSwitch(ec, current, chosen) {
|
|
|
8201
8225
|
for (const l of ec.describeEmbedChange(plan)) console.log(GUTTER + dim(l));
|
|
8202
8226
|
const go = await pickFromList([`switch to ${chosen}`, `keep ${current}`], plan.stale.length ? 1 : 0);
|
|
8203
8227
|
if (go !== 0) {
|
|
8204
|
-
console.log(dim("
|
|
8228
|
+
console.log(dim(" (kept)\n"));
|
|
8205
8229
|
return { changed: false, plan: null };
|
|
8206
8230
|
}
|
|
8207
8231
|
return { changed: true, plan };
|
|
@@ -8211,13 +8235,13 @@ async function confirmEmbedSwitch(ec, current, chosen) {
|
|
|
8211
8235
|
// shared recognition step. Returns { changed, chosen, plan }.
|
|
8212
8236
|
async function pickEmbedModel(ec, current) {
|
|
8213
8237
|
const rows = ec.embedChoiceRows(current);
|
|
8214
|
-
console.log(dim(`
|
|
8238
|
+
console.log(dim(` Choose the embedder (${PICK_HINT}):`));
|
|
8215
8239
|
const labels = rows.map(
|
|
8216
8240
|
(r) => `${r.model}${r.dims ? dim(` · ${r.dims}d`) : ""}${dim(` · ${r.note}`)}` + (r.current ? accent(" (current)") : "")
|
|
8217
8241
|
);
|
|
8218
8242
|
const idx = await pickFromList(labels, Math.max(0, rows.findIndex((r) => r.current)));
|
|
8219
8243
|
if (idx < 0) {
|
|
8220
|
-
console.log(dim("
|
|
8244
|
+
console.log(dim(" (cancelled)\n"));
|
|
8221
8245
|
return { changed: false, chosen: "", plan: null };
|
|
8222
8246
|
}
|
|
8223
8247
|
const chosen = rows[idx].model;
|
|
@@ -8231,21 +8255,21 @@ async function pickEmbedModel(ec, current) {
|
|
|
8231
8255
|
// a model that was never pulled fails at the first search, not here. Say it once rather than let
|
|
8232
8256
|
// it surface as "RAG is broken".
|
|
8233
8257
|
function announceEmbedSaved({ chosen, plan, url }) {
|
|
8234
|
-
console.log(accent(`
|
|
8258
|
+
console.log(accent(` ✓ embedding model → ${chosen}` + (url ? ` on ${hostOf(url)}` : "")));
|
|
8235
8259
|
if (plan?.stale.length) {
|
|
8236
8260
|
console.log(
|
|
8237
8261
|
dim(` ${plan.stale.length} knowledge base${plan.stale.length === 1 ? "" : "s"} ` +
|
|
8238
8262
|
"will rebuild on the next search — the first one will be slower.")
|
|
8239
8263
|
);
|
|
8240
8264
|
}
|
|
8241
|
-
console.log(dim(`
|
|
8265
|
+
console.log(dim(` Make sure it is pulled there: ollama pull ${chosen}\n`));
|
|
8242
8266
|
}
|
|
8243
8267
|
|
|
8244
8268
|
async function chooseEmbedModel() {
|
|
8245
8269
|
const ec = await import("./embed-model-command.mjs");
|
|
8246
8270
|
const settings = await withPendingStatus("reading your embedder", fetchWorkerSettings);
|
|
8247
8271
|
if (!settings) {
|
|
8248
|
-
console.log(red("
|
|
8272
|
+
console.log(red(" couldn't reach the API to read your current embedder.\n"));
|
|
8249
8273
|
return;
|
|
8250
8274
|
}
|
|
8251
8275
|
const current = ec.currentEmbedModel(settings);
|
|
@@ -8258,11 +8282,11 @@ async function chooseEmbedModel() {
|
|
|
8258
8282
|
const keyRequired = isOllamaCloudUrl(currentUrl) || /\/v1\/?$/i.test(currentUrl) ||
|
|
8259
8283
|
Boolean(String(settings.ragEmbedKeyId ?? "").trim());
|
|
8260
8284
|
const menu = ec.embedMenuOptions({ current, host, keyRequired });
|
|
8261
|
-
console.log(dim(`
|
|
8285
|
+
console.log(dim(` Embedding model (currently ${current} on ${host || currentUrl})`));
|
|
8262
8286
|
const mi = await pickFromList(menu.map((o) => o.label), 0);
|
|
8263
8287
|
const action = mi >= 0 ? menu[mi].action : "cancel";
|
|
8264
8288
|
if (action === "cancel") {
|
|
8265
|
-
console.log(dim("
|
|
8289
|
+
console.log(dim(" (cancelled)\n"));
|
|
8266
8290
|
return;
|
|
8267
8291
|
}
|
|
8268
8292
|
|
|
@@ -8303,12 +8327,12 @@ async function saveAgentModel(agentUrl, model, host) {
|
|
|
8303
8327
|
md.saveWorkerSettings(apiBase, workerKey, { agentModelUrl: agentUrl, agentModelName: model, agentModel: model })
|
|
8304
8328
|
);
|
|
8305
8329
|
if (!ok) {
|
|
8306
|
-
console.log(red(`
|
|
8330
|
+
console.log(red(` couldn't save the agent model — nothing changed.`));
|
|
8307
8331
|
console.log(GUTTER + dim("if this machine's local API is old, restart it with `gu api restart`.\n"));
|
|
8308
8332
|
return false;
|
|
8309
8333
|
}
|
|
8310
|
-
console.log(accent(`
|
|
8311
|
-
console.log(dim("
|
|
8334
|
+
console.log(accent(` ✓ agent model → ${model}`) + dim(` · ${host}`));
|
|
8335
|
+
console.log(dim(" takes effect on your next question.\n"));
|
|
8312
8336
|
return true;
|
|
8313
8337
|
}
|
|
8314
8338
|
|
|
@@ -8321,7 +8345,7 @@ async function askKeyName(prompt = " key name: ") {
|
|
|
8321
8345
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
8322
8346
|
const name = (await ask(prompt)).trim();
|
|
8323
8347
|
if (!am.looksLikeSecret(name)) return name;
|
|
8324
|
-
console.log(yellow("
|
|
8348
|
+
console.log(yellow(" that looks like the API key itself — the NAME is shown on screen, so it must not be the key."));
|
|
8325
8349
|
console.log(GUTTER + dim('type a short label like "work", or press Enter for none — the key comes next.'));
|
|
8326
8350
|
}
|
|
8327
8351
|
return "";
|
|
@@ -8342,7 +8366,7 @@ async function askServerKey({ host = "", nameHint = '"work", "personal"', option
|
|
|
8342
8366
|
console.log("\n" + GUTTER + dim(`A name just tells keys apart — ${nameHint}.`));
|
|
8343
8367
|
const name = (await askKeyName()).trim();
|
|
8344
8368
|
console.log(GUTTER + dim(SECRET_KEY_HINT));
|
|
8345
|
-
const secret = (await askSecret(`
|
|
8369
|
+
const secret = (await askSecret(` API key${host ? ` for ${host}` : ""}${name ? ` (${name})` : ""}: `)).trim();
|
|
8346
8370
|
if (!secret) {
|
|
8347
8371
|
console.log(GUTTER + dim(optional ? "(no key — the server will be called anonymously)" : "(cancelled — nothing was saved)\n"));
|
|
8348
8372
|
return { name, secret: "" };
|
|
@@ -8377,15 +8401,15 @@ async function chooseAgentProvider() {
|
|
|
8377
8401
|
const am = await import("./agent-model-command.mjs");
|
|
8378
8402
|
const settings = await withPendingStatus("reading your providers", fetchWorkerSettings);
|
|
8379
8403
|
if (!settings) {
|
|
8380
|
-
console.log(red("
|
|
8404
|
+
console.log(red(" couldn't reach the API to read your providers.\n"));
|
|
8381
8405
|
return;
|
|
8382
8406
|
}
|
|
8383
8407
|
const choices = am.agentProviderChoices(settings);
|
|
8384
8408
|
if (choices.length === 1) return addServer({ role: "agent", dialect: "openai" });
|
|
8385
|
-
console.log(dim(`
|
|
8409
|
+
console.log(dim(` Which OpenAI-compatible provider should the agent use? (${PICK_HINT})`));
|
|
8386
8410
|
const idx = await pickFromList(choices.map((c) => c.label), 0);
|
|
8387
8411
|
if (idx < 0) {
|
|
8388
|
-
console.log(dim("
|
|
8412
|
+
console.log(dim(" (cancelled)\n"));
|
|
8389
8413
|
return;
|
|
8390
8414
|
}
|
|
8391
8415
|
const pick = choices[idx];
|
|
@@ -8407,7 +8431,7 @@ async function chooseAgentModel() {
|
|
|
8407
8431
|
const am = await import("./agent-model-command.mjs");
|
|
8408
8432
|
const settings = await withPendingStatus("reading your agent model", fetchWorkerSettings);
|
|
8409
8433
|
if (!settings) {
|
|
8410
|
-
console.log(red("
|
|
8434
|
+
console.log(red(" couldn't reach the API to read your current agent model.\n"));
|
|
8411
8435
|
return;
|
|
8412
8436
|
}
|
|
8413
8437
|
const current = am.currentAgentModel(settings);
|
|
@@ -8493,12 +8517,12 @@ async function chooseAgentModel() {
|
|
|
8493
8517
|
// WHAT TO CHANGE, FIRST (#44). The ways to use an Ollama server or another provider used to sit
|
|
8494
8518
|
// after every model on this one — 130 rows down on api.openai.com — which read as "OpenAI models
|
|
8495
8519
|
// are all you can choose". A short menu of what to change comes before any list.
|
|
8496
|
-
console.log(dim(`
|
|
8520
|
+
console.log(dim(` Agent model — the one that TALKS: ${current || "unset"} on ${listHost}`));
|
|
8497
8521
|
const menu = am.agentMenuOptions({ host: listHost, modelCount: models.length, needsKey });
|
|
8498
8522
|
const first = await pickFromList(menu.map((o) => o.label), 0);
|
|
8499
8523
|
const firstAction = first >= 0 ? menu[first].action : "cancel";
|
|
8500
8524
|
if (firstAction === "cancel") {
|
|
8501
|
-
console.log(dim("
|
|
8525
|
+
console.log(dim(" (cancelled)\n"));
|
|
8502
8526
|
return;
|
|
8503
8527
|
}
|
|
8504
8528
|
if (firstAction === "ollama") return addServer({ role: "agent", dialect: "ollama" });
|
|
@@ -8512,7 +8536,7 @@ async function chooseAgentModel() {
|
|
|
8512
8536
|
}
|
|
8513
8537
|
|
|
8514
8538
|
const rows = am.agentModelRows(models, current);
|
|
8515
|
-
console.log(dim(`
|
|
8539
|
+
console.log(dim(` Agent model — the one that TALKS (currently ${current || "unset"})`));
|
|
8516
8540
|
// Strip whichever list endpoint was asked, so the header names the SERVER rather than the path.
|
|
8517
8541
|
console.log(
|
|
8518
8542
|
dim(` ${models.length} on ${url.replace(/\/api\/tags$/, "").replace(/\/models$/, "")} (${PICK_HINT}):`)
|
|
@@ -8541,12 +8565,12 @@ async function chooseAgentModel() {
|
|
|
8541
8565
|
// where they can be seen, rather than after every model.
|
|
8542
8566
|
const idx = await pickFromList(labels, Math.max(0, rows.findIndex((r) => r.current)));
|
|
8543
8567
|
if (idx < 0) {
|
|
8544
|
-
console.log(dim("
|
|
8568
|
+
console.log(dim(" (cancelled)\n"));
|
|
8545
8569
|
return;
|
|
8546
8570
|
}
|
|
8547
8571
|
const chosen = rows[idx].model;
|
|
8548
8572
|
if (chosen === current) {
|
|
8549
|
-
console.log(dim(`
|
|
8573
|
+
console.log(dim(` already using ${current} — nothing changed.\n`));
|
|
8550
8574
|
return;
|
|
8551
8575
|
}
|
|
8552
8576
|
if (rows[idx].embedder) {
|
|
@@ -8557,7 +8581,7 @@ async function chooseAgentModel() {
|
|
|
8557
8581
|
console.log(GUTTER + yellow("produce nonsense rather than an error."));
|
|
8558
8582
|
const go = await pickFromList([`use it anyway`, `pick something else`], 1);
|
|
8559
8583
|
if (go !== 0) {
|
|
8560
|
-
console.log(dim("
|
|
8584
|
+
console.log(dim(" (kept)\n"));
|
|
8561
8585
|
return;
|
|
8562
8586
|
}
|
|
8563
8587
|
}
|
|
@@ -8570,15 +8594,15 @@ async function chooseAgentModel() {
|
|
|
8570
8594
|
agentModel: chosen,
|
|
8571
8595
|
});
|
|
8572
8596
|
if (!saved) {
|
|
8573
|
-
console.log(red(`
|
|
8597
|
+
console.log(red(` couldn't save that to your account — still using ${current || "unset"}.`));
|
|
8574
8598
|
// The likeliest cause by far, and it is invisible otherwise: this field only became writable
|
|
8575
8599
|
// through the worker route in task #169, so an older local API silently ignores it.
|
|
8576
8600
|
console.log(GUTTER + dim("if this machine's local API predates /agent-model, restart it with"));
|
|
8577
8601
|
console.log(GUTTER + dim("`gu api restart` to pick up the current version.\n"));
|
|
8578
8602
|
return;
|
|
8579
8603
|
}
|
|
8580
|
-
console.log(accent(`
|
|
8581
|
-
console.log(dim("
|
|
8604
|
+
console.log(accent(` ✓ agent model → ${chosen}`));
|
|
8605
|
+
console.log(dim(" takes effect on your next question.\n"));
|
|
8582
8606
|
}
|
|
8583
8607
|
|
|
8584
8608
|
// Probe whether SSH KEY auth already works for a server (so we can tell the user to run
|
|
@@ -8603,7 +8627,7 @@ async function checkServerKeyAuth(s) {
|
|
|
8603
8627
|
if (ok) console.log(dim(" ✓ key auth works — deploys will connect without a password."));
|
|
8604
8628
|
else
|
|
8605
8629
|
console.log(
|
|
8606
|
-
yellow("
|
|
8630
|
+
yellow(" key auth not set up yet.") +
|
|
8607
8631
|
dim(` Run once in your terminal: `) +
|
|
8608
8632
|
`ssh-copy-id ${s.user}@${s.host}`
|
|
8609
8633
|
);
|
|
@@ -8803,20 +8827,20 @@ async function runS3Command(line) {
|
|
|
8803
8827
|
// and, for the secret, does not have to know it.
|
|
8804
8828
|
const keep = (typed, was) => (String(typed ?? "").trim() ? String(typed).trim() : was);
|
|
8805
8829
|
const location = keep(
|
|
8806
|
-
await ask(`
|
|
8830
|
+
await ask(` Bucket (ARN, s3://… or name)${current.location ? ` [${current.location}]` : ""}: `),
|
|
8807
8831
|
current.location
|
|
8808
8832
|
);
|
|
8809
8833
|
const region = keep(
|
|
8810
|
-
await ask(`
|
|
8834
|
+
await ask(` Region [${current.region || "ap-southeast-1"}]: `),
|
|
8811
8835
|
current.region || "ap-southeast-1"
|
|
8812
8836
|
);
|
|
8813
8837
|
const accessKeyId = keep(
|
|
8814
|
-
await ask(`
|
|
8838
|
+
await ask(` AWS access key id${current.accessKeyId ? ` [${s3.maskKeyId(current.accessKeyId)}]` : ""}: `),
|
|
8815
8839
|
current.accessKeyId
|
|
8816
8840
|
);
|
|
8817
8841
|
// askSecret, never ask: the echo is muted and the value never reaches the scrollback (#127).
|
|
8818
8842
|
const typedSecret = String(
|
|
8819
|
-
await askSecret(`
|
|
8843
|
+
await askSecret(` AWS secret${current.secretConfigured ? " [Enter keeps the stored one]" : ""}:`)
|
|
8820
8844
|
).trim();
|
|
8821
8845
|
|
|
8822
8846
|
if (!location || !accessKeyId || !(typedSecret || current.secretConfigured)) {
|
|
@@ -9013,16 +9037,16 @@ async function chooseServer() {
|
|
|
9013
9037
|
);
|
|
9014
9038
|
const data = await res.json().catch(() => ({}));
|
|
9015
9039
|
if (!res.ok) {
|
|
9016
|
-
console.log(red(`
|
|
9040
|
+
console.log(red(` couldn't load servers: ${data?.error || `HTTP ${res.status}`}\n`));
|
|
9017
9041
|
return;
|
|
9018
9042
|
}
|
|
9019
9043
|
servers = Array.isArray(data?.servers) ? data.servers : [];
|
|
9020
9044
|
} catch (err) {
|
|
9021
|
-
console.log(red(`
|
|
9045
|
+
console.log(red(` ${describeApiFailure(err, "couldn't load servers")}\n`));
|
|
9022
9046
|
return;
|
|
9023
9047
|
}
|
|
9024
9048
|
if (servers.length === 0) {
|
|
9025
|
-
console.log(dim("
|
|
9049
|
+
console.log(dim(" No deployment servers yet — add them in the web app → Servers.\n"));
|
|
9026
9050
|
return;
|
|
9027
9051
|
}
|
|
9028
9052
|
// Arrow-key picker: ↑/↓ to move, Enter to choose (no number typing). The list ends with
|
|
@@ -9030,21 +9054,21 @@ async function chooseServer() {
|
|
|
9030
9054
|
const labels = servers.map((s) => `${s.name} (${s.user}@${s.host})`);
|
|
9031
9055
|
labels.push("none (clear selection)");
|
|
9032
9056
|
const activeIdx = servers.findIndex((s) => s.host === selectedServer?.host);
|
|
9033
|
-
console.log(dim(`
|
|
9057
|
+
console.log(dim(` Choose a deployment server (${PICK_HINT}):`));
|
|
9034
9058
|
const idx = await pickFromList(labels, activeIdx >= 0 ? activeIdx : 0);
|
|
9035
9059
|
if (idx < 0) {
|
|
9036
|
-
console.log(dim("
|
|
9060
|
+
console.log(dim(" (cancelled)\n"));
|
|
9037
9061
|
return;
|
|
9038
9062
|
}
|
|
9039
9063
|
if (idx === servers.length) {
|
|
9040
9064
|
// the trailing "none" row
|
|
9041
9065
|
selectedServer = null;
|
|
9042
|
-
console.log(dim("
|
|
9066
|
+
console.log(dim(" ✓ deployment server cleared.\n"));
|
|
9043
9067
|
return;
|
|
9044
9068
|
}
|
|
9045
9069
|
const s = servers[idx];
|
|
9046
9070
|
selectedServer = { name: s.name, host: s.host, user: s.user };
|
|
9047
|
-
console.log(accent(`
|
|
9071
|
+
console.log(accent(` ✓ deploy target → ${s.name} (${s.user}@${s.host})`));
|
|
9048
9072
|
await checkServerKeyAuth(selectedServer);
|
|
9049
9073
|
console.log("");
|
|
9050
9074
|
}
|
|
@@ -9136,10 +9160,10 @@ async function showOtherAgents() {
|
|
|
9136
9160
|
return;
|
|
9137
9161
|
}
|
|
9138
9162
|
console.log("");
|
|
9139
|
-
console.log(dim(`
|
|
9163
|
+
console.log(dim(` Your other running agents — pick one to act on (${PICK_HINT}):`));
|
|
9140
9164
|
const which = await pickFromList(items.map((i) => i.label), 0);
|
|
9141
9165
|
if (which < 0) {
|
|
9142
|
-
console.log(dim("
|
|
9166
|
+
console.log(dim(" (cancelled)\n"));
|
|
9143
9167
|
paintBar();
|
|
9144
9168
|
return;
|
|
9145
9169
|
}
|
|
@@ -9162,10 +9186,10 @@ async function showOtherAgents() {
|
|
|
9162
9186
|
{ id: "stop", label: "Stop it", note: "ends the run — its unsaved work is lost" },
|
|
9163
9187
|
];
|
|
9164
9188
|
console.log("");
|
|
9165
|
-
console.log(dim(`
|
|
9189
|
+
console.log(dim(` ${shortName} (${PICK_HINT}):`));
|
|
9166
9190
|
const act = await pickFromList(actions.map((a) => a.label + dim(` · ${a.note}`)), 0);
|
|
9167
9191
|
if (act < 0) {
|
|
9168
|
-
console.log(dim("
|
|
9192
|
+
console.log(dim(" (cancelled)\n"));
|
|
9169
9193
|
paintBar();
|
|
9170
9194
|
return;
|
|
9171
9195
|
}
|
|
@@ -9463,9 +9487,9 @@ async function promptNewCodingKey(backendId = "", provider = "") {
|
|
|
9463
9487
|
const id = await addCodingKey(name, secret, backendId);
|
|
9464
9488
|
console.log(
|
|
9465
9489
|
id
|
|
9466
|
-
? accent(`
|
|
9490
|
+
? accent(` ✓ added ${name || "Unknown"}`) +
|
|
9467
9491
|
(backendId && provider ? dim(` · for ${provider}`) : "") + "\n"
|
|
9468
|
-
: red("
|
|
9492
|
+
: red(" couldn't save the key — try again, or set it in web Settings → Agent\n")
|
|
9469
9493
|
);
|
|
9470
9494
|
return id;
|
|
9471
9495
|
}
|
|
@@ -9558,9 +9582,9 @@ async function replaceCodingKey() {
|
|
|
9558
9582
|
console.log(
|
|
9559
9583
|
ok
|
|
9560
9584
|
? clearing
|
|
9561
|
-
? accent(`
|
|
9562
|
-
: accent(`
|
|
9563
|
-
: red("
|
|
9585
|
+
? accent(` ✓ ${ks.model} API key removed`) + dim(" — coding falls back to your local model\n")
|
|
9586
|
+
: accent(` ✓ ${ks.model} API key saved\n`)
|
|
9587
|
+
: red(" couldn't save the key — try again, or set it in web Settings → Agent\n")
|
|
9564
9588
|
);
|
|
9565
9589
|
paintBar();
|
|
9566
9590
|
}
|
|
@@ -9590,7 +9614,7 @@ async function manageServerKeys({ role = "coding", backendId, host, model = "" }
|
|
|
9590
9614
|
backendId ? fetchServerKeys(backendId) : fetchCodingKeys()
|
|
9591
9615
|
);
|
|
9592
9616
|
if (!keys) {
|
|
9593
|
-
console.log(red(`
|
|
9617
|
+
console.log(red(` couldn't read the keys for ${host} — is this machine's API up to date? (\`gu api restart\`)\n`));
|
|
9594
9618
|
paintBar();
|
|
9595
9619
|
return "";
|
|
9596
9620
|
}
|
|
@@ -9635,7 +9659,7 @@ async function manageServerKeys({ role = "coding", backendId, host, model = "" }
|
|
|
9635
9659
|
const fresh = await fetchServerKeys(backendId);
|
|
9636
9660
|
const added = fresh?.find((k) => k.id === id);
|
|
9637
9661
|
if (added?.default) {
|
|
9638
|
-
console.log(accent(`
|
|
9662
|
+
console.log(accent(` ✓ ${added.name} is ${host}'s default key\n`));
|
|
9639
9663
|
paintBar();
|
|
9640
9664
|
return id;
|
|
9641
9665
|
}
|
|
@@ -9647,7 +9671,7 @@ async function manageServerKeys({ role = "coding", backendId, host, model = "" }
|
|
|
9647
9671
|
if (!key.backendId && backendId) {
|
|
9648
9672
|
const ok = await withPendingStatus(`tying ${key.name} to ${host}`, () => bindKeyToServer(key.id, backendId));
|
|
9649
9673
|
if (!ok) {
|
|
9650
|
-
console.log(red(`
|
|
9674
|
+
console.log(red(` couldn't tie ${key.name} to ${host} — nothing changed.\n`));
|
|
9651
9675
|
paintBar();
|
|
9652
9676
|
return "";
|
|
9653
9677
|
}
|
|
@@ -9666,10 +9690,10 @@ async function applyKeyPick({ role, backendId, host, key }) {
|
|
|
9666
9690
|
let scope = "folder";
|
|
9667
9691
|
if (!backendId) {
|
|
9668
9692
|
await saveFolderKeyPick(role, key.id);
|
|
9669
|
-
console.log(accent(`
|
|
9693
|
+
console.log(accent(` ✓ ${folderLabel()}'s ${roleLabel} → ${key.name}`) + dim(` ····${key.tail}\n`));
|
|
9670
9694
|
} else if (key.default) {
|
|
9671
9695
|
await saveFolderKeyPick(role, "");
|
|
9672
|
-
console.log(accent(`
|
|
9696
|
+
console.log(accent(` ✓ ${folderLabel()}'s ${roleLabel} → ${key.name}`) + dim(` ····${key.tail} · ${host}'s default\n`));
|
|
9673
9697
|
} else {
|
|
9674
9698
|
const choices = sk.scopeChoices({ host, role });
|
|
9675
9699
|
console.log("\n" + GUTTER + dim(`Use ${key.name} for…`));
|
|
@@ -9683,16 +9707,16 @@ async function applyKeyPick({ role, backendId, host, key }) {
|
|
|
9683
9707
|
if (scope === "default") {
|
|
9684
9708
|
const ok = await withPendingStatus(`making ${key.name} the default for ${host}`, () => setServerDefaultKey(backendId, key.id));
|
|
9685
9709
|
if (!ok) {
|
|
9686
|
-
console.log(red("
|
|
9710
|
+
console.log(red(" couldn't change the default — nothing changed.\n"));
|
|
9687
9711
|
paintBar();
|
|
9688
9712
|
return "";
|
|
9689
9713
|
}
|
|
9690
9714
|
// This folder follows the default now, rather than pinning a copy of it.
|
|
9691
9715
|
await saveFolderKeyPick(role, "");
|
|
9692
|
-
console.log(accent(`
|
|
9716
|
+
console.log(accent(` ✓ ${key.name} is now ${host}'s default`) + dim(` ····${key.tail} · every folder without its own pick\n`));
|
|
9693
9717
|
} else {
|
|
9694
9718
|
await saveFolderKeyPick(role, key.id);
|
|
9695
|
-
console.log(accent(`
|
|
9719
|
+
console.log(accent(` ✓ ${folderLabel()}'s ${roleLabel} → ${key.name}`) + dim(` ····${key.tail} · this folder only\n`));
|
|
9696
9720
|
}
|
|
9697
9721
|
}
|
|
9698
9722
|
if (role === "coding") {
|
|
@@ -9746,7 +9770,7 @@ async function deleteServerKeyFlow({ role, key, host, folderKeyId, lastOfServer
|
|
|
9746
9770
|
}
|
|
9747
9771
|
const out = await deleteCodingKey(key.id);
|
|
9748
9772
|
if (!out.ok) {
|
|
9749
|
-
console.log(red("
|
|
9773
|
+
console.log(red(" couldn't delete the key — try again, or remove it in web Settings → Agent\n"));
|
|
9750
9774
|
paintBar();
|
|
9751
9775
|
return;
|
|
9752
9776
|
}
|
|
@@ -9755,7 +9779,7 @@ async function deleteServerKeyFlow({ role, key, host, folderKeyId, lastOfServer
|
|
|
9755
9779
|
const mine = r === "agent" ? sessionAgentKeyId : r === "embed" ? sessionEmbedKeyId : sessionCodingKeyId;
|
|
9756
9780
|
if (mine === key.id) await saveFolderKeyPick(r, "");
|
|
9757
9781
|
}
|
|
9758
|
-
console.log(accent(`
|
|
9782
|
+
console.log(accent(` ✓ deleted ${out.name || key.name}`) + dim(` ····${key.tail} · ${out.remaining} key(s) left\n`));
|
|
9759
9783
|
paintBar();
|
|
9760
9784
|
}
|
|
9761
9785
|
|
|
@@ -10023,7 +10047,7 @@ async function approvalPrompt(command) {
|
|
|
10023
10047
|
ok = (await runS3Command("/s3")) === true;
|
|
10024
10048
|
} catch (e) {
|
|
10025
10049
|
if (!wasPromptCancelled(e)) throw e;
|
|
10026
|
-
console.log(dim("
|
|
10050
|
+
console.log(dim(" (cancelled — the file is saved on this computer)\n"));
|
|
10027
10051
|
} finally {
|
|
10028
10052
|
following = wasFollowing;
|
|
10029
10053
|
}
|
|
@@ -12531,7 +12555,7 @@ async function main() {
|
|
|
12531
12555
|
// meaningfully refuse.
|
|
12532
12556
|
const steps = await import("./first-run-steps.mjs");
|
|
12533
12557
|
const step = steps.stepById("daemon");
|
|
12534
|
-
console.log(dim(`
|
|
12558
|
+
console.log(dim(` ${step.doing}…`));
|
|
12535
12559
|
let started = false;
|
|
12536
12560
|
let why = "";
|
|
12537
12561
|
try {
|
|
@@ -12539,7 +12563,7 @@ async function main() {
|
|
|
12539
12563
|
started = !!r.started;
|
|
12540
12564
|
localDaemonRunning = started || localDaemonRunning;
|
|
12541
12565
|
if (started) {
|
|
12542
|
-
console.log(dim(`
|
|
12566
|
+
console.log(dim(` started background worker (pid ${r.pid}) · logs ${ctl.LOG_FILE}`));
|
|
12543
12567
|
} else {
|
|
12544
12568
|
why = r.reason || "the worker did not report a pid";
|
|
12545
12569
|
}
|
|
@@ -12553,7 +12577,7 @@ async function main() {
|
|
|
12553
12577
|
await recordStepOutcome("daemon", started, why);
|
|
12554
12578
|
if (!started) {
|
|
12555
12579
|
console.log(
|
|
12556
|
-
yellow("
|
|
12580
|
+
yellow(" the background worker did not start") +
|
|
12557
12581
|
dim(` — ${why}\n ${step.why}.\n Try \`gu start\` to see the error in full.\n`)
|
|
12558
12582
|
);
|
|
12559
12583
|
}
|
|
@@ -12906,7 +12930,7 @@ async function main() {
|
|
|
12906
12930
|
} else {
|
|
12907
12931
|
await saveSessionSkip(resolve(process.cwd()), "skipDepsPrompt", true);
|
|
12908
12932
|
console.log(
|
|
12909
|
-
dim("
|
|
12933
|
+
dim(" Skipped — install later with `gu doctor`") +
|
|
12910
12934
|
dim(clientModeBanner ? " --client" : "") +
|
|
12911
12935
|
dim("; I won't ask again in this folder.\n")
|
|
12912
12936
|
);
|
|
@@ -12940,7 +12964,7 @@ async function main() {
|
|
|
12940
12964
|
);
|
|
12941
12965
|
if (skipSetup) {
|
|
12942
12966
|
console.log(
|
|
12943
|
-
dim("
|
|
12967
|
+
dim(" Set-up skipped (remembered for this folder) — `gu doctor` runs it ") +
|
|
12944
12968
|
dim("any time.\n")
|
|
12945
12969
|
);
|
|
12946
12970
|
}
|
|
@@ -12968,7 +12992,7 @@ async function main() {
|
|
|
12968
12992
|
await ctl2.setClientMode(true);
|
|
12969
12993
|
const st = await ctl2.daemonStatus();
|
|
12970
12994
|
console.log(
|
|
12971
|
-
dim("
|
|
12995
|
+
dim(" Skipped — this machine won't SERVE models; the agent still runs here and\n") +
|
|
12972
12996
|
dim(" calls your model machine over the network. Point the Agent model URL at\n") +
|
|
12973
12997
|
dim(" that machine (not 127.0.0.1) — `gu status` shows what it resolves to.\n") +
|
|
12974
12998
|
dim(" `gu doctor` sets models up here · `/host-mode` undoes this.\n")
|
|
@@ -13146,8 +13170,8 @@ async function main() {
|
|
|
13146
13170
|
const started = await tryStartLocalApi();
|
|
13147
13171
|
console.error(
|
|
13148
13172
|
started
|
|
13149
|
-
? dim("
|
|
13150
|
-
: dim("
|
|
13173
|
+
? dim(" Started it — run `gu` again.\n")
|
|
13174
|
+
: dim(" Start it with: gu-cli api start") +
|
|
13151
13175
|
dim("\n If that fails, check MongoDB is running and see ~/.gonext/local-api.log\n")
|
|
13152
13176
|
);
|
|
13153
13177
|
}
|
|
@@ -13596,7 +13620,7 @@ async function main() {
|
|
|
13596
13620
|
const direct = line === "/rag-local" ? "local" : line === "/rag-cloud" ? "both" : null;
|
|
13597
13621
|
let chosen = direct;
|
|
13598
13622
|
if (!chosen) {
|
|
13599
|
-
console.log(dim(`
|
|
13623
|
+
console.log(dim(` Where this folder's RAG knowledge base lives (${PICK_HINT}):`));
|
|
13600
13624
|
const idx = await pickFromList(
|
|
13601
13625
|
RAG_STORAGE.map(
|
|
13602
13626
|
(r) =>
|
|
@@ -13607,7 +13631,7 @@ async function main() {
|
|
|
13607
13631
|
Math.max(0, RAG_STORAGE.findIndex((r) => r.id === sessionRagStorage))
|
|
13608
13632
|
);
|
|
13609
13633
|
if (idx < 0) {
|
|
13610
|
-
console.log(dim("
|
|
13634
|
+
console.log(dim(" (cancelled)\n"));
|
|
13611
13635
|
continue;
|
|
13612
13636
|
}
|
|
13613
13637
|
chosen = RAG_STORAGE[idx].id;
|
|
@@ -13623,7 +13647,7 @@ async function main() {
|
|
|
13623
13647
|
// switching to cloud leaves the local shards on disk unread, and switching to local
|
|
13624
13648
|
// leaves the S3 ones untouched — either way the new store is built by the next index.
|
|
13625
13649
|
console.log(
|
|
13626
|
-
accent(`
|
|
13650
|
+
accent(` ✓ RAG storage → ${row.label}`) +
|
|
13627
13651
|
dim(`\n ${row.note}`) +
|
|
13628
13652
|
dim("\n Existing shards are not moved — the new store fills as this folder is re-indexed.\n")
|
|
13629
13653
|
);
|
|
@@ -13730,7 +13754,7 @@ async function main() {
|
|
|
13730
13754
|
if (!wasPromptCancelled(e)) throw e;
|
|
13731
13755
|
// The same words the pickers print, for the same event — a cancel should not read as one
|
|
13732
13756
|
// thing when the answer was typed and another when it was arrowed to.
|
|
13733
|
-
console.log(dim("
|
|
13757
|
+
console.log(dim(" (cancelled)\n"));
|
|
13734
13758
|
continue;
|
|
13735
13759
|
}
|
|
13736
13760
|
// SWITCH TO ANOTHER AGENT (task #211) — chosen with the mouse in showOtherAgents, delivered
|