@tiens.nguyen/gu-cli 1.0.796 → 1.0.798

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