@tiens.nguyen/gu-cli 1.0.795 → 1.0.797
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/client-expert-api.mjs +1 -1
- package/coding-choice-tags.mjs +1 -1
- package/coding-key-prompt.mjs +4 -4
- package/device-login.mjs +1 -1
- package/embed-model-command.mjs +37 -0
- package/gonext_agent_chat.py +209 -28
- package/gu-cli.mjs +39 -4
- package/gu-repl.mjs +288 -199
- package/log-level.mjs +193 -0
- package/model-prices.mjs +27 -0
- package/model-setup-ui.mjs +1 -1
- package/ollama-setup.mjs +5 -5
- package/package.json +2 -1
- package/proxy-config.mjs +3 -3
- package/terminal-hover.mjs +32 -1
- package/terminal-layout.mjs +55 -0
package/gu-repl.mjs
CHANGED
|
@@ -138,7 +138,8 @@ import { unapprovedProjectServers } from "./mcp-command.mjs";
|
|
|
138
138
|
import { readHooks } from "./hooks-config.mjs";
|
|
139
139
|
import { readSkillSource, sourceLabel, SKILL_SOURCE_KEY } from "./skill-source.mjs";
|
|
140
140
|
import { codingBannerLabel, isOllamaCloudUrl, SECRET_KEY_HINT } from "./coding-key-prompt.mjs";
|
|
141
|
-
import { thoughtPrintSeq } from "./terminal-layout.mjs";
|
|
141
|
+
import { questionEchoText, thoughtPrintSeq } from "./terminal-layout.mjs";
|
|
142
|
+
import { embedNotice, embedState } from "./embed-model-command.mjs";
|
|
142
143
|
import {
|
|
143
144
|
MODELFALLBACK_PREFIX, fallbackAfterTurn, fallbackHistoryLine, fallbackNotice, keyMissingNotice,
|
|
144
145
|
parseModelFallback,
|
|
@@ -160,7 +161,7 @@ import {
|
|
|
160
161
|
registeredRange,
|
|
161
162
|
selectionKeyAction,
|
|
162
163
|
} from "./terminal-copy.mjs";
|
|
163
|
-
import { createHoverController, applyToggleShift } from "./terminal-hover.mjs";
|
|
164
|
+
import { createHoverController, applyToggleShift, rowsDiffer } from "./terminal-hover.mjs";
|
|
164
165
|
// The required pair, imported rather than re-typed: the startup "no models" message names them,
|
|
165
166
|
// and a second copy of these strings would drift from the check that enforces them.
|
|
166
167
|
import { ACCEPTED_EMBED_MODELS, REQUIRED_AGENT_MODEL, REQUIRED_AGENT_PREFIX } from "./ollama-setup.mjs";
|
|
@@ -587,13 +588,15 @@ if (cliVerdict.kind === "run") {
|
|
|
587
588
|
} else if (argv.includes("--help") || argv.includes("-h")) {
|
|
588
589
|
console.log(
|
|
589
590
|
"gu — terminal agent REPL for the current folder\n\n" +
|
|
590
|
-
"Usage: gu [-v|--verbose] [--license <pro token>]\n" +
|
|
591
|
+
"Usage: gu [-v|--verbose] [--log] [--license <pro token>]\n" +
|
|
591
592
|
" gu status # worker + a table of the running model servers\n" +
|
|
592
593
|
" gu start|stop|restart|logs # manage the background worker\n" +
|
|
593
594
|
" gu logs <port|pid> [-f] # tail a running model server's log\n" +
|
|
594
595
|
" gu doctor|login|set # models / account (same as `gu-cli …`)\n" +
|
|
595
596
|
" gu rag # is RAG working? tests the embedder end-to-end\n\n" +
|
|
596
597
|
" -v, --verbose show RAG index-file activity in the bottom bar (each shard written)\n" +
|
|
598
|
+
" --log log this run in detail (~/.gonext/worker.log). Without it only starts,\n" +
|
|
599
|
+
" stops and errors are logged. Ends when this gu closes.\n" +
|
|
597
600
|
" --license <token> pro license: a first install ASKS how this machine runs (hosted\n" +
|
|
598
601
|
" client / MLX host / private). Without it, a first install is\n" +
|
|
599
602
|
" private by default — the whole stack on this machine.\n" +
|
|
@@ -613,6 +616,17 @@ if (cliVerdict.kind === "run") {
|
|
|
613
616
|
// could not deliver, because agent LOG events stay on the worker's console and never enter
|
|
614
617
|
// the job stream. It now drives a real, streamable signal.
|
|
615
618
|
const REPL_VERBOSE = argv.includes("-v") || argv.includes("--verbose");
|
|
619
|
+
// --log (task #352): THIS RUN logs in detail. By default the worker writes only starts, stops and
|
|
620
|
+
// errors; a run started with --log takes a lease (its pid, in ~/.gonext/log-detail.json) that the
|
|
621
|
+
// worker reads, and gives it back on exit. Per run by the owner's rule — nothing is saved, and a
|
|
622
|
+
// run that is killed leaves a lease for a dead process, which is no lease.
|
|
623
|
+
const REPL_LOG = argv.includes("--log");
|
|
624
|
+
if (REPL_LOG) {
|
|
625
|
+
const ll = await import("./log-level.mjs");
|
|
626
|
+
await ll.takeLease().catch(() => {});
|
|
627
|
+
process.on("exit", () => ll.releaseLeaseSync());
|
|
628
|
+
console.log(dim(" --log: detailed logging is on for this run (~/.gonext/worker.log); back to minimal when gu closes."));
|
|
629
|
+
}
|
|
616
630
|
if (REPL_VERBOSE) {
|
|
617
631
|
process.env.GONEXT_REPL_VERBOSE = "1";
|
|
618
632
|
}
|
|
@@ -847,7 +861,7 @@ if (!workerKey || !apiBase) {
|
|
|
847
861
|
console.error(red(`gu: setup failed: ${e instanceof Error ? e.message : e}`));
|
|
848
862
|
if (e && e.code === "PAIRING_UNSUPPORTED") {
|
|
849
863
|
console.error(
|
|
850
|
-
"Configure manually: gu-cli set <workerKey> --api-base https://chat-api-v2.
|
|
864
|
+
"Configure manually: gu-cli set <workerKey> --api-base https://chat-api-v2.getmarsic.cc"
|
|
851
865
|
);
|
|
852
866
|
}
|
|
853
867
|
if (e && (e.code === "KEY_REJECTED" || e.code === "KEY_UNCHECKED")) {
|
|
@@ -862,7 +876,7 @@ if (!workerKey || !apiBase) {
|
|
|
862
876
|
console.error(
|
|
863
877
|
red("gu: no worker key / API base found.") +
|
|
864
878
|
`\nRun ${accent("gu-cli login")} to sign in, or configure manually:` +
|
|
865
|
-
"\n gu-cli set <workerKey> --api-base https://chat-api-v2.
|
|
879
|
+
"\n gu-cli set <workerKey> --api-base https://chat-api-v2.getmarsic.cc"
|
|
866
880
|
);
|
|
867
881
|
process.exit(1);
|
|
868
882
|
}
|
|
@@ -1633,11 +1647,14 @@ let agentMissing = false; // no agent model chosen → agent falls back to the c
|
|
|
1633
1647
|
// question to. Seeded from the startup payload; re-checked before a question is refused.
|
|
1634
1648
|
let noModelAtAll = false;
|
|
1635
1649
|
let embedMissing = false; // no embed model chosen → RAG is off until /embed-model picks one
|
|
1650
|
+
// #347: WHY it is off — "unset" (none chosen) or "off" (one IS chosen, RAG is switched off) — and
|
|
1651
|
+
// the chosen model's name, so the row never says "no embed model" about a model that is set.
|
|
1652
|
+
let embedWhy = { state: "unset", model: "" };
|
|
1636
1653
|
// One footer row; the two halves are independent so a model that IS set is never named.
|
|
1637
1654
|
// ONE SHORT ROW EACH (#317): joined, they ran past 80 columns and were cut mid-word.
|
|
1638
1655
|
const missingNoticeRows = () => [
|
|
1639
1656
|
...(agentMissing ? ["⚠ agent model not set — using the coding model · /agent-model"] : []),
|
|
1640
|
-
...(embedMissing ? ["⚠ no embed model — RAG is off · /embed-model"] : []),
|
|
1657
|
+
...(embedMissing ? [embedNotice(embedWhy) || "⚠ no embed model — RAG is off · /embed-model"] : []),
|
|
1641
1658
|
];
|
|
1642
1659
|
|
|
1643
1660
|
// ANOTHER MODEL STOOD IN (task #343). The agent reports it as a __MODELFALLBACK__:: step; it is
|
|
@@ -1662,6 +1679,30 @@ const fallbackNoticeRows = () => {
|
|
|
1662
1679
|
];
|
|
1663
1680
|
};
|
|
1664
1681
|
|
|
1682
|
+
/**
|
|
1683
|
+
* Set the embed warning from what the settings SAY (task #347) — the one place it is decided
|
|
1684
|
+
* after startup's first guess.
|
|
1685
|
+
*
|
|
1686
|
+
* `repair`: an embedder is chosen but RAG is off. On a Local-mode machine the only way to choose
|
|
1687
|
+
* one is /embed-model or setup, and until this fix /embed-model never switched RAG on — so that
|
|
1688
|
+
* state is this bug's leftover, and it is finished here instead of asking the person to choose
|
|
1689
|
+
* again. In Cloud mode the web Settings has its own RAG switch; "off" may be a decision, so it is
|
|
1690
|
+
* reported, not changed.
|
|
1691
|
+
*/
|
|
1692
|
+
async function syncEmbedWarning({ repair = false } = {}) {
|
|
1693
|
+
const settings = await fetchWorkerSettings();
|
|
1694
|
+
if (!settings) return; // offline: keep what the bar already says
|
|
1695
|
+
let st = embedState(settings);
|
|
1696
|
+
if (st.state === "off" && repair && isClientExpert()) {
|
|
1697
|
+
const md = await import("./model-doctor.mjs");
|
|
1698
|
+
const saved = await md.saveWorkerSettings(apiBase, workerKey, { ragEnabled: true }).catch(() => false);
|
|
1699
|
+
if (saved) st = { ...st, state: "on" };
|
|
1700
|
+
}
|
|
1701
|
+
embedWhy = st;
|
|
1702
|
+
embedMissing = st.state !== "on";
|
|
1703
|
+
if (barPinned()) paintBar();
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1665
1706
|
// #307: the calm note after a quick change of mind — dim, not the ⚠ orange of a warning, and in the
|
|
1666
1707
|
// bar rather than the transcript (which the whole point is to leave untouched).
|
|
1667
1708
|
let stopNotice = "";
|
|
@@ -2418,10 +2459,10 @@ async function runUpdate() {
|
|
|
2418
2459
|
if (cli) rows.push({ id: "cli", label: `Update gu to ${cli}`, note: "takes effect after you restart gu" });
|
|
2419
2460
|
if (!rows.length) return;
|
|
2420
2461
|
console.log("");
|
|
2421
|
-
console.log(dim(`
|
|
2462
|
+
console.log(dim(` Update (${PICK_HINT}):`));
|
|
2422
2463
|
const idx = await pickFromList(rows.map((r) => r.label + dim(` · ${r.note}`)), 0);
|
|
2423
2464
|
if (idx < 0) {
|
|
2424
|
-
console.log(dim("
|
|
2465
|
+
console.log(dim(" (cancelled)\n"));
|
|
2425
2466
|
paintBar();
|
|
2426
2467
|
return;
|
|
2427
2468
|
}
|
|
@@ -2448,7 +2489,7 @@ async function runUpdate() {
|
|
|
2448
2489
|
r.stderr ? ` — ${String(r.stderr).trim().split("\n").slice(-1)[0]}` : ""}`);
|
|
2449
2490
|
}
|
|
2450
2491
|
});
|
|
2451
|
-
console.log(accent(`
|
|
2492
|
+
console.log(accent(` ✓ API updated to ${api}\n`));
|
|
2452
2493
|
availableUpdates = { ...availableUpdates, api: "", any: Boolean(availableUpdates.cli) };
|
|
2453
2494
|
} else {
|
|
2454
2495
|
await withPendingStatus(`updating gu to ${cli}`, async () => {
|
|
@@ -2478,12 +2519,12 @@ async function runUpdate() {
|
|
|
2478
2519
|
});
|
|
2479
2520
|
// NOT "updated" — the files changed, THIS process did not. Claiming otherwise is the one
|
|
2480
2521
|
// way this feature could lie to someone.
|
|
2481
|
-
console.log(accent(`
|
|
2522
|
+
console.log(accent(` ✓ gu ${cli} installed`) +
|
|
2482
2523
|
dim(` — ${updateRestartNote()}\n`));
|
|
2483
2524
|
availableUpdates = { ...availableUpdates, cli: "", any: Boolean(availableUpdates.api) };
|
|
2484
2525
|
}
|
|
2485
2526
|
} catch (e) {
|
|
2486
|
-
console.log(red(`
|
|
2527
|
+
console.log(red(` update failed: ${e?.message ?? e}\n`));
|
|
2487
2528
|
}
|
|
2488
2529
|
paintBar();
|
|
2489
2530
|
}
|
|
@@ -2577,12 +2618,18 @@ const tokenLineText = () => {
|
|
|
2577
2618
|
);
|
|
2578
2619
|
const total = dim(" · ") + dim("total ") + accent(`↓ ${fmtTokens(Math.max(globalOutputTokens, lastTurnTotalOutput))}`);
|
|
2579
2620
|
// #323: what the last turn's prompt cost, LAST on the row after a "|" — "~" when the cached share
|
|
2580
|
-
// was assumed;
|
|
2621
|
+
// was assumed; "no price" when the model has none (never a misleading "| $0"). It survives a
|
|
2581
2622
|
// narrow window: "total ↓" is dropped before it is (tokenRowWithPrice).
|
|
2582
2623
|
return tokenRowWithPrice({
|
|
2583
2624
|
withTotal: head + total,
|
|
2584
2625
|
withoutTotal: head,
|
|
2585
|
-
|
|
2626
|
+
// #349: a model with NO price says so ("no price", dim) — the blank it used to leave read as
|
|
2627
|
+
// the cost feature being broken. Still never "$0": unpriced is not free.
|
|
2628
|
+
price: lastTurnCost?.usd != null
|
|
2629
|
+
? dim(" | ") + accent(fmtCost(lastTurnCost))
|
|
2630
|
+
: lastTurnCost
|
|
2631
|
+
? dim(" | no price")
|
|
2632
|
+
: "",
|
|
2586
2633
|
room: (process.stdout.columns || 80) - 1 - visibleLen(GUTTER),
|
|
2587
2634
|
});
|
|
2588
2635
|
};
|
|
@@ -3348,6 +3395,7 @@ const hoverCtl = createHoverController({
|
|
|
3348
3395
|
pinned: () => barPinned(),
|
|
3349
3396
|
transcriptRows: () => transcriptRows(),
|
|
3350
3397
|
blockAtRow: (row) => thoughtBlockAtRow(row),
|
|
3398
|
+
canToggle: (b) => blockCanToggle(b),
|
|
3351
3399
|
paintRows: (b, on) => paintThoughtRows(b, on),
|
|
3352
3400
|
replaceRange: (from, to, rows) => transcript.replaceRange(from, to, rows),
|
|
3353
3401
|
repaintSeq: (b) => thoughtBlockRepaintSeq(b),
|
|
@@ -3646,9 +3694,20 @@ function copyTargetAtRow(row) {
|
|
|
3646
3694
|
return { block, renderedLines: lines.slice(from, to + 1) };
|
|
3647
3695
|
}
|
|
3648
3696
|
|
|
3697
|
+
/**
|
|
3698
|
+
* Would a click on this block change anything? (#350) — its collapsed and its expanded rows, laid
|
|
3699
|
+
* out for THIS window, are not the same. Asked by the hover (may it light up?), by the click and
|
|
3700
|
+
* by Ctrl+T, so the three can never disagree. See rowsDiffer in terminal-hover.mjs.
|
|
3701
|
+
*/
|
|
3702
|
+
function blockCanToggle(block) {
|
|
3703
|
+
if (!block) return false;
|
|
3704
|
+
return rowsDiffer(paintThoughtRows(block, false, true), paintThoughtRows(block, false, false));
|
|
3705
|
+
}
|
|
3706
|
+
|
|
3649
3707
|
function toggleThoughtAtRow(row) {
|
|
3650
3708
|
const block = thoughtBlockAtRow(row);
|
|
3651
|
-
|
|
3709
|
+
// A block shown whole has no other form: it did not light up, and the click is not for it.
|
|
3710
|
+
return block && blockCanToggle(block) ? toggleThoughtBlock(block) : false;
|
|
3652
3711
|
}
|
|
3653
3712
|
|
|
3654
3713
|
/**
|
|
@@ -3718,8 +3777,12 @@ function toggleThoughtBlock(block) {
|
|
|
3718
3777
|
|
|
3719
3778
|
/** Expand/collapse the most recently printed thought block. The keyboard path (Ctrl+T). */
|
|
3720
3779
|
function toggleNewestThought() {
|
|
3721
|
-
|
|
3722
|
-
|
|
3780
|
+
if (!barPinned()) return false;
|
|
3781
|
+
// The newest block that HAS another form (#350): Ctrl+T on a thought already shown whole did
|
|
3782
|
+
// nothing visible, while an expandable one sat just above it.
|
|
3783
|
+
let block = null;
|
|
3784
|
+
for (let i = thoughtBlocks.length - 1; i >= 0 && !block; i--) if (blockCanToggle(thoughtBlocks[i])) block = thoughtBlocks[i];
|
|
3785
|
+
if (!block) return false;
|
|
3723
3786
|
return toggleThoughtBlock(block);
|
|
3724
3787
|
}
|
|
3725
3788
|
|
|
@@ -4254,8 +4317,8 @@ if (process.stdin.isTTY) {
|
|
|
4254
4317
|
// PROCESS (reported: "PromptCancelled: cancelled … Node.js v22"). Cancel is caught here; any
|
|
4255
4318
|
// other failure is reported and the session goes on — a keypress must not kill the REPL.
|
|
4256
4319
|
replaceCodingKey().catch((e) => {
|
|
4257
|
-
if (wasPromptCancelled(e)) console.log(dim("
|
|
4258
|
-
else console.log(red(`
|
|
4320
|
+
if (wasPromptCancelled(e)) console.log(dim(" (cancelled)\n"));
|
|
4321
|
+
else console.log(red(` ctrl+k failed: ${e?.message ?? e}\n`));
|
|
4259
4322
|
paintBar();
|
|
4260
4323
|
});
|
|
4261
4324
|
return;
|
|
@@ -4617,13 +4680,13 @@ const ask = async (q, { prefill = "", hint = [], atomic = false, noCancel = fals
|
|
|
4617
4680
|
rl.line = String(prefill);
|
|
4618
4681
|
rl.cursor = rl.line.length;
|
|
4619
4682
|
}
|
|
4620
|
-
// ELLIPSIS, so a truncation is VISIBLE. Silently cutting "…[https://ollama2.
|
|
4683
|
+
// ELLIPSIS, so a truncation is VISIBLE. Silently cutting "…[https://ollama2.getmarsic.cc/v1]:"
|
|
4621
4684
|
// to "…[https:/" hides the default Enter would accept, which is worse than not showing it —
|
|
4622
4685
|
// the reader cannot tell whether the value is short or the row lied. Callers should keep the
|
|
4623
4686
|
// final line short and put the long part above; this is the guard, not the plan.
|
|
4624
4687
|
// "… " with the space: the label normally ends in ": ", and a truncation that eats the
|
|
4625
4688
|
// separator leaves the typed text jammed against the last surviving word
|
|
4626
|
-
// ("agent + embed ser…ollama2.
|
|
4689
|
+
// ("agent + embed ser…ollama2.getmarsic.cc"). Two columns for the ellipsis and its space.
|
|
4627
4690
|
inlinePrompt =
|
|
4628
4691
|
(visibleLen(label) > budget ? clipVisible(label, budget - 2) + "… " : label) || PROMPT;
|
|
4629
4692
|
try {
|
|
@@ -5603,10 +5666,10 @@ async function runModelDoctor() {
|
|
|
5603
5666
|
const dc = await import("./doctor-check.mjs");
|
|
5604
5667
|
const settings = await withPendingStatus("reading your model settings", fetchWorkerSettings);
|
|
5605
5668
|
if (!settings) {
|
|
5606
|
-
console.log(red("
|
|
5669
|
+
console.log(red(" couldn't reach the API to read your model settings.\n"));
|
|
5607
5670
|
return;
|
|
5608
5671
|
}
|
|
5609
|
-
console.log(dim("
|
|
5672
|
+
console.log(dim(" Asking each model to answer — the real request, not just a ping."));
|
|
5610
5673
|
const roles = dc.rolesFromSettings(settings);
|
|
5611
5674
|
const results = [];
|
|
5612
5675
|
for (const r of roles) {
|
|
@@ -5649,7 +5712,7 @@ async function reconfigureOllamaServer() {
|
|
|
5649
5712
|
|
|
5650
5713
|
const settings = await withPendingStatus("reading your model settings", fetchWorkerSettings);
|
|
5651
5714
|
if (!settings) {
|
|
5652
|
-
console.log(red("
|
|
5715
|
+
console.log(red(" couldn't reach the API to read your current settings.\n"));
|
|
5653
5716
|
return;
|
|
5654
5717
|
}
|
|
5655
5718
|
|
|
@@ -5678,7 +5741,7 @@ async function reconfigureOllamaServer() {
|
|
|
5678
5741
|
console.log(dim(` ${hint()}`));
|
|
5679
5742
|
// askSecret: masked, kept out of history; Esc throws PromptCancelled → /model is cancelled
|
|
5680
5743
|
// with nothing changed, which is what "skip all" means here.
|
|
5681
|
-
const k = (await askSecret(`
|
|
5744
|
+
const k = (await askSecret(` API key for ${host} (Enter for none): `)).trim();
|
|
5682
5745
|
typedKeys.set(host, k);
|
|
5683
5746
|
console.log(GUTTER + dim(ui.keyConfirmation(host, k)));
|
|
5684
5747
|
return k;
|
|
@@ -5694,7 +5757,7 @@ async function reconfigureOllamaServer() {
|
|
|
5694
5757
|
// instruction, leaving the reader unsure whether their own address needs it. It does not
|
|
5695
5758
|
// — normalizeOllamaUrl adds it — and displayUrl round-trips, so what is shown can be fed
|
|
5696
5759
|
// straight back as the default.
|
|
5697
|
-
console.log(dim(`
|
|
5760
|
+
console.log(dim(` ${label} — Enter keeps ${os.displayUrl(dflt)}`));
|
|
5698
5761
|
console.log(dim(` ${hint()}`));
|
|
5699
5762
|
// THE ROW MUST SAY WHICH SERVER IT IS ASKING FOR. It said a bare "server:" — chosen to
|
|
5700
5763
|
// fit the frozen row, which traded away the one thing the label was for. Two questions
|
|
@@ -5704,13 +5767,13 @@ async function reconfigureOllamaServer() {
|
|
|
5704
5767
|
const typed = (await ask(` ${short}: `)).trim();
|
|
5705
5768
|
// "s" is the advertised skip (#301); the older words still work.
|
|
5706
5769
|
if (ui.readStepAnswer(typed).kind === "skip") return null;
|
|
5707
|
-
// EVERY READING OF A BARE HOSTNAME, in order. Typing "ollama2.
|
|
5770
|
+
// EVERY READING OF A BARE HOSTNAME, in order. Typing "ollama2.getmarsic.cc" means
|
|
5708
5771
|
// https://…:443 behind a proxy, while "192.168.1.40" means http://…:11434 — one guess
|
|
5709
5772
|
// is right for one of them and produces a timeout that looks like a dead server for the
|
|
5710
5773
|
// other. See urlCandidates; the load decides, not the heuristic.
|
|
5711
5774
|
const candidates = os.urlCandidates(typed || dflt);
|
|
5712
5775
|
if (!candidates.length) {
|
|
5713
|
-
console.log(dim("
|
|
5776
|
+
console.log(dim(" that is not a server address — try 192.168.1.40 or a full URL"));
|
|
5714
5777
|
continue;
|
|
5715
5778
|
}
|
|
5716
5779
|
let n = null;
|
|
@@ -5725,7 +5788,7 @@ async function reconfigureOllamaServer() {
|
|
|
5725
5788
|
try {
|
|
5726
5789
|
models = await load(cand, key);
|
|
5727
5790
|
n = cand;
|
|
5728
|
-
if (candidates.length > 1) console.log(dim(`
|
|
5791
|
+
if (candidates.length > 1) console.log(dim(` using ${os.displayUrl(cand.url)}`));
|
|
5729
5792
|
return;
|
|
5730
5793
|
} catch (err) {
|
|
5731
5794
|
lastErr = err;
|
|
@@ -5735,31 +5798,31 @@ async function reconfigureOllamaServer() {
|
|
|
5735
5798
|
await tryAll();
|
|
5736
5799
|
// Refused for want of credentials: ask now, whatever the address looked like, and list again.
|
|
5737
5800
|
if (!n && ui.needsKeyAfterStatus(lastErr?.httpStatus) && !key) {
|
|
5738
|
-
console.log(yellow(`
|
|
5801
|
+
console.log(yellow(` ${candidates[0].host} needs an API key to list its models.`));
|
|
5739
5802
|
typedKeys.delete(candidates[0].host);
|
|
5740
5803
|
key = await askKeyFor(candidates[0].host);
|
|
5741
5804
|
if (key) await tryAll();
|
|
5742
5805
|
}
|
|
5743
5806
|
if (!n && ui.needsKeyAfterStatus(lastErr?.httpStatus)) {
|
|
5744
|
-
console.log(red(`
|
|
5807
|
+
console.log(red(` ${candidates[0].host} refused the request (HTTP ${lastErr.httpStatus})${key ? " — that key was not accepted" : ""}.`));
|
|
5745
5808
|
continue;
|
|
5746
5809
|
}
|
|
5747
5810
|
if (!n) {
|
|
5748
5811
|
if (lastErr?.httpStatus === 404) {
|
|
5749
5812
|
// IT ANSWERED — it is simply not an Ollama (#43). "Couldn't reach … is Ollama listening"
|
|
5750
5813
|
// sent people to debug a server that was working.
|
|
5751
|
-
for (const l of am.describeNotOllama(candidates[0].host, role)) console.log(yellow(`
|
|
5814
|
+
for (const l of am.describeNotOllama(candidates[0].host, role)) console.log(yellow(` ${l}`));
|
|
5752
5815
|
continue;
|
|
5753
5816
|
}
|
|
5754
5817
|
// One address typed, one failure reported — naming every candidate would be two errors
|
|
5755
5818
|
// for one mistake, and neither would be the address they gave.
|
|
5756
|
-
console.log(red(`
|
|
5757
|
-
console.log(dim("
|
|
5819
|
+
console.log(red(` couldn't reach ${candidates[0].host}: ${os.networkReason(lastErr)}`));
|
|
5820
|
+
console.log(dim(" check the address, that Ollama is listening on the network, and /proxy."));
|
|
5758
5821
|
continue;
|
|
5759
5822
|
}
|
|
5760
5823
|
const usable = os.usableModelList(models);
|
|
5761
5824
|
if (!usable.ok) {
|
|
5762
|
-
for (const l of os.describeUnusableList(usable.why, n.host)) console.log(dim(`
|
|
5825
|
+
for (const l of os.describeUnusableList(usable.why, n.host)) console.log(dim(` ${l}`));
|
|
5763
5826
|
continue;
|
|
5764
5827
|
}
|
|
5765
5828
|
if (require) {
|
|
@@ -5768,13 +5831,13 @@ async function reconfigureOllamaServer() {
|
|
|
5768
5831
|
// rather than the server.
|
|
5769
5832
|
const need = os.checkRequired(models);
|
|
5770
5833
|
if (!need.ok) {
|
|
5771
|
-
for (const l of os.describeNotReady(n.host, need.missing)) console.log(yellow(`
|
|
5772
|
-
console.log(dim("
|
|
5834
|
+
for (const l of os.describeNotReady(n.host, need.missing)) console.log(yellow(` ${l}`));
|
|
5835
|
+
console.log(dim(" (or type s to skip — you can set it later with /model)"));
|
|
5773
5836
|
continue;
|
|
5774
5837
|
}
|
|
5775
|
-
console.log(dim(`
|
|
5838
|
+
console.log(dim(` ✓ ${n.host} has the ${os.REQUIRED_AGENT_PREFIX} family and the embedder`));
|
|
5776
5839
|
}
|
|
5777
|
-
console.log(dim(`
|
|
5840
|
+
console.log(dim(` found ${models.length} model${models.length === 1 ? "" : "s"} on ${n.host}`));
|
|
5778
5841
|
return { normalized: n, models };
|
|
5779
5842
|
}
|
|
5780
5843
|
return null;
|
|
@@ -5785,17 +5848,17 @@ async function reconfigureOllamaServer() {
|
|
|
5785
5848
|
// ask again. Keep it, say where it is changed, and go on to the coder.
|
|
5786
5849
|
const agentOnProvider = am.agentIsOnProvider(settings);
|
|
5787
5850
|
const curAgent =
|
|
5788
|
-
String(settings.agentModelUrl ?? "").trim() || "https://ollama1.
|
|
5851
|
+
String(settings.agentModelUrl ?? "").trim() || "https://ollama1.getmarsic.cc/v1";
|
|
5789
5852
|
const curCode = am.coderServerDefault(settings);
|
|
5790
5853
|
|
|
5791
5854
|
let agent = null;
|
|
5792
5855
|
if (agentOnProvider) {
|
|
5793
5856
|
const host = hostOf(String(settings.agentModelUrl ?? ""));
|
|
5794
|
-
console.log(dim(`
|
|
5795
|
-
console.log(dim("
|
|
5857
|
+
console.log(dim(` The agent model is on ${host} (OpenAI-compatible) — kept as it is; change it with /agent-model.`));
|
|
5858
|
+
console.log(dim(" Enter keeps the current address."));
|
|
5796
5859
|
} else {
|
|
5797
|
-
console.log(dim("
|
|
5798
|
-
console.log(dim("
|
|
5860
|
+
console.log(dim(" TWO servers may be used: the agent + embedder on one, the coder on another."));
|
|
5861
|
+
console.log(dim(" Enter keeps the current address."));
|
|
5799
5862
|
agent = await askServer("Agent + embedding server", curAgent, {
|
|
5800
5863
|
require: true,
|
|
5801
5864
|
short: "agent + embed server",
|
|
@@ -5815,7 +5878,7 @@ async function reconfigureOllamaServer() {
|
|
|
5815
5878
|
}
|
|
5816
5879
|
|
|
5817
5880
|
if (!agent && !coder) {
|
|
5818
|
-
console.log(dim("
|
|
5881
|
+
console.log(dim(" nothing set — models stay as they are.\n"));
|
|
5819
5882
|
return;
|
|
5820
5883
|
}
|
|
5821
5884
|
|
|
@@ -5838,13 +5901,13 @@ async function reconfigureOllamaServer() {
|
|
|
5838
5901
|
const size = am.formatSize(m.sizeBytes);
|
|
5839
5902
|
return `${m.model}${size ? dim(` ${size}`) : ""}${m.model === agentDefault ? dim(" (current)") : ""}`;
|
|
5840
5903
|
});
|
|
5841
|
-
console.log(dim(`
|
|
5904
|
+
console.log(dim(` AGENT model — routes every turn, writes the plain replies (${PICK_HINT}):`));
|
|
5842
5905
|
const pick = await pickFromList(
|
|
5843
5906
|
labels,
|
|
5844
5907
|
Math.max(0, cands.findIndex((m) => m.model === agentDefault))
|
|
5845
5908
|
);
|
|
5846
5909
|
if (pick < 0) {
|
|
5847
|
-
console.log(dim("
|
|
5910
|
+
console.log(dim(" (cancelled — nothing changed)\n"));
|
|
5848
5911
|
return;
|
|
5849
5912
|
}
|
|
5850
5913
|
chat = cands[pick].model;
|
|
@@ -5863,10 +5926,10 @@ async function reconfigureOllamaServer() {
|
|
|
5863
5926
|
const kind = am.isLikelyEmbedder(m) ? dim(" · embedder") : "";
|
|
5864
5927
|
return `${m.model}${size ? dim(` ${size}`) : ""}${kind}${m.model === roles.code ? dim(" (current)") : ""}`;
|
|
5865
5928
|
});
|
|
5866
|
-
console.log(dim(`
|
|
5929
|
+
console.log(dim(` CODING model — writes the code; worth waiting for (${PICK_HINT}):`));
|
|
5867
5930
|
const idx = await pickFromList(labels, Math.max(0, coder.models.findIndex((m) => m.model === roles.code)));
|
|
5868
5931
|
if (idx < 0) {
|
|
5869
|
-
console.log(dim("
|
|
5932
|
+
console.log(dim(" (cancelled — nothing changed)\n"));
|
|
5870
5933
|
return;
|
|
5871
5934
|
}
|
|
5872
5935
|
code = coder.models[idx].model;
|
|
@@ -5881,7 +5944,7 @@ async function reconfigureOllamaServer() {
|
|
|
5881
5944
|
.flatMap((b) => (Array.isArray(b.models) ? b.models : []))
|
|
5882
5945
|
);
|
|
5883
5946
|
console.log(
|
|
5884
|
-
dim(`
|
|
5947
|
+
dim(` Also offer these in /model? (up to ${MAX_ALT_CODERS} · ${PICK_MANY_HINT}):`)
|
|
5885
5948
|
);
|
|
5886
5949
|
const ticks = await pickManyFromList(
|
|
5887
5950
|
rest.map((m) => `${m.model}${am.formatSize(m.sizeBytes) ? dim(` ${am.formatSize(m.sizeBytes)}`) : ""}`),
|
|
@@ -5890,7 +5953,7 @@ async function reconfigureOllamaServer() {
|
|
|
5890
5953
|
);
|
|
5891
5954
|
// null is Escape and cancels the command; an EMPTY list is a legitimate "offer none".
|
|
5892
5955
|
if (ticks === null) {
|
|
5893
|
-
console.log(dim("
|
|
5956
|
+
console.log(dim(" (cancelled — nothing changed)\n"));
|
|
5894
5957
|
return;
|
|
5895
5958
|
}
|
|
5896
5959
|
extras = ticks.map((i) => rest[i].model);
|
|
@@ -5936,7 +5999,7 @@ async function reconfigureOllamaServer() {
|
|
|
5936
5999
|
md.saveWorkerSettings(apiBase, workerKey, patch)
|
|
5937
6000
|
);
|
|
5938
6001
|
if (!saved) {
|
|
5939
|
-
console.log(red("
|
|
6002
|
+
console.log(red(" couldn't save that to this machine — nothing changed."));
|
|
5940
6003
|
console.log(GUTTER + dim("if this machine's local API is old, restart it with `gu api restart`.\n"));
|
|
5941
6004
|
return;
|
|
5942
6005
|
}
|
|
@@ -5956,13 +6019,13 @@ async function reconfigureOllamaServer() {
|
|
|
5956
6019
|
const url = used.find((u) => hostnameOf(u) === host);
|
|
5957
6020
|
const found = ps.providerRowForUrl(ps.existingBackendRows(fresh ?? {}), url);
|
|
5958
6021
|
if (!found) {
|
|
5959
|
-
console.log(yellow(`
|
|
6022
|
+
console.log(yellow(` no server row holds ${host} yet — add its key with ctrl+k in /model.`));
|
|
5960
6023
|
continue;
|
|
5961
6024
|
}
|
|
5962
6025
|
const ok = await withPendingStatus("saving the key", () => addCodingKey(host, k, found.id));
|
|
5963
6026
|
console.log(ok
|
|
5964
6027
|
? GUTTER + dim(`✓ key stored for ${host} — the server's default key`)
|
|
5965
|
-
: red(`
|
|
6028
|
+
: red(` couldn't store the key for ${host} — add it with ctrl+k.`));
|
|
5966
6029
|
}
|
|
5967
6030
|
}
|
|
5968
6031
|
}
|
|
@@ -5980,11 +6043,11 @@ async function reconfigureOllamaServer() {
|
|
|
5980
6043
|
for (const u of cloudUrls) {
|
|
5981
6044
|
const found = ps.providerRowForUrl(ps.existingBackendRows(fresh ?? {}), u);
|
|
5982
6045
|
if (found && !am.boundKeyFor(keys, found.id)) {
|
|
5983
|
-
console.log(yellow(`
|
|
6046
|
+
console.log(yellow(` ${hostOf(u)} is the Ollama cloud — it needs its own API key (the model list worked without one).`));
|
|
5984
6047
|
// The shared key prompt (task #245); the server's first key becomes its default.
|
|
5985
6048
|
const { name, secret } = await askServerKey({ host: hostOf(u) });
|
|
5986
6049
|
if (secret && !(await withPendingStatus("saving the key", () => addCodingKey(name, secret, found.id)))) {
|
|
5987
|
-
console.log(red("
|
|
6050
|
+
console.log(red(" couldn't save the key — add it with ctrl+k.\n"));
|
|
5988
6051
|
}
|
|
5989
6052
|
}
|
|
5990
6053
|
}
|
|
@@ -6020,7 +6083,7 @@ async function reconfigureOllamaServer() {
|
|
|
6020
6083
|
if (g !== null) globalOutputTokens = g;
|
|
6021
6084
|
}
|
|
6022
6085
|
paintBar(); // …and show all of it now, not at the next keystroke
|
|
6023
|
-
console.log(accent("
|
|
6086
|
+
console.log(accent(" ✓ models saved"));
|
|
6024
6087
|
if (agent) {
|
|
6025
6088
|
console.log(dim(` chat ${chat} @ ${agent.normalized.host}`));
|
|
6026
6089
|
console.log(dim(` RAG ${os.defaultEmbedModel?.(agent.models) ?? os.REQUIRED_EMBED_MODEL} @ ${agent.normalized.host}`));
|
|
@@ -6038,9 +6101,9 @@ async function reconfigureOllamaServer() {
|
|
|
6038
6101
|
// falsely scream "changed" while the seeded value is exactly what settings now hold.
|
|
6039
6102
|
const seededEmbed = agent ? (os.defaultEmbedModel?.(agent.models) ?? os.REQUIRED_EMBED_MODEL) : "";
|
|
6040
6103
|
if (agent && String(settings.ragEmbedModel ?? "") !== seededEmbed) {
|
|
6041
|
-
console.log(dim("
|
|
6104
|
+
console.log(dim(" the embedder changed — existing RAG indexes need rebuilding (/rag)."));
|
|
6042
6105
|
}
|
|
6043
|
-
console.log(dim("
|
|
6106
|
+
console.log(dim(" takes effect on your next question.\n"));
|
|
6044
6107
|
}
|
|
6045
6108
|
|
|
6046
6109
|
// ---------- fetch the ready-to-run agent payload from user settings ----------
|
|
@@ -6091,7 +6154,7 @@ async function reconfigureOllamaServer() {
|
|
|
6091
6154
|
*/
|
|
6092
6155
|
async function pickExtraModels(rest, preChecked = []) {
|
|
6093
6156
|
if (rest.length === 0) return [];
|
|
6094
|
-
console.log(dim(`
|
|
6157
|
+
console.log(dim(` Also offer these in /model? (up to ${MAX_ALT_CODERS} · ${PICK_MANY_HINT}):`));
|
|
6095
6158
|
const already = new Set(preChecked);
|
|
6096
6159
|
const ticks = await pickManyFromList(
|
|
6097
6160
|
rest,
|
|
@@ -6137,7 +6200,7 @@ async function chooseCodingModelFrom(ps, all, question, current) {
|
|
|
6137
6200
|
const idx = await pickFromList(rows, at >= 0 ? at : 0);
|
|
6138
6201
|
if (idx < 0) return null;
|
|
6139
6202
|
if (idx === list.length) {
|
|
6140
|
-
const typed = (await ask("
|
|
6203
|
+
const typed = (await ask(" model name: ")).trim();
|
|
6141
6204
|
return typed || null;
|
|
6142
6205
|
}
|
|
6143
6206
|
if (idx === list.length + 1) {
|
|
@@ -6165,11 +6228,11 @@ async function chooseCodingModelFrom(ps, all, question, current) {
|
|
|
6165
6228
|
async function chooseFromModelList(all, question, current = 0) {
|
|
6166
6229
|
const rows = [...all, "type a model name…"];
|
|
6167
6230
|
const at = typeof current === "string" ? all.indexOf(current) : Number(current) || 0;
|
|
6168
|
-
console.log(dim(`
|
|
6231
|
+
console.log(dim(` ${question} (${PICK_HINT})`));
|
|
6169
6232
|
const idx = await pickFromList(rows, at >= 0 ? at : 0);
|
|
6170
6233
|
if (idx < 0) return null;
|
|
6171
6234
|
if (idx === all.length) {
|
|
6172
|
-
const typed = (await ask("
|
|
6235
|
+
const typed = (await ask(" model name: ")).trim();
|
|
6173
6236
|
return typed || null;
|
|
6174
6237
|
}
|
|
6175
6238
|
return all[idx];
|
|
@@ -6179,7 +6242,7 @@ async function manageCodingProviders() {
|
|
|
6179
6242
|
const ps = await import("./coding-provider-setup.mjs");
|
|
6180
6243
|
const settings = await withPendingStatus("reading your providers", fetchWorkerSettings);
|
|
6181
6244
|
if (!settings) {
|
|
6182
|
-
console.log(red("
|
|
6245
|
+
console.log(red(" couldn't reach the API to read your current settings.\n"));
|
|
6183
6246
|
return;
|
|
6184
6247
|
}
|
|
6185
6248
|
const rows = ps.existingBackendRows(settings);
|
|
@@ -6196,10 +6259,10 @@ async function manageCodingProviders() {
|
|
|
6196
6259
|
);
|
|
6197
6260
|
const ADD = labels.length;
|
|
6198
6261
|
labels.push("add another provider — Claude API, OpenAI, Kimi…");
|
|
6199
|
-
console.log(dim(`
|
|
6262
|
+
console.log(dim(` Providers — pick one to change it, or add another (${PICK_HINT}):`));
|
|
6200
6263
|
const idx = await pickFromList(labels, 0);
|
|
6201
6264
|
if (idx < 0) {
|
|
6202
|
-
console.log(dim("
|
|
6265
|
+
console.log(dim(" (cancelled)\n"));
|
|
6203
6266
|
return;
|
|
6204
6267
|
}
|
|
6205
6268
|
if (idx === ADD) {
|
|
@@ -6214,7 +6277,7 @@ async function manageCodingProviders() {
|
|
|
6214
6277
|
1
|
|
6215
6278
|
);
|
|
6216
6279
|
if (action <= 0) {
|
|
6217
|
-
console.log(dim("
|
|
6280
|
+
console.log(dim(" (nothing changed)\n"));
|
|
6218
6281
|
return;
|
|
6219
6282
|
}
|
|
6220
6283
|
if (action === 1) await changeProviderModel(ps, rows, p);
|
|
@@ -6273,12 +6336,12 @@ async function changeProviderModel(ps, rows, p) {
|
|
|
6273
6336
|
if (models.length) {
|
|
6274
6337
|
model = await chooseCodingModelFrom(ps, models, `Which model should ${p.host} use?`, p.model);
|
|
6275
6338
|
if (model === null) {
|
|
6276
|
-
console.log(dim("
|
|
6339
|
+
console.log(dim(" (nothing changed)\n"));
|
|
6277
6340
|
return;
|
|
6278
6341
|
}
|
|
6279
6342
|
} else {
|
|
6280
6343
|
console.log(GUTTER + dim(`Enter keeps ${p.model || "it unset"}`));
|
|
6281
|
-
model = (await ask("
|
|
6344
|
+
model = (await ask(" model name: ")).trim();
|
|
6282
6345
|
}
|
|
6283
6346
|
if (!model) {
|
|
6284
6347
|
console.log(GUTTER + dim("(nothing changed)\n"));
|
|
@@ -6302,7 +6365,7 @@ async function changeProviderModel(ps, rows, p) {
|
|
|
6302
6365
|
})
|
|
6303
6366
|
);
|
|
6304
6367
|
if (!saved) {
|
|
6305
|
-
console.log(red("
|
|
6368
|
+
console.log(red(" couldn't save that — nothing changed.\n"));
|
|
6306
6369
|
return;
|
|
6307
6370
|
}
|
|
6308
6371
|
// THE FOLDER'S PICK NAMES THE OLD MODEL. Leaving it would send a name this provider no longer
|
|
@@ -6316,14 +6379,14 @@ async function changeProviderModel(ps, rows, p) {
|
|
|
6316
6379
|
await syncActiveCoderWhere();
|
|
6317
6380
|
console.log(GUTTER + dim("this folder's pick named the old model — cleared it."));
|
|
6318
6381
|
}
|
|
6319
|
-
console.log(accent(`
|
|
6382
|
+
console.log(accent(` ✓ ${p.host} → ${model}`) + dim(" · pick it in /model\n"));
|
|
6320
6383
|
}
|
|
6321
6384
|
|
|
6322
6385
|
/** Point a provider at a different URL, keeping its id so saved picks still resolve. */
|
|
6323
6386
|
async function changeProviderUrl(ps, rows, p) {
|
|
6324
6387
|
const md = await import("./model-doctor.mjs");
|
|
6325
6388
|
console.log("\n" + GUTTER + dim(`Enter keeps ${p.url}`));
|
|
6326
|
-
const typed = (await ask("
|
|
6389
|
+
const typed = (await ask(" provider URL: ")).trim();
|
|
6327
6390
|
if (!typed || typed === p.url) {
|
|
6328
6391
|
console.log(GUTTER + dim("(nothing changed)\n"));
|
|
6329
6392
|
return;
|
|
@@ -6348,12 +6411,12 @@ async function changeProviderUrl(ps, rows, p) {
|
|
|
6348
6411
|
})
|
|
6349
6412
|
);
|
|
6350
6413
|
if (!saved) {
|
|
6351
|
-
console.log(red("
|
|
6414
|
+
console.log(red(" couldn't save that — nothing changed.\n"));
|
|
6352
6415
|
return;
|
|
6353
6416
|
}
|
|
6354
6417
|
// The KEY still belongs to this row, because the row id did not change — which is the whole
|
|
6355
6418
|
// reason an id is not derived from the URL.
|
|
6356
|
-
console.log(accent(`
|
|
6419
|
+
console.log(accent(` ✓ ${p.host} → ${r.host}`) + dim(" · its key still applies\n"));
|
|
6357
6420
|
}
|
|
6358
6421
|
|
|
6359
6422
|
/** Remove a provider, and the keys that would be stranded by its removal. */
|
|
@@ -6386,7 +6449,7 @@ async function removeCodingProvider(ps, rows, p) {
|
|
|
6386
6449
|
})
|
|
6387
6450
|
);
|
|
6388
6451
|
if (!saved) {
|
|
6389
|
-
console.log(red("
|
|
6452
|
+
console.log(red(" couldn't save that — nothing was removed.\n"));
|
|
6390
6453
|
return;
|
|
6391
6454
|
}
|
|
6392
6455
|
// THE ROW GOES FIRST. If a key delete fails afterwards the key is merely orphaned, which is
|
|
@@ -6410,7 +6473,7 @@ async function removeCodingProvider(ps, rows, p) {
|
|
|
6410
6473
|
console.log(GUTTER + dim("this folder was using it — cleared the pick.\n"));
|
|
6411
6474
|
}
|
|
6412
6475
|
console.log(
|
|
6413
|
-
accent(`
|
|
6476
|
+
accent(` ✓ removed ${p.host}`) +
|
|
6414
6477
|
(stranded.length ? dim(` · ${deleted} key${deleted === 1 ? "" : "s"} deleted`) : "") +
|
|
6415
6478
|
"\n"
|
|
6416
6479
|
);
|
|
@@ -6451,7 +6514,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6451
6514
|
|
|
6452
6515
|
const settings = await withPendingStatus("reading your model settings", fetchWorkerSettings);
|
|
6453
6516
|
if (!settings) {
|
|
6454
|
-
console.log(red("
|
|
6517
|
+
console.log(red(" couldn't reach the API to read your current settings.\n"));
|
|
6455
6518
|
return;
|
|
6456
6519
|
}
|
|
6457
6520
|
// EVERY ROW THAT IS ALREADY THERE, including the synthesized one a pre-#123 account keeps in
|
|
@@ -6477,10 +6540,10 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6477
6540
|
// without Claude, which serves no embeddings).
|
|
6478
6541
|
const presets = ps.providerPresets(role);
|
|
6479
6542
|
const rowsShown = [...presets.map((p) => `${p.name}${dim(` · ${p.url}`)}`), "another URL…"];
|
|
6480
|
-
console.log(dim(`
|
|
6543
|
+
console.log(dim(` Which provider? (${PICK_HINT})`));
|
|
6481
6544
|
const pi = await pickFromList(rowsShown, 0);
|
|
6482
6545
|
if (pi < 0) {
|
|
6483
|
-
console.log(dim("
|
|
6546
|
+
console.log(dim(" (nothing saved)\n"));
|
|
6484
6547
|
return;
|
|
6485
6548
|
}
|
|
6486
6549
|
if (pi < presets.length) target = as.serverTarget("openai", presets[pi].url);
|
|
@@ -6488,12 +6551,12 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6488
6551
|
if (!target && !presetUrl) {
|
|
6489
6552
|
const dflt = as.defaultServerUrl(role, dialect, settings, existing);
|
|
6490
6553
|
console.log(dim(dialect === "ollama"
|
|
6491
|
-
? "
|
|
6492
|
-
: "
|
|
6554
|
+
? " An Ollama server: this machine, a box on your network, or https://ollama.com (needs a key)."
|
|
6555
|
+
: " Any OpenAI-compatible endpoint: OpenRouter, Together, vLLM, LM Studio, …"));
|
|
6493
6556
|
// The address goes ABOVE, in full, and Enter's meaning is said out loud (46d1524e).
|
|
6494
6557
|
console.log(dim(dflt ? ` Enter uses ${dflt} · Esc cancels\n` : " Esc cancels.\n"));
|
|
6495
6558
|
for (let attempt = 0; attempt < 3 && !target; attempt++) {
|
|
6496
|
-
const typed = await ask("
|
|
6559
|
+
const typed = await ask(" server URL: ");
|
|
6497
6560
|
if (typed === null || as.isCancelWord(typed)) break;
|
|
6498
6561
|
const raw = typed.trim() || dflt;
|
|
6499
6562
|
if (!raw) break;
|
|
@@ -6510,7 +6573,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6510
6573
|
}
|
|
6511
6574
|
}
|
|
6512
6575
|
if (!target) {
|
|
6513
|
-
console.log(dim("
|
|
6576
|
+
console.log(dim(" (nothing saved)\n"));
|
|
6514
6577
|
return;
|
|
6515
6578
|
}
|
|
6516
6579
|
const host = target.host;
|
|
@@ -6532,7 +6595,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6532
6595
|
// THE ROW CAP BEFORE ANY QUESTION about a new server: exceeding it is a silent 200 with no row,
|
|
6533
6596
|
// and asking for a key and a model first would be the worst order to discover that in.
|
|
6534
6597
|
if (!found && existing.length >= ps.MAX_BACKENDS) {
|
|
6535
|
-
console.log(yellow(`
|
|
6598
|
+
console.log(yellow(` ${ps.MAX_BACKENDS} servers is the limit — remove one in /model → manage providers first.\n`));
|
|
6536
6599
|
return;
|
|
6537
6600
|
}
|
|
6538
6601
|
const need = as.keyRequirement(target);
|
|
@@ -6577,7 +6640,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6577
6640
|
let probe = await list();
|
|
6578
6641
|
// THE STORED KEY WAS REFUSED — ask for the right one once, rather than saving nothing.
|
|
6579
6642
|
if ((probe.status === 401 || probe.status === 403) && !typedKey && key) {
|
|
6580
|
-
console.log(yellow(`
|
|
6643
|
+
console.log(yellow(` ${host} refused the stored key (HTTP ${probe.status}) — enter the right one.`));
|
|
6581
6644
|
if (!(await askKey()) || !key) return;
|
|
6582
6645
|
probe = await list();
|
|
6583
6646
|
}
|
|
@@ -6612,11 +6675,11 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6612
6675
|
? await chooseCodingModelFrom(ps, models, R.question, 0)
|
|
6613
6676
|
: await chooseFromModelList(models, R.question, Math.max(0, models.indexOf(current)));
|
|
6614
6677
|
if (model === null) {
|
|
6615
|
-
console.log(dim("
|
|
6678
|
+
console.log(dim(" (cancelled — nothing was saved)\n"));
|
|
6616
6679
|
return;
|
|
6617
6680
|
}
|
|
6618
6681
|
} else {
|
|
6619
|
-
model = String((await ask("
|
|
6682
|
+
model = String((await ask(" model name: ")) ?? "").trim();
|
|
6620
6683
|
}
|
|
6621
6684
|
if (!model) {
|
|
6622
6685
|
console.log(GUTTER + dim("(cancelled — nothing was saved)\n"));
|
|
@@ -6633,7 +6696,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6633
6696
|
if (!sw.changed) return;
|
|
6634
6697
|
plan = sw.plan;
|
|
6635
6698
|
} else if (as.serverTarget(target.dialect, String(settings.ragEmbedUrl ?? "")).root === target.root && !typedKey) {
|
|
6636
|
-
console.log(dim(`
|
|
6699
|
+
console.log(dim(` already embedding with ${model} on ${host} — nothing changed.\n`));
|
|
6637
6700
|
return;
|
|
6638
6701
|
}
|
|
6639
6702
|
}
|
|
@@ -6662,7 +6725,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6662
6725
|
} else {
|
|
6663
6726
|
const r = as.rowForSave(role, { existing: found.row, model });
|
|
6664
6727
|
if (r.full) {
|
|
6665
|
-
console.log(yellow(`
|
|
6728
|
+
console.log(yellow(` ${host} already offers ${ps.MAX_MODELS_PER_SERVER} models — remove one in /model → manage providers first.\n`));
|
|
6666
6729
|
return;
|
|
6667
6730
|
}
|
|
6668
6731
|
if (r.changed) rows = existing.map((b) => (b === found.row ? r.row : b));
|
|
@@ -6670,7 +6733,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6670
6733
|
if (rows !== existing) {
|
|
6671
6734
|
const ok = await withPendingStatus(`saving ${host}`, () => md.saveWorkerSettings(apiBase, workerKey, { agentCodingBackends: rows }));
|
|
6672
6735
|
if (!ok) {
|
|
6673
|
-
console.log(red(`
|
|
6736
|
+
console.log(red(` couldn't save ${host} — nothing changed.`));
|
|
6674
6737
|
console.log(GUTTER + dim("if this machine's local API is old, restart it with `gu api restart`.\n"));
|
|
6675
6738
|
return;
|
|
6676
6739
|
}
|
|
@@ -6679,7 +6742,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6679
6742
|
if (typedKey) {
|
|
6680
6743
|
const id = await withPendingStatus("saving the key", () => addCodingKey(keyName, key, rowId));
|
|
6681
6744
|
if (!id) {
|
|
6682
|
-
console.log(yellow(`
|
|
6745
|
+
console.log(yellow(` ${host} was saved, but its key was not — nothing else changed.`));
|
|
6683
6746
|
console.log(GUTTER + dim("try again, or add the key with ctrl+k.\n"));
|
|
6684
6747
|
return;
|
|
6685
6748
|
}
|
|
@@ -6697,18 +6760,20 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6697
6760
|
console.log(GUTTER + dim("the banner shows the new model next time gu starts.\n"));
|
|
6698
6761
|
} else if (rows !== existing || typedKey) {
|
|
6699
6762
|
// The SERVER landed — say so, or the user re-enters a key that is already stored.
|
|
6700
|
-
console.log(yellow(`
|
|
6763
|
+
console.log(yellow(` ${host} was saved, but the agent model was not pointed at it.`));
|
|
6701
6764
|
console.log(GUTTER + dim("run /agent-model to finish, or check the API is reachable.\n"));
|
|
6702
6765
|
}
|
|
6703
6766
|
return;
|
|
6704
6767
|
}
|
|
6705
6768
|
if (role === "embed") {
|
|
6706
6769
|
// ragEmbedKeyId "" — the server's own keys decide now (its default, or this folder's pick).
|
|
6707
|
-
|
|
6770
|
+
// …and ragEnabled: true (#347): choosing an embedder IS turning RAG on. Without it the choice
|
|
6771
|
+
// was stored and never took effect, and the "no embed model" warning came back at every start.
|
|
6772
|
+
const fields = ec.embedSaveFields({ url: target.roleUrl("embed"), model });
|
|
6708
6773
|
if (!(await saveEmbedSettings(fields))) return;
|
|
6709
6774
|
if (plan) announceEmbedSaved({ chosen: model, plan, url: target.roleUrl("embed") });
|
|
6710
6775
|
else {
|
|
6711
|
-
console.log(accent(`
|
|
6776
|
+
console.log(accent(` ✓ embedding server → ${host}`));
|
|
6712
6777
|
console.log(dim(` ${model} did not change, so no knowledge base rebuilds — the same model just answers from ${host}.\n`));
|
|
6713
6778
|
}
|
|
6714
6779
|
return;
|
|
@@ -6736,7 +6801,7 @@ async function useCoderInThisFolder({ ps, rows, beforeChoices, url, host, locali
|
|
|
6736
6801
|
(c) => c.model === model && (c.backendId ? c.backendId === backendId : sameHost(c.url, url))
|
|
6737
6802
|
);
|
|
6738
6803
|
if (!offered) {
|
|
6739
|
-
console.log(yellow(`
|
|
6804
|
+
console.log(yellow(` ${host} was saved, but this API is not offering ${model} yet.`));
|
|
6740
6805
|
console.log(GUTTER + dim("run /model — if it is missing there too, the API may be older than"));
|
|
6741
6806
|
console.log(GUTTER + dim("this feature; restart it with `gu api restart`.\n"));
|
|
6742
6807
|
return;
|
|
@@ -6750,7 +6815,7 @@ async function useCoderInThisFolder({ ps, rows, beforeChoices, url, host, locali
|
|
|
6750
6815
|
])
|
|
6751
6816
|
);
|
|
6752
6817
|
if (lost.length) {
|
|
6753
|
-
console.log(yellow(`
|
|
6818
|
+
console.log(yellow(` ⚠ this API dropped ${lost.length === 1 ? "a provider" : `${lost.length} providers`} when saving:`));
|
|
6754
6819
|
for (const u of lost) console.log(GUTTER + dim(` ${u}`));
|
|
6755
6820
|
if (ps.apiKeysBackendsByKind(after?.codingChoices)) {
|
|
6756
6821
|
console.log(GUTTER + dim("this API identifies a backend by its KIND, so it holds ONE"));
|
|
@@ -6774,8 +6839,8 @@ async function useCoderInThisFolder({ ps, rows, beforeChoices, url, host, locali
|
|
|
6774
6839
|
// The key question for this coder is answered — leaving the flag set would warn about a key
|
|
6775
6840
|
// that was just entered, which reads as the save having failed.
|
|
6776
6841
|
codingKeyMissingKnown = false;
|
|
6777
|
-
console.log(accent(`
|
|
6778
|
-
if (key) console.log(dim(`
|
|
6842
|
+
console.log(accent(` ✓ coding model → ${model}`) + dim(` · ${host}`));
|
|
6843
|
+
if (key) console.log(dim(` API key ${keyName || "saved"} ····${key.slice(-4)} · ${host}'s key\n`));
|
|
6779
6844
|
// A HOSTED SERVER NEEDS ITS WINDOW SET: with nothing set gu assumes 24,000 prompt tokens and
|
|
6780
6845
|
// compacts there — on a large hosted model that throws history away every turn.
|
|
6781
6846
|
if (ps.shouldMentionContextWindow(locality)) {
|
|
@@ -6795,7 +6860,7 @@ async function chooseModel() {
|
|
|
6795
6860
|
fetchAgentPayload([{ role: "user", content: "ping" }])
|
|
6796
6861
|
);
|
|
6797
6862
|
} catch (err) {
|
|
6798
|
-
console.log(red(`
|
|
6863
|
+
console.log(red(` ${describeApiFailure(err, "couldn't load models")}\n`));
|
|
6799
6864
|
return;
|
|
6800
6865
|
}
|
|
6801
6866
|
const allowed = Array.isArray(probe?.codingAllowed) ? probe.codingAllowed : [];
|
|
@@ -6816,7 +6881,7 @@ async function chooseModel() {
|
|
|
6816
6881
|
// choices while still occupying its URL, so sending this branch straight to the ADD flow
|
|
6817
6882
|
// would meet "already configured here" and dead-end. The manager fixes such a row, and falls
|
|
6818
6883
|
// through to add when there is genuinely nothing there.
|
|
6819
|
-
console.log(dim("
|
|
6884
|
+
console.log(dim(" No coding model configured."));
|
|
6820
6885
|
const go = await pickFromList(
|
|
6821
6886
|
[dim("leave it"), "add or change a provider — Claude API, OpenAI, Kimi…", "change the Ollama server this machine uses…"],
|
|
6822
6887
|
1
|
|
@@ -6834,7 +6899,7 @@ async function chooseModel() {
|
|
|
6834
6899
|
// symptom of being pointed at the wrong box, and the old message sent people to a web app a
|
|
6835
6900
|
// Client Expert machine does not have.
|
|
6836
6901
|
if (choices.length === 1) {
|
|
6837
|
-
console.log(dim(`
|
|
6902
|
+
console.log(dim(` Only one coding model is available: ${choices[0].model}.`));
|
|
6838
6903
|
// APPENDED AFTER the existing entry, here and below, so that entry keeps the index anyone
|
|
6839
6904
|
// who has used it already reaches for.
|
|
6840
6905
|
const go = await pickFromList(
|
|
@@ -6882,10 +6947,10 @@ async function chooseModel() {
|
|
|
6882
6947
|
// enough in name to have already been mixed up. The fix is to say which one THIS is — not to
|
|
6883
6948
|
// print the other two here: the agent model and the embedder are independent settings with
|
|
6884
6949
|
// their own commands, and listing them together implies a relationship that does not exist.
|
|
6885
|
-
console.log(dim(`
|
|
6950
|
+
console.log(dim(` Choose the CODING model — the one that writes code (${PICK_HINT}):`));
|
|
6886
6951
|
const idx = await pickFromList(labels, activeIdx >= 0 ? activeIdx : 0);
|
|
6887
6952
|
if (idx < 0) {
|
|
6888
|
-
console.log(dim("
|
|
6953
|
+
console.log(dim(" (cancelled)\n"));
|
|
6889
6954
|
return;
|
|
6890
6955
|
}
|
|
6891
6956
|
// The appended entries sit past the last model, so they are the only indices without a choice.
|
|
@@ -6915,7 +6980,7 @@ async function chooseModel() {
|
|
|
6915
6980
|
const where = showTag && !activeCoderWhere ? ` (${tags[idx]})` : "";
|
|
6916
6981
|
console.log(
|
|
6917
6982
|
accent(
|
|
6918
|
-
`
|
|
6983
|
+
` ✓ coding model → ${coderDisplayName(chosen.model, activeCoderWhere)}${where}${chosen.id === def ? " (default)" : ""}\n`
|
|
6919
6984
|
)
|
|
6920
6985
|
);
|
|
6921
6986
|
// DOES THIS ONE NEED AN API KEY? (task #181) Reported live on Windows 11 / Client Expert:
|
|
@@ -6968,7 +7033,7 @@ async function chooseModel() {
|
|
|
6968
7033
|
host: ks.provider,
|
|
6969
7034
|
});
|
|
6970
7035
|
console.log(
|
|
6971
|
-
dim("
|
|
7036
|
+
dim(" API key ") + accent(line.text) +
|
|
6972
7037
|
(line.note ? orange(` · ${line.note}`) : "") +
|
|
6973
7038
|
dim(` · ctrl+k to ${line.verb} it`)
|
|
6974
7039
|
);
|
|
@@ -6977,7 +7042,7 @@ async function chooseModel() {
|
|
|
6977
7042
|
// the more common one — "how do I change the model" — with no visible answer at all. Reported
|
|
6978
7043
|
// three times from exactly this view, twice after being told where the picker entry was.
|
|
6979
7044
|
// /provider is hidden (see HIDDEN_COMMANDS): point at the row of /model that does the same.
|
|
6980
|
-
console.log(dim("
|
|
7045
|
+
console.log(dim(" to change this provider's model or URL, or remove it: /model → add or change a provider\n"));
|
|
6981
7046
|
}
|
|
6982
7047
|
// Task #125: the GLOBAL lifetime total is per-backend, so re-fetch it for the newly
|
|
6983
7048
|
// selected backend NOW — otherwise the footer would keep showing the previous backend's
|
|
@@ -7331,13 +7396,13 @@ async function runSwitchSkillCommand(line) {
|
|
|
7331
7396
|
}
|
|
7332
7397
|
} else {
|
|
7333
7398
|
const options = ["claude", "gu"];
|
|
7334
|
-
console.log(dim(`
|
|
7399
|
+
console.log(dim(` Whose skills, MCP servers and hooks this folder uses (${PICK_HINT}):`));
|
|
7335
7400
|
const idx = await pickFromList(
|
|
7336
7401
|
options.map((o) => sourceLabel(o) + dim(` · ${where(o)}`) + (o === current ? dim(" · in use") : "")),
|
|
7337
7402
|
Math.max(0, options.indexOf(current))
|
|
7338
7403
|
);
|
|
7339
7404
|
if (idx < 0) {
|
|
7340
|
-
console.log(dim("
|
|
7405
|
+
console.log(dim(" (cancelled)\n"));
|
|
7341
7406
|
paintBar();
|
|
7342
7407
|
return;
|
|
7343
7408
|
}
|
|
@@ -7355,7 +7420,7 @@ async function runSwitchSkillCommand(line) {
|
|
|
7355
7420
|
_skillScan = { at: 0, skills: [] }; // the "/" menu and Tab follow at once
|
|
7356
7421
|
const say = (l) => console.log(GUTTER + l);
|
|
7357
7422
|
say(accent(`✓ ${pick === current ? "this folder uses" : "this folder now uses"} ${sourceLabel(pick)}`));
|
|
7358
|
-
say(dim(`
|
|
7423
|
+
say(dim(` skills, MCP servers and hooks are read from ${where(pick)}`));
|
|
7359
7424
|
// What was found there — and what was not, said rather than shown as an empty list.
|
|
7360
7425
|
try {
|
|
7361
7426
|
const sk = discoverSkills({ cwd: process.cwd(), reserved: COMMAND_NAMES });
|
|
@@ -7367,20 +7432,20 @@ async function runSwitchSkillCommand(line) {
|
|
|
7367
7432
|
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")}`));
|
|
7368
7433
|
// Nothing there at all is a normal state, not a fault: say so, and that gu needs none of it.
|
|
7369
7434
|
if (!sk.skills.length && !mcp.servers.length && !nHooks(hk.user) && !nHooks(hk.project) && !hk.notes.length) {
|
|
7370
|
-
say(dim("
|
|
7435
|
+
say(dim(" Nothing is set up there yet — gu works the same without any of them."));
|
|
7371
7436
|
}
|
|
7372
7437
|
for (const w of [...sk.warnings, ...mcp.warnings, ...hk.warnings]) say(yellow(" " + w));
|
|
7373
7438
|
const unsupported = mcp.servers.filter((x) => x.error);
|
|
7374
7439
|
if (unsupported.length) say(yellow(` ${count(unsupported.length, "MCP server")} cannot run here: ${unsupported.map((x) => `${x.name} (${x.error})`).join("; ")}`));
|
|
7375
7440
|
const waiting = mcp.servers.filter((x) => x.scope === "project" && !x.approved && !x.error);
|
|
7376
7441
|
if (waiting.length) say(yellow(` ${count(waiting.length, "project MCP server")} waiting for your approval: ${waiting.map((x) => x.name).join(", ")} — /mcp approve <name>`));
|
|
7377
|
-
if (hk.projectPending) say(yellow(`
|
|
7442
|
+
if (hk.projectPending) say(yellow(` this folder's ${hk.projectFile.replace(/^\.\//, "")} has hooks that will not run until you approve them — /hooks approve`));
|
|
7378
7443
|
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`));
|
|
7379
|
-
if (pick === "claude" && (nHooks(hk.user) || nHooks(hk.project))) say(dim("
|
|
7444
|
+
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."));
|
|
7380
7445
|
} catch (err) {
|
|
7381
|
-
say(yellow(`
|
|
7446
|
+
say(yellow(` (could not read it back: ${err.message})`));
|
|
7382
7447
|
}
|
|
7383
|
-
say(dim(`
|
|
7448
|
+
say(dim(` It holds from your next question. ${pick === "claude" ? "/switch-skill gu" : "/switch-skill claude"} switches back.`));
|
|
7384
7449
|
console.log("");
|
|
7385
7450
|
if (barPinned()) paintBar();
|
|
7386
7451
|
}
|
|
@@ -7529,7 +7594,7 @@ async function runUsePresetsCommand(line) {
|
|
|
7529
7594
|
console.log(GUTTER + dim("/save-preset pastes one in — copy it from another computer's /open-presets.") + "\n");
|
|
7530
7595
|
return;
|
|
7531
7596
|
}
|
|
7532
|
-
console.log(dim(`
|
|
7597
|
+
console.log(dim(` Presets (${ed.PICKER_HINT}):`));
|
|
7533
7598
|
const pick = await pickFromList(found, 0, { keys: ed.PICKER_KEYS });
|
|
7534
7599
|
if (!pick || pick.index < 0) {
|
|
7535
7600
|
console.log(GUTTER + dim("nothing changed.") + "\n");
|
|
@@ -7608,7 +7673,7 @@ async function runSaveSettingsCommand(line) {
|
|
|
7608
7673
|
// have to type is a name — and Enter keeps gu-settings.json, as before.
|
|
7609
7674
|
let typed = settingsArg(line, "/save-settings");
|
|
7610
7675
|
if (!typed) {
|
|
7611
|
-
const answer = await ask(`
|
|
7676
|
+
const answer = await ask(` file name in ${home} [${st.DEFAULT_SETTINGS_NAME}]: `);
|
|
7612
7677
|
if (answer === null) {
|
|
7613
7678
|
console.log(GUTTER + dim(" (cancelled — nothing was written)\n"));
|
|
7614
7679
|
return;
|
|
@@ -7668,7 +7733,7 @@ async function runLoadSettingsCommand(line) {
|
|
|
7668
7733
|
if (!typed) {
|
|
7669
7734
|
const found = st.settingsFilesIn(listJsonFiles(home));
|
|
7670
7735
|
if (found.length > 1) {
|
|
7671
|
-
console.log(dim(`
|
|
7736
|
+
console.log(dim(` Settings files in ${home} (${PICK_HINT}):`));
|
|
7672
7737
|
const i = await pickFromList(found, 0);
|
|
7673
7738
|
if (i < 0) {
|
|
7674
7739
|
console.log(GUTTER + dim(" (cancelled — nothing changed)\n"));
|
|
@@ -7849,6 +7914,7 @@ async function noteTurnCost(inputTokens, breakdown, coder = null) {
|
|
|
7849
7914
|
const c = mp.turnCost({ input: inputTokens, ...cache, price });
|
|
7850
7915
|
if (!c) {
|
|
7851
7916
|
lastTurnCost = { usd: null, model, host: mp.hostOf(url) };
|
|
7917
|
+
if (barPinned()) paintBar(); // #349: "no price" is shown, so it is painted like a cost is
|
|
7852
7918
|
return;
|
|
7853
7919
|
}
|
|
7854
7920
|
lastTurnCost = { ...c, input: inputTokens, cacheReported: cache.cacheReported, model, host: mp.hostOf(url) };
|
|
@@ -7891,7 +7957,7 @@ async function runPricesCommand(line = "/prices") {
|
|
|
7891
7957
|
console.log(GUTTER + dim(pricePublishedAt ? `from gu's tables published ${new Date(pricePublishedAt).toLocaleString()}` : "gu's built-in tables (none published applied yet)"));
|
|
7892
7958
|
if (priceUpdate) console.log(GUTTER + accent(`${mp.priceUpdateLabel(priceUpdate.publishedAt)} — click it below, or /prices update`));
|
|
7893
7959
|
if (!mp.findPrice(rows, url, model)) {
|
|
7894
|
-
|
|
7960
|
+
for (const r of wrapBlock(mp.noPriceLine({ model, host: mp.hostOf(url) }), GUTTER, width)) console.log(GUTTER + yellow(r));
|
|
7895
7961
|
}
|
|
7896
7962
|
if (lastTurnCost?.usd != null) {
|
|
7897
7963
|
const share = lastTurnCost.input ? Math.round((100 * lastTurnCost.cachedTokens) / lastTurnCost.input) : 0;
|
|
@@ -8017,11 +8083,11 @@ async function runClearLogs() {
|
|
|
8017
8083
|
const question = (q) => {
|
|
8018
8084
|
const hint = `(${PICK_HINT}):`;
|
|
8019
8085
|
console.log("");
|
|
8020
|
-
if (2 + q.length + 2 + hint.length < width) console.log(dim(`
|
|
8021
|
-
else console.log(dim(`
|
|
8086
|
+
if (2 + q.length + 2 + hint.length < width) console.log(dim(` ${q} ${hint}`));
|
|
8087
|
+
else console.log(dim(` ${q}\n ${hint}`));
|
|
8022
8088
|
};
|
|
8023
8089
|
const cancelled = () => {
|
|
8024
|
-
console.log(dim(`
|
|
8090
|
+
console.log(dim(` (stopped — nothing ${freed.length ? "further " : ""}removed)\n`));
|
|
8025
8091
|
paintBar();
|
|
8026
8092
|
};
|
|
8027
8093
|
|
|
@@ -8122,7 +8188,7 @@ async function saveEmbedSettings(fields) {
|
|
|
8122
8188
|
const saved = await withPendingStatus("saving your embedder", () =>
|
|
8123
8189
|
md.saveWorkerSettings(apiBase, workerKey, fields)
|
|
8124
8190
|
);
|
|
8125
|
-
if (!saved) console.log(red("
|
|
8191
|
+
if (!saved) console.log(red(" couldn't save that to your account — nothing changed.\n"));
|
|
8126
8192
|
return saved;
|
|
8127
8193
|
}
|
|
8128
8194
|
|
|
@@ -8140,7 +8206,7 @@ async function confirmEmbedSwitch(ec, current, chosen) {
|
|
|
8140
8206
|
}
|
|
8141
8207
|
const plan = ec.embedChangePlan({ current, chosen, indexes });
|
|
8142
8208
|
if (!plan.changed) {
|
|
8143
|
-
console.log(dim(`
|
|
8209
|
+
console.log(dim(` already using ${current} — nothing changed.\n`));
|
|
8144
8210
|
return { changed: false, plan: null };
|
|
8145
8211
|
}
|
|
8146
8212
|
// "Are you sure?" with nothing behind it is a prompt people learn to dismiss; the four projects
|
|
@@ -8148,7 +8214,7 @@ async function confirmEmbedSwitch(ec, current, chosen) {
|
|
|
8148
8214
|
for (const l of ec.describeEmbedChange(plan)) console.log(GUTTER + dim(l));
|
|
8149
8215
|
const go = await pickFromList([`switch to ${chosen}`, `keep ${current}`], plan.stale.length ? 1 : 0);
|
|
8150
8216
|
if (go !== 0) {
|
|
8151
|
-
console.log(dim("
|
|
8217
|
+
console.log(dim(" (kept)\n"));
|
|
8152
8218
|
return { changed: false, plan: null };
|
|
8153
8219
|
}
|
|
8154
8220
|
return { changed: true, plan };
|
|
@@ -8158,13 +8224,13 @@ async function confirmEmbedSwitch(ec, current, chosen) {
|
|
|
8158
8224
|
// shared recognition step. Returns { changed, chosen, plan }.
|
|
8159
8225
|
async function pickEmbedModel(ec, current) {
|
|
8160
8226
|
const rows = ec.embedChoiceRows(current);
|
|
8161
|
-
console.log(dim(`
|
|
8227
|
+
console.log(dim(` Choose the embedder (${PICK_HINT}):`));
|
|
8162
8228
|
const labels = rows.map(
|
|
8163
8229
|
(r) => `${r.model}${r.dims ? dim(` · ${r.dims}d`) : ""}${dim(` · ${r.note}`)}` + (r.current ? accent(" (current)") : "")
|
|
8164
8230
|
);
|
|
8165
8231
|
const idx = await pickFromList(labels, Math.max(0, rows.findIndex((r) => r.current)));
|
|
8166
8232
|
if (idx < 0) {
|
|
8167
|
-
console.log(dim("
|
|
8233
|
+
console.log(dim(" (cancelled)\n"));
|
|
8168
8234
|
return { changed: false, chosen: "", plan: null };
|
|
8169
8235
|
}
|
|
8170
8236
|
const chosen = rows[idx].model;
|
|
@@ -8178,21 +8244,21 @@ async function pickEmbedModel(ec, current) {
|
|
|
8178
8244
|
// a model that was never pulled fails at the first search, not here. Say it once rather than let
|
|
8179
8245
|
// it surface as "RAG is broken".
|
|
8180
8246
|
function announceEmbedSaved({ chosen, plan, url }) {
|
|
8181
|
-
console.log(accent(`
|
|
8247
|
+
console.log(accent(` ✓ embedding model → ${chosen}` + (url ? ` on ${hostOf(url)}` : "")));
|
|
8182
8248
|
if (plan?.stale.length) {
|
|
8183
8249
|
console.log(
|
|
8184
8250
|
dim(` ${plan.stale.length} knowledge base${plan.stale.length === 1 ? "" : "s"} ` +
|
|
8185
8251
|
"will rebuild on the next search — the first one will be slower.")
|
|
8186
8252
|
);
|
|
8187
8253
|
}
|
|
8188
|
-
console.log(dim(`
|
|
8254
|
+
console.log(dim(` Make sure it is pulled there: ollama pull ${chosen}\n`));
|
|
8189
8255
|
}
|
|
8190
8256
|
|
|
8191
8257
|
async function chooseEmbedModel() {
|
|
8192
8258
|
const ec = await import("./embed-model-command.mjs");
|
|
8193
8259
|
const settings = await withPendingStatus("reading your embedder", fetchWorkerSettings);
|
|
8194
8260
|
if (!settings) {
|
|
8195
|
-
console.log(red("
|
|
8261
|
+
console.log(red(" couldn't reach the API to read your current embedder.\n"));
|
|
8196
8262
|
return;
|
|
8197
8263
|
}
|
|
8198
8264
|
const current = ec.currentEmbedModel(settings);
|
|
@@ -8205,11 +8271,11 @@ async function chooseEmbedModel() {
|
|
|
8205
8271
|
const keyRequired = isOllamaCloudUrl(currentUrl) || /\/v1\/?$/i.test(currentUrl) ||
|
|
8206
8272
|
Boolean(String(settings.ragEmbedKeyId ?? "").trim());
|
|
8207
8273
|
const menu = ec.embedMenuOptions({ current, host, keyRequired });
|
|
8208
|
-
console.log(dim(`
|
|
8274
|
+
console.log(dim(` Embedding model (currently ${current} on ${host || currentUrl})`));
|
|
8209
8275
|
const mi = await pickFromList(menu.map((o) => o.label), 0);
|
|
8210
8276
|
const action = mi >= 0 ? menu[mi].action : "cancel";
|
|
8211
8277
|
if (action === "cancel") {
|
|
8212
|
-
console.log(dim("
|
|
8278
|
+
console.log(dim(" (cancelled)\n"));
|
|
8213
8279
|
return;
|
|
8214
8280
|
}
|
|
8215
8281
|
|
|
@@ -8250,12 +8316,12 @@ async function saveAgentModel(agentUrl, model, host) {
|
|
|
8250
8316
|
md.saveWorkerSettings(apiBase, workerKey, { agentModelUrl: agentUrl, agentModelName: model, agentModel: model })
|
|
8251
8317
|
);
|
|
8252
8318
|
if (!ok) {
|
|
8253
|
-
console.log(red(`
|
|
8319
|
+
console.log(red(` couldn't save the agent model — nothing changed.`));
|
|
8254
8320
|
console.log(GUTTER + dim("if this machine's local API is old, restart it with `gu api restart`.\n"));
|
|
8255
8321
|
return false;
|
|
8256
8322
|
}
|
|
8257
|
-
console.log(accent(`
|
|
8258
|
-
console.log(dim("
|
|
8323
|
+
console.log(accent(` ✓ agent model → ${model}`) + dim(` · ${host}`));
|
|
8324
|
+
console.log(dim(" takes effect on your next question.\n"));
|
|
8259
8325
|
return true;
|
|
8260
8326
|
}
|
|
8261
8327
|
|
|
@@ -8268,7 +8334,7 @@ async function askKeyName(prompt = " key name: ") {
|
|
|
8268
8334
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
8269
8335
|
const name = (await ask(prompt)).trim();
|
|
8270
8336
|
if (!am.looksLikeSecret(name)) return name;
|
|
8271
|
-
console.log(yellow("
|
|
8337
|
+
console.log(yellow(" that looks like the API key itself — the NAME is shown on screen, so it must not be the key."));
|
|
8272
8338
|
console.log(GUTTER + dim('type a short label like "work", or press Enter for none — the key comes next.'));
|
|
8273
8339
|
}
|
|
8274
8340
|
return "";
|
|
@@ -8289,7 +8355,7 @@ async function askServerKey({ host = "", nameHint = '"work", "personal"', option
|
|
|
8289
8355
|
console.log("\n" + GUTTER + dim(`A name just tells keys apart — ${nameHint}.`));
|
|
8290
8356
|
const name = (await askKeyName()).trim();
|
|
8291
8357
|
console.log(GUTTER + dim(SECRET_KEY_HINT));
|
|
8292
|
-
const secret = (await askSecret(`
|
|
8358
|
+
const secret = (await askSecret(` API key${host ? ` for ${host}` : ""}${name ? ` (${name})` : ""}: `)).trim();
|
|
8293
8359
|
if (!secret) {
|
|
8294
8360
|
console.log(GUTTER + dim(optional ? "(no key — the server will be called anonymously)" : "(cancelled — nothing was saved)\n"));
|
|
8295
8361
|
return { name, secret: "" };
|
|
@@ -8324,15 +8390,15 @@ async function chooseAgentProvider() {
|
|
|
8324
8390
|
const am = await import("./agent-model-command.mjs");
|
|
8325
8391
|
const settings = await withPendingStatus("reading your providers", fetchWorkerSettings);
|
|
8326
8392
|
if (!settings) {
|
|
8327
|
-
console.log(red("
|
|
8393
|
+
console.log(red(" couldn't reach the API to read your providers.\n"));
|
|
8328
8394
|
return;
|
|
8329
8395
|
}
|
|
8330
8396
|
const choices = am.agentProviderChoices(settings);
|
|
8331
8397
|
if (choices.length === 1) return addServer({ role: "agent", dialect: "openai" });
|
|
8332
|
-
console.log(dim(`
|
|
8398
|
+
console.log(dim(` Which OpenAI-compatible provider should the agent use? (${PICK_HINT})`));
|
|
8333
8399
|
const idx = await pickFromList(choices.map((c) => c.label), 0);
|
|
8334
8400
|
if (idx < 0) {
|
|
8335
|
-
console.log(dim("
|
|
8401
|
+
console.log(dim(" (cancelled)\n"));
|
|
8336
8402
|
return;
|
|
8337
8403
|
}
|
|
8338
8404
|
const pick = choices[idx];
|
|
@@ -8354,7 +8420,7 @@ async function chooseAgentModel() {
|
|
|
8354
8420
|
const am = await import("./agent-model-command.mjs");
|
|
8355
8421
|
const settings = await withPendingStatus("reading your agent model", fetchWorkerSettings);
|
|
8356
8422
|
if (!settings) {
|
|
8357
|
-
console.log(red("
|
|
8423
|
+
console.log(red(" couldn't reach the API to read your current agent model.\n"));
|
|
8358
8424
|
return;
|
|
8359
8425
|
}
|
|
8360
8426
|
const current = am.currentAgentModel(settings);
|
|
@@ -8440,12 +8506,12 @@ async function chooseAgentModel() {
|
|
|
8440
8506
|
// WHAT TO CHANGE, FIRST (#44). The ways to use an Ollama server or another provider used to sit
|
|
8441
8507
|
// after every model on this one — 130 rows down on api.openai.com — which read as "OpenAI models
|
|
8442
8508
|
// are all you can choose". A short menu of what to change comes before any list.
|
|
8443
|
-
console.log(dim(`
|
|
8509
|
+
console.log(dim(` Agent model — the one that TALKS: ${current || "unset"} on ${listHost}`));
|
|
8444
8510
|
const menu = am.agentMenuOptions({ host: listHost, modelCount: models.length, needsKey });
|
|
8445
8511
|
const first = await pickFromList(menu.map((o) => o.label), 0);
|
|
8446
8512
|
const firstAction = first >= 0 ? menu[first].action : "cancel";
|
|
8447
8513
|
if (firstAction === "cancel") {
|
|
8448
|
-
console.log(dim("
|
|
8514
|
+
console.log(dim(" (cancelled)\n"));
|
|
8449
8515
|
return;
|
|
8450
8516
|
}
|
|
8451
8517
|
if (firstAction === "ollama") return addServer({ role: "agent", dialect: "ollama" });
|
|
@@ -8459,7 +8525,7 @@ async function chooseAgentModel() {
|
|
|
8459
8525
|
}
|
|
8460
8526
|
|
|
8461
8527
|
const rows = am.agentModelRows(models, current);
|
|
8462
|
-
console.log(dim(`
|
|
8528
|
+
console.log(dim(` Agent model — the one that TALKS (currently ${current || "unset"})`));
|
|
8463
8529
|
// Strip whichever list endpoint was asked, so the header names the SERVER rather than the path.
|
|
8464
8530
|
console.log(
|
|
8465
8531
|
dim(` ${models.length} on ${url.replace(/\/api\/tags$/, "").replace(/\/models$/, "")} (${PICK_HINT}):`)
|
|
@@ -8488,12 +8554,12 @@ async function chooseAgentModel() {
|
|
|
8488
8554
|
// where they can be seen, rather than after every model.
|
|
8489
8555
|
const idx = await pickFromList(labels, Math.max(0, rows.findIndex((r) => r.current)));
|
|
8490
8556
|
if (idx < 0) {
|
|
8491
|
-
console.log(dim("
|
|
8557
|
+
console.log(dim(" (cancelled)\n"));
|
|
8492
8558
|
return;
|
|
8493
8559
|
}
|
|
8494
8560
|
const chosen = rows[idx].model;
|
|
8495
8561
|
if (chosen === current) {
|
|
8496
|
-
console.log(dim(`
|
|
8562
|
+
console.log(dim(` already using ${current} — nothing changed.\n`));
|
|
8497
8563
|
return;
|
|
8498
8564
|
}
|
|
8499
8565
|
if (rows[idx].embedder) {
|
|
@@ -8504,7 +8570,7 @@ async function chooseAgentModel() {
|
|
|
8504
8570
|
console.log(GUTTER + yellow("produce nonsense rather than an error."));
|
|
8505
8571
|
const go = await pickFromList([`use it anyway`, `pick something else`], 1);
|
|
8506
8572
|
if (go !== 0) {
|
|
8507
|
-
console.log(dim("
|
|
8573
|
+
console.log(dim(" (kept)\n"));
|
|
8508
8574
|
return;
|
|
8509
8575
|
}
|
|
8510
8576
|
}
|
|
@@ -8517,15 +8583,15 @@ async function chooseAgentModel() {
|
|
|
8517
8583
|
agentModel: chosen,
|
|
8518
8584
|
});
|
|
8519
8585
|
if (!saved) {
|
|
8520
|
-
console.log(red(`
|
|
8586
|
+
console.log(red(` couldn't save that to your account — still using ${current || "unset"}.`));
|
|
8521
8587
|
// The likeliest cause by far, and it is invisible otherwise: this field only became writable
|
|
8522
8588
|
// through the worker route in task #169, so an older local API silently ignores it.
|
|
8523
8589
|
console.log(GUTTER + dim("if this machine's local API predates /agent-model, restart it with"));
|
|
8524
8590
|
console.log(GUTTER + dim("`gu api restart` to pick up the current version.\n"));
|
|
8525
8591
|
return;
|
|
8526
8592
|
}
|
|
8527
|
-
console.log(accent(`
|
|
8528
|
-
console.log(dim("
|
|
8593
|
+
console.log(accent(` ✓ agent model → ${chosen}`));
|
|
8594
|
+
console.log(dim(" takes effect on your next question.\n"));
|
|
8529
8595
|
}
|
|
8530
8596
|
|
|
8531
8597
|
// Probe whether SSH KEY auth already works for a server (so we can tell the user to run
|
|
@@ -8550,7 +8616,7 @@ async function checkServerKeyAuth(s) {
|
|
|
8550
8616
|
if (ok) console.log(dim(" ✓ key auth works — deploys will connect without a password."));
|
|
8551
8617
|
else
|
|
8552
8618
|
console.log(
|
|
8553
|
-
yellow("
|
|
8619
|
+
yellow(" key auth not set up yet.") +
|
|
8554
8620
|
dim(` Run once in your terminal: `) +
|
|
8555
8621
|
`ssh-copy-id ${s.user}@${s.host}`
|
|
8556
8622
|
);
|
|
@@ -8750,20 +8816,20 @@ async function runS3Command(line) {
|
|
|
8750
8816
|
// and, for the secret, does not have to know it.
|
|
8751
8817
|
const keep = (typed, was) => (String(typed ?? "").trim() ? String(typed).trim() : was);
|
|
8752
8818
|
const location = keep(
|
|
8753
|
-
await ask(`
|
|
8819
|
+
await ask(` Bucket (ARN, s3://… or name)${current.location ? ` [${current.location}]` : ""}: `),
|
|
8754
8820
|
current.location
|
|
8755
8821
|
);
|
|
8756
8822
|
const region = keep(
|
|
8757
|
-
await ask(`
|
|
8823
|
+
await ask(` Region [${current.region || "ap-southeast-1"}]: `),
|
|
8758
8824
|
current.region || "ap-southeast-1"
|
|
8759
8825
|
);
|
|
8760
8826
|
const accessKeyId = keep(
|
|
8761
|
-
await ask(`
|
|
8827
|
+
await ask(` AWS access key id${current.accessKeyId ? ` [${s3.maskKeyId(current.accessKeyId)}]` : ""}: `),
|
|
8762
8828
|
current.accessKeyId
|
|
8763
8829
|
);
|
|
8764
8830
|
// askSecret, never ask: the echo is muted and the value never reaches the scrollback (#127).
|
|
8765
8831
|
const typedSecret = String(
|
|
8766
|
-
await askSecret(`
|
|
8832
|
+
await askSecret(` AWS secret${current.secretConfigured ? " [Enter keeps the stored one]" : ""}:`)
|
|
8767
8833
|
).trim();
|
|
8768
8834
|
|
|
8769
8835
|
if (!location || !accessKeyId || !(typedSecret || current.secretConfigured)) {
|
|
@@ -8960,16 +9026,16 @@ async function chooseServer() {
|
|
|
8960
9026
|
);
|
|
8961
9027
|
const data = await res.json().catch(() => ({}));
|
|
8962
9028
|
if (!res.ok) {
|
|
8963
|
-
console.log(red(`
|
|
9029
|
+
console.log(red(` couldn't load servers: ${data?.error || `HTTP ${res.status}`}\n`));
|
|
8964
9030
|
return;
|
|
8965
9031
|
}
|
|
8966
9032
|
servers = Array.isArray(data?.servers) ? data.servers : [];
|
|
8967
9033
|
} catch (err) {
|
|
8968
|
-
console.log(red(`
|
|
9034
|
+
console.log(red(` ${describeApiFailure(err, "couldn't load servers")}\n`));
|
|
8969
9035
|
return;
|
|
8970
9036
|
}
|
|
8971
9037
|
if (servers.length === 0) {
|
|
8972
|
-
console.log(dim("
|
|
9038
|
+
console.log(dim(" No deployment servers yet — add them in the web app → Servers.\n"));
|
|
8973
9039
|
return;
|
|
8974
9040
|
}
|
|
8975
9041
|
// Arrow-key picker: ↑/↓ to move, Enter to choose (no number typing). The list ends with
|
|
@@ -8977,21 +9043,21 @@ async function chooseServer() {
|
|
|
8977
9043
|
const labels = servers.map((s) => `${s.name} (${s.user}@${s.host})`);
|
|
8978
9044
|
labels.push("none (clear selection)");
|
|
8979
9045
|
const activeIdx = servers.findIndex((s) => s.host === selectedServer?.host);
|
|
8980
|
-
console.log(dim(`
|
|
9046
|
+
console.log(dim(` Choose a deployment server (${PICK_HINT}):`));
|
|
8981
9047
|
const idx = await pickFromList(labels, activeIdx >= 0 ? activeIdx : 0);
|
|
8982
9048
|
if (idx < 0) {
|
|
8983
|
-
console.log(dim("
|
|
9049
|
+
console.log(dim(" (cancelled)\n"));
|
|
8984
9050
|
return;
|
|
8985
9051
|
}
|
|
8986
9052
|
if (idx === servers.length) {
|
|
8987
9053
|
// the trailing "none" row
|
|
8988
9054
|
selectedServer = null;
|
|
8989
|
-
console.log(dim("
|
|
9055
|
+
console.log(dim(" ✓ deployment server cleared.\n"));
|
|
8990
9056
|
return;
|
|
8991
9057
|
}
|
|
8992
9058
|
const s = servers[idx];
|
|
8993
9059
|
selectedServer = { name: s.name, host: s.host, user: s.user };
|
|
8994
|
-
console.log(accent(`
|
|
9060
|
+
console.log(accent(` ✓ deploy target → ${s.name} (${s.user}@${s.host})`));
|
|
8995
9061
|
await checkServerKeyAuth(selectedServer);
|
|
8996
9062
|
console.log("");
|
|
8997
9063
|
}
|
|
@@ -9083,10 +9149,10 @@ async function showOtherAgents() {
|
|
|
9083
9149
|
return;
|
|
9084
9150
|
}
|
|
9085
9151
|
console.log("");
|
|
9086
|
-
console.log(dim(`
|
|
9152
|
+
console.log(dim(` Your other running agents — pick one to act on (${PICK_HINT}):`));
|
|
9087
9153
|
const which = await pickFromList(items.map((i) => i.label), 0);
|
|
9088
9154
|
if (which < 0) {
|
|
9089
|
-
console.log(dim("
|
|
9155
|
+
console.log(dim(" (cancelled)\n"));
|
|
9090
9156
|
paintBar();
|
|
9091
9157
|
return;
|
|
9092
9158
|
}
|
|
@@ -9109,10 +9175,10 @@ async function showOtherAgents() {
|
|
|
9109
9175
|
{ id: "stop", label: "Stop it", note: "ends the run — its unsaved work is lost" },
|
|
9110
9176
|
];
|
|
9111
9177
|
console.log("");
|
|
9112
|
-
console.log(dim(`
|
|
9178
|
+
console.log(dim(` ${shortName} (${PICK_HINT}):`));
|
|
9113
9179
|
const act = await pickFromList(actions.map((a) => a.label + dim(` · ${a.note}`)), 0);
|
|
9114
9180
|
if (act < 0) {
|
|
9115
|
-
console.log(dim("
|
|
9181
|
+
console.log(dim(" (cancelled)\n"));
|
|
9116
9182
|
paintBar();
|
|
9117
9183
|
return;
|
|
9118
9184
|
}
|
|
@@ -9410,9 +9476,9 @@ async function promptNewCodingKey(backendId = "", provider = "") {
|
|
|
9410
9476
|
const id = await addCodingKey(name, secret, backendId);
|
|
9411
9477
|
console.log(
|
|
9412
9478
|
id
|
|
9413
|
-
? accent(`
|
|
9479
|
+
? accent(` ✓ added ${name || "Unknown"}`) +
|
|
9414
9480
|
(backendId && provider ? dim(` · for ${provider}`) : "") + "\n"
|
|
9415
|
-
: red("
|
|
9481
|
+
: red(" couldn't save the key — try again, or set it in web Settings → Agent\n")
|
|
9416
9482
|
);
|
|
9417
9483
|
return id;
|
|
9418
9484
|
}
|
|
@@ -9505,9 +9571,9 @@ async function replaceCodingKey() {
|
|
|
9505
9571
|
console.log(
|
|
9506
9572
|
ok
|
|
9507
9573
|
? clearing
|
|
9508
|
-
? accent(`
|
|
9509
|
-
: accent(`
|
|
9510
|
-
: red("
|
|
9574
|
+
? accent(` ✓ ${ks.model} API key removed`) + dim(" — coding falls back to your local model\n")
|
|
9575
|
+
: accent(` ✓ ${ks.model} API key saved\n`)
|
|
9576
|
+
: red(" couldn't save the key — try again, or set it in web Settings → Agent\n")
|
|
9511
9577
|
);
|
|
9512
9578
|
paintBar();
|
|
9513
9579
|
}
|
|
@@ -9537,7 +9603,7 @@ async function manageServerKeys({ role = "coding", backendId, host, model = "" }
|
|
|
9537
9603
|
backendId ? fetchServerKeys(backendId) : fetchCodingKeys()
|
|
9538
9604
|
);
|
|
9539
9605
|
if (!keys) {
|
|
9540
|
-
console.log(red(`
|
|
9606
|
+
console.log(red(` couldn't read the keys for ${host} — is this machine's API up to date? (\`gu api restart\`)\n`));
|
|
9541
9607
|
paintBar();
|
|
9542
9608
|
return "";
|
|
9543
9609
|
}
|
|
@@ -9582,7 +9648,7 @@ async function manageServerKeys({ role = "coding", backendId, host, model = "" }
|
|
|
9582
9648
|
const fresh = await fetchServerKeys(backendId);
|
|
9583
9649
|
const added = fresh?.find((k) => k.id === id);
|
|
9584
9650
|
if (added?.default) {
|
|
9585
|
-
console.log(accent(`
|
|
9651
|
+
console.log(accent(` ✓ ${added.name} is ${host}'s default key\n`));
|
|
9586
9652
|
paintBar();
|
|
9587
9653
|
return id;
|
|
9588
9654
|
}
|
|
@@ -9594,7 +9660,7 @@ async function manageServerKeys({ role = "coding", backendId, host, model = "" }
|
|
|
9594
9660
|
if (!key.backendId && backendId) {
|
|
9595
9661
|
const ok = await withPendingStatus(`tying ${key.name} to ${host}`, () => bindKeyToServer(key.id, backendId));
|
|
9596
9662
|
if (!ok) {
|
|
9597
|
-
console.log(red(`
|
|
9663
|
+
console.log(red(` couldn't tie ${key.name} to ${host} — nothing changed.\n`));
|
|
9598
9664
|
paintBar();
|
|
9599
9665
|
return "";
|
|
9600
9666
|
}
|
|
@@ -9613,10 +9679,10 @@ async function applyKeyPick({ role, backendId, host, key }) {
|
|
|
9613
9679
|
let scope = "folder";
|
|
9614
9680
|
if (!backendId) {
|
|
9615
9681
|
await saveFolderKeyPick(role, key.id);
|
|
9616
|
-
console.log(accent(`
|
|
9682
|
+
console.log(accent(` ✓ ${folderLabel()}'s ${roleLabel} → ${key.name}`) + dim(` ····${key.tail}\n`));
|
|
9617
9683
|
} else if (key.default) {
|
|
9618
9684
|
await saveFolderKeyPick(role, "");
|
|
9619
|
-
console.log(accent(`
|
|
9685
|
+
console.log(accent(` ✓ ${folderLabel()}'s ${roleLabel} → ${key.name}`) + dim(` ····${key.tail} · ${host}'s default\n`));
|
|
9620
9686
|
} else {
|
|
9621
9687
|
const choices = sk.scopeChoices({ host, role });
|
|
9622
9688
|
console.log("\n" + GUTTER + dim(`Use ${key.name} for…`));
|
|
@@ -9630,16 +9696,16 @@ async function applyKeyPick({ role, backendId, host, key }) {
|
|
|
9630
9696
|
if (scope === "default") {
|
|
9631
9697
|
const ok = await withPendingStatus(`making ${key.name} the default for ${host}`, () => setServerDefaultKey(backendId, key.id));
|
|
9632
9698
|
if (!ok) {
|
|
9633
|
-
console.log(red("
|
|
9699
|
+
console.log(red(" couldn't change the default — nothing changed.\n"));
|
|
9634
9700
|
paintBar();
|
|
9635
9701
|
return "";
|
|
9636
9702
|
}
|
|
9637
9703
|
// This folder follows the default now, rather than pinning a copy of it.
|
|
9638
9704
|
await saveFolderKeyPick(role, "");
|
|
9639
|
-
console.log(accent(`
|
|
9705
|
+
console.log(accent(` ✓ ${key.name} is now ${host}'s default`) + dim(` ····${key.tail} · every folder without its own pick\n`));
|
|
9640
9706
|
} else {
|
|
9641
9707
|
await saveFolderKeyPick(role, key.id);
|
|
9642
|
-
console.log(accent(`
|
|
9708
|
+
console.log(accent(` ✓ ${folderLabel()}'s ${roleLabel} → ${key.name}`) + dim(` ····${key.tail} · this folder only\n`));
|
|
9643
9709
|
}
|
|
9644
9710
|
}
|
|
9645
9711
|
if (role === "coding") {
|
|
@@ -9693,7 +9759,7 @@ async function deleteServerKeyFlow({ role, key, host, folderKeyId, lastOfServer
|
|
|
9693
9759
|
}
|
|
9694
9760
|
const out = await deleteCodingKey(key.id);
|
|
9695
9761
|
if (!out.ok) {
|
|
9696
|
-
console.log(red("
|
|
9762
|
+
console.log(red(" couldn't delete the key — try again, or remove it in web Settings → Agent\n"));
|
|
9697
9763
|
paintBar();
|
|
9698
9764
|
return;
|
|
9699
9765
|
}
|
|
@@ -9702,7 +9768,7 @@ async function deleteServerKeyFlow({ role, key, host, folderKeyId, lastOfServer
|
|
|
9702
9768
|
const mine = r === "agent" ? sessionAgentKeyId : r === "embed" ? sessionEmbedKeyId : sessionCodingKeyId;
|
|
9703
9769
|
if (mine === key.id) await saveFolderKeyPick(r, "");
|
|
9704
9770
|
}
|
|
9705
|
-
console.log(accent(`
|
|
9771
|
+
console.log(accent(` ✓ deleted ${out.name || key.name}`) + dim(` ····${key.tail} · ${out.remaining} key(s) left\n`));
|
|
9706
9772
|
paintBar();
|
|
9707
9773
|
}
|
|
9708
9774
|
|
|
@@ -9970,7 +10036,7 @@ async function approvalPrompt(command) {
|
|
|
9970
10036
|
ok = (await runS3Command("/s3")) === true;
|
|
9971
10037
|
} catch (e) {
|
|
9972
10038
|
if (!wasPromptCancelled(e)) throw e;
|
|
9973
|
-
console.log(dim("
|
|
10039
|
+
console.log(dim(" (cancelled — the file is saved on this computer)\n"));
|
|
9974
10040
|
} finally {
|
|
9975
10041
|
following = wasFollowing;
|
|
9976
10042
|
}
|
|
@@ -11088,6 +11154,7 @@ async function runAgentTurn(history, { attachJobId = "", ownJob = false } = {})
|
|
|
11088
11154
|
// whether the user has clicked to expand it. Both reset when the step's Thought changes.
|
|
11089
11155
|
let fullLiveThought = "";
|
|
11090
11156
|
let thoughtExpanded = false;
|
|
11157
|
+
let liveExpandable = false; // the live thought line has more to show than fits (set each tick)
|
|
11091
11158
|
// Task #148: the CURRENT step's raw model output, accumulated in full (fenceThought stops at
|
|
11092
11159
|
// 400 chars — enough for a one-line label, not for the paragraphs we now print). Bounded so a
|
|
11093
11160
|
// runaway generation cannot grow without limit; the prose we keep is far shorter anyway.
|
|
@@ -11298,6 +11365,9 @@ async function runAgentTurn(history, { attachJobId = "", ownJob = false } = {})
|
|
|
11298
11365
|
// "almost done" > "Thinking". When clickable we drive line 1 from the SAME fuller string, so
|
|
11299
11366
|
// the grey expansion CONTINUES where line 1 was cut off instead of repeating the head.
|
|
11300
11367
|
const clickable = !!fullLiveThought && !runningCmd && fullLiveThought.length > max;
|
|
11368
|
+
// #350: the click and Ctrl+T ask the SAME question the highlight does. They used to accept
|
|
11369
|
+
// any thought at all, so a line that was already whole took the click and redrew for nothing.
|
|
11370
|
+
liveExpandable = clickable;
|
|
11301
11371
|
let label, rest;
|
|
11302
11372
|
if (clickable) {
|
|
11303
11373
|
// Split at a WORD boundary near the line-1 width so nothing is cut mid-word: the last
|
|
@@ -11450,14 +11520,14 @@ async function runAgentTurn(history, { attachJobId = "", ownJob = false } = {})
|
|
|
11450
11520
|
if (row < top || row > top + statusContentLines - LEAD_ROWS - WORD_GAP - 2) return;
|
|
11451
11521
|
// Only meaningful when there IS a fuller thought to reveal; otherwise ignore the click
|
|
11452
11522
|
// so we don't flicker an empty expand.
|
|
11453
|
-
if (!thoughtExpanded && !
|
|
11523
|
+
if (!thoughtExpanded && !liveExpandable) return;
|
|
11454
11524
|
toggleThought();
|
|
11455
11525
|
};
|
|
11456
11526
|
// Same expand/collapse, reachable without the mouse: Ctrl+T while a turn runs (see
|
|
11457
11527
|
// toggleActiveThought). Ignored when there's nothing more to reveal, so it never flickers an
|
|
11458
11528
|
// empty expansion.
|
|
11459
11529
|
const toggleThought = () => {
|
|
11460
|
-
if (!thoughtExpanded && !
|
|
11530
|
+
if (!thoughtExpanded && !liveExpandable) return;
|
|
11461
11531
|
thoughtExpanded = !thoughtExpanded;
|
|
11462
11532
|
clearStatus(); // next tick (≤120ms) redraws at the new size; keeps cursor math correct
|
|
11463
11533
|
};
|
|
@@ -12457,7 +12527,10 @@ async function main() {
|
|
|
12457
12527
|
// reaches the model over HTTP), so it runs HERE, where the files are, and calls the
|
|
12458
12528
|
// model machine for tokens. That needs a worker here like any other machine. The earlier
|
|
12459
12529
|
// build suppressed the daemon entirely, which is why this comment exists.
|
|
12460
|
-
|
|
12530
|
+
// GU_NO_WORKER_AUTOSTART=1 (task #351): the terminal UI tests play a scripted turn from a fake
|
|
12531
|
+
// API — no worker is needed, and one started for a throwaway home outlived the test (on
|
|
12532
|
+
// Windows it would be a scheduled task). Nothing a user sets; without it, as always:
|
|
12533
|
+
if (!status.running && process.env.GU_NO_WORKER_AUTOSTART !== "1") {
|
|
12461
12534
|
// NO LONGER A QUESTION (task #159, user 2026-08-09: "it need to run otherwise cannot work,
|
|
12462
12535
|
// so, just notify"). It used to ask, and a "no" was then remembered per folder — but the
|
|
12463
12536
|
// only workable answer was yes: without a worker nothing claims your turns, so a question
|
|
@@ -12471,7 +12544,7 @@ async function main() {
|
|
|
12471
12544
|
// meaningfully refuse.
|
|
12472
12545
|
const steps = await import("./first-run-steps.mjs");
|
|
12473
12546
|
const step = steps.stepById("daemon");
|
|
12474
|
-
console.log(dim(`
|
|
12547
|
+
console.log(dim(` ${step.doing}…`));
|
|
12475
12548
|
let started = false;
|
|
12476
12549
|
let why = "";
|
|
12477
12550
|
try {
|
|
@@ -12479,7 +12552,7 @@ async function main() {
|
|
|
12479
12552
|
started = !!r.started;
|
|
12480
12553
|
localDaemonRunning = started || localDaemonRunning;
|
|
12481
12554
|
if (started) {
|
|
12482
|
-
console.log(dim(`
|
|
12555
|
+
console.log(dim(` started background worker (pid ${r.pid}) · logs ${ctl.LOG_FILE}`));
|
|
12483
12556
|
} else {
|
|
12484
12557
|
why = r.reason || "the worker did not report a pid";
|
|
12485
12558
|
}
|
|
@@ -12493,7 +12566,7 @@ async function main() {
|
|
|
12493
12566
|
await recordStepOutcome("daemon", started, why);
|
|
12494
12567
|
if (!started) {
|
|
12495
12568
|
console.log(
|
|
12496
|
-
yellow("
|
|
12569
|
+
yellow(" the background worker did not start") +
|
|
12497
12570
|
dim(` — ${why}\n ${step.why}.\n Try \`gu start\` to see the error in full.\n`)
|
|
12498
12571
|
);
|
|
12499
12572
|
}
|
|
@@ -12613,7 +12686,12 @@ async function main() {
|
|
|
12613
12686
|
// private mode's local API can itself be unconfigured.
|
|
12614
12687
|
if (p.agentMissing === true) agentMissing = true;
|
|
12615
12688
|
noModelAtAll = nothingToAnswerWith(p);
|
|
12616
|
-
if (p.ragEnabled !== true)
|
|
12689
|
+
if (p.ragEnabled !== true) {
|
|
12690
|
+
embedMissing = true;
|
|
12691
|
+
// …but "off" has two causes, and only the settings can tell them apart (#347). In the
|
|
12692
|
+
// background: startup never waits on it, and the bar is repainted when the answer is in.
|
|
12693
|
+
void syncEmbedWarning({ repair: true });
|
|
12694
|
+
}
|
|
12617
12695
|
// A RESTORED PICK MUST STILL EXIST. The stored id names a model on the box this folder was
|
|
12618
12696
|
// last pointed at — and the box can change (`/model` → change the Ollama server), a model
|
|
12619
12697
|
// can be removed, or the folder can be opened on another machine entirely. Sending a name
|
|
@@ -12797,7 +12875,10 @@ async function main() {
|
|
|
12797
12875
|
// check used to sit INSIDE the "chat model is down" branch, so a client pointing at
|
|
12798
12876
|
// a healthy remote model was never offered them and every turn died with
|
|
12799
12877
|
// "No module named 'smolagents'". Independent now.
|
|
12800
|
-
|
|
12878
|
+
// GU_NO_AGENT_DEPS_PROMPT=1 (task #351): the terminal UI tests run no agent — on a machine
|
|
12879
|
+
// without the framework every test's throwaway home answered "yes" and sat through a pip
|
|
12880
|
+
// install (found on the Ubuntu server: 21 seconds of downloads, then a timeout).
|
|
12881
|
+
if (process.env.GU_NO_AGENT_DEPS_PROMPT !== "1" && !(await md.hasAgentLibs())) {
|
|
12801
12882
|
const skipDeps = await loadSessionSkip(resolve(process.cwd()), "skipDepsPrompt");
|
|
12802
12883
|
if (!skipDeps) {
|
|
12803
12884
|
console.log(
|
|
@@ -12838,7 +12919,7 @@ async function main() {
|
|
|
12838
12919
|
} else {
|
|
12839
12920
|
await saveSessionSkip(resolve(process.cwd()), "skipDepsPrompt", true);
|
|
12840
12921
|
console.log(
|
|
12841
|
-
dim("
|
|
12922
|
+
dim(" Skipped — install later with `gu doctor`") +
|
|
12842
12923
|
dim(clientModeBanner ? " --client" : "") +
|
|
12843
12924
|
dim("; I won't ask again in this folder.\n")
|
|
12844
12925
|
);
|
|
@@ -12872,7 +12953,7 @@ async function main() {
|
|
|
12872
12953
|
);
|
|
12873
12954
|
if (skipSetup) {
|
|
12874
12955
|
console.log(
|
|
12875
|
-
dim("
|
|
12956
|
+
dim(" Set-up skipped (remembered for this folder) — `gu doctor` runs it ") +
|
|
12876
12957
|
dim("any time.\n")
|
|
12877
12958
|
);
|
|
12878
12959
|
}
|
|
@@ -12900,7 +12981,7 @@ async function main() {
|
|
|
12900
12981
|
await ctl2.setClientMode(true);
|
|
12901
12982
|
const st = await ctl2.daemonStatus();
|
|
12902
12983
|
console.log(
|
|
12903
|
-
dim("
|
|
12984
|
+
dim(" Skipped — this machine won't SERVE models; the agent still runs here and\n") +
|
|
12904
12985
|
dim(" calls your model machine over the network. Point the Agent model URL at\n") +
|
|
12905
12986
|
dim(" that machine (not 127.0.0.1) — `gu status` shows what it resolves to.\n") +
|
|
12906
12987
|
dim(" `gu doctor` sets models up here · `/host-mode` undoes this.\n")
|
|
@@ -13078,8 +13159,8 @@ async function main() {
|
|
|
13078
13159
|
const started = await tryStartLocalApi();
|
|
13079
13160
|
console.error(
|
|
13080
13161
|
started
|
|
13081
|
-
? dim("
|
|
13082
|
-
: dim("
|
|
13162
|
+
? dim(" Started it — run `gu` again.\n")
|
|
13163
|
+
: dim(" Start it with: gu-cli api start") +
|
|
13083
13164
|
dim("\n If that fails, check MongoDB is running and see ~/.gonext/local-api.log\n")
|
|
13084
13165
|
);
|
|
13085
13166
|
}
|
|
@@ -13338,7 +13419,12 @@ async function main() {
|
|
|
13338
13419
|
// TRANSCRIPT: the echo is written exactly once per submitted line and never repainted,
|
|
13339
13420
|
// so there is nothing here for a blank to accumulate against.
|
|
13340
13421
|
// As typed, not as sent: "[Image #1]", never the file path the agent needs (#51).
|
|
13341
|
-
|
|
13422
|
+
// Every row under the first one's text (#348): a pasted multi-line question used to be
|
|
13423
|
+
// echoed with only its first line indented, the rest hard against the left edge.
|
|
13424
|
+
const echo = questionEchoText(imageMarkersForDisplay(line), {
|
|
13425
|
+
width: process.stdout.columns || 80,
|
|
13426
|
+
paintPrompt: () => PROMPT_PAST,
|
|
13427
|
+
}) + "\n\n";
|
|
13342
13428
|
if (stopNotice) {
|
|
13343
13429
|
stopNotice = "";
|
|
13344
13430
|
if (stopNoticeTimer) clearTimeout(stopNoticeTimer);
|
|
@@ -13418,7 +13504,10 @@ async function main() {
|
|
|
13418
13504
|
}
|
|
13419
13505
|
if (line === "/embed-model") {
|
|
13420
13506
|
await chooseEmbedModel();
|
|
13421
|
-
|
|
13507
|
+
// Read back what is SAVED (#347). The warning used to be cleared here whatever happened —
|
|
13508
|
+
// a cancelled menu or a failed save removed it for the session, and it returned at the
|
|
13509
|
+
// next start as if the choice had been forgotten.
|
|
13510
|
+
await syncEmbedWarning({ repair: false });
|
|
13422
13511
|
continue;
|
|
13423
13512
|
}
|
|
13424
13513
|
if (line === "/server") {
|
|
@@ -13520,7 +13609,7 @@ async function main() {
|
|
|
13520
13609
|
const direct = line === "/rag-local" ? "local" : line === "/rag-cloud" ? "both" : null;
|
|
13521
13610
|
let chosen = direct;
|
|
13522
13611
|
if (!chosen) {
|
|
13523
|
-
console.log(dim(`
|
|
13612
|
+
console.log(dim(` Where this folder's RAG knowledge base lives (${PICK_HINT}):`));
|
|
13524
13613
|
const idx = await pickFromList(
|
|
13525
13614
|
RAG_STORAGE.map(
|
|
13526
13615
|
(r) =>
|
|
@@ -13531,7 +13620,7 @@ async function main() {
|
|
|
13531
13620
|
Math.max(0, RAG_STORAGE.findIndex((r) => r.id === sessionRagStorage))
|
|
13532
13621
|
);
|
|
13533
13622
|
if (idx < 0) {
|
|
13534
|
-
console.log(dim("
|
|
13623
|
+
console.log(dim(" (cancelled)\n"));
|
|
13535
13624
|
continue;
|
|
13536
13625
|
}
|
|
13537
13626
|
chosen = RAG_STORAGE[idx].id;
|
|
@@ -13547,7 +13636,7 @@ async function main() {
|
|
|
13547
13636
|
// switching to cloud leaves the local shards on disk unread, and switching to local
|
|
13548
13637
|
// leaves the S3 ones untouched — either way the new store is built by the next index.
|
|
13549
13638
|
console.log(
|
|
13550
|
-
accent(`
|
|
13639
|
+
accent(` ✓ RAG storage → ${row.label}`) +
|
|
13551
13640
|
dim(`\n ${row.note}`) +
|
|
13552
13641
|
dim("\n Existing shards are not moved — the new store fills as this folder is re-indexed.\n")
|
|
13553
13642
|
);
|
|
@@ -13654,7 +13743,7 @@ async function main() {
|
|
|
13654
13743
|
if (!wasPromptCancelled(e)) throw e;
|
|
13655
13744
|
// The same words the pickers print, for the same event — a cancel should not read as one
|
|
13656
13745
|
// thing when the answer was typed and another when it was arrowed to.
|
|
13657
|
-
console.log(dim("
|
|
13746
|
+
console.log(dim(" (cancelled)\n"));
|
|
13658
13747
|
continue;
|
|
13659
13748
|
}
|
|
13660
13749
|
// SWITCH TO ANOTHER AGENT (task #211) — chosen with the mouse in showOtherAgents, delivered
|