@tiens.nguyen/gu-cli 1.0.796 → 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/gu-repl.mjs CHANGED
@@ -588,13 +588,15 @@ if (cliVerdict.kind === "run") {
588
588
  } else if (argv.includes("--help") || argv.includes("-h")) {
589
589
  console.log(
590
590
  "gu — terminal agent REPL for the current folder\n\n" +
591
- "Usage: gu [-v|--verbose] [--license <pro token>]\n" +
591
+ "Usage: gu [-v|--verbose] [--log] [--license <pro token>]\n" +
592
592
  " gu status # worker + a table of the running model servers\n" +
593
593
  " gu start|stop|restart|logs # manage the background worker\n" +
594
594
  " gu logs <port|pid> [-f] # tail a running model server's log\n" +
595
595
  " gu doctor|login|set # models / account (same as `gu-cli …`)\n" +
596
596
  " gu rag # is RAG working? tests the embedder end-to-end\n\n" +
597
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" +
598
600
  " --license <token> pro license: a first install ASKS how this machine runs (hosted\n" +
599
601
  " client / MLX host / private). Without it, a first install is\n" +
600
602
  " private by default — the whole stack on this machine.\n" +
@@ -614,6 +616,17 @@ if (cliVerdict.kind === "run") {
614
616
  // could not deliver, because agent LOG events stay on the worker's console and never enter
615
617
  // the job stream. It now drives a real, streamable signal.
616
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
+ }
617
630
  if (REPL_VERBOSE) {
618
631
  process.env.GONEXT_REPL_VERBOSE = "1";
619
632
  }
@@ -2446,10 +2459,10 @@ async function runUpdate() {
2446
2459
  if (cli) rows.push({ id: "cli", label: `Update gu to ${cli}`, note: "takes effect after you restart gu" });
2447
2460
  if (!rows.length) return;
2448
2461
  console.log("");
2449
- console.log(dim(` Update (${PICK_HINT}):`));
2462
+ console.log(dim(` Update (${PICK_HINT}):`));
2450
2463
  const idx = await pickFromList(rows.map((r) => r.label + dim(` · ${r.note}`)), 0);
2451
2464
  if (idx < 0) {
2452
- console.log(dim(" (cancelled)\n"));
2465
+ console.log(dim(" (cancelled)\n"));
2453
2466
  paintBar();
2454
2467
  return;
2455
2468
  }
@@ -2476,7 +2489,7 @@ async function runUpdate() {
2476
2489
  r.stderr ? ` — ${String(r.stderr).trim().split("\n").slice(-1)[0]}` : ""}`);
2477
2490
  }
2478
2491
  });
2479
- console.log(accent(` ✓ API updated to ${api}\n`));
2492
+ console.log(accent(` ✓ API updated to ${api}\n`));
2480
2493
  availableUpdates = { ...availableUpdates, api: "", any: Boolean(availableUpdates.cli) };
2481
2494
  } else {
2482
2495
  await withPendingStatus(`updating gu to ${cli}`, async () => {
@@ -2506,12 +2519,12 @@ async function runUpdate() {
2506
2519
  });
2507
2520
  // NOT "updated" — the files changed, THIS process did not. Claiming otherwise is the one
2508
2521
  // way this feature could lie to someone.
2509
- console.log(accent(` ✓ gu ${cli} installed`) +
2522
+ console.log(accent(` ✓ gu ${cli} installed`) +
2510
2523
  dim(` — ${updateRestartNote()}\n`));
2511
2524
  availableUpdates = { ...availableUpdates, cli: "", any: Boolean(availableUpdates.api) };
2512
2525
  }
2513
2526
  } catch (e) {
2514
- console.log(red(` update failed: ${e?.message ?? e}\n`));
2527
+ console.log(red(` update failed: ${e?.message ?? e}\n`));
2515
2528
  }
2516
2529
  paintBar();
2517
2530
  }
@@ -4304,8 +4317,8 @@ if (process.stdin.isTTY) {
4304
4317
  // PROCESS (reported: "PromptCancelled: cancelled … Node.js v22"). Cancel is caught here; any
4305
4318
  // other failure is reported and the session goes on — a keypress must not kill the REPL.
4306
4319
  replaceCodingKey().catch((e) => {
4307
- if (wasPromptCancelled(e)) console.log(dim(" (cancelled)\n"));
4308
- else console.log(red(` ctrl+k failed: ${e?.message ?? e}\n`));
4320
+ if (wasPromptCancelled(e)) console.log(dim(" (cancelled)\n"));
4321
+ else console.log(red(` ctrl+k failed: ${e?.message ?? e}\n`));
4309
4322
  paintBar();
4310
4323
  });
4311
4324
  return;
@@ -5653,10 +5666,10 @@ async function runModelDoctor() {
5653
5666
  const dc = await import("./doctor-check.mjs");
5654
5667
  const settings = await withPendingStatus("reading your model settings", fetchWorkerSettings);
5655
5668
  if (!settings) {
5656
- console.log(red(" couldn't reach the API to read your model settings.\n"));
5669
+ console.log(red(" couldn't reach the API to read your model settings.\n"));
5657
5670
  return;
5658
5671
  }
5659
- console.log(dim(" Asking each model to answer — the real request, not just a ping."));
5672
+ console.log(dim(" Asking each model to answer — the real request, not just a ping."));
5660
5673
  const roles = dc.rolesFromSettings(settings);
5661
5674
  const results = [];
5662
5675
  for (const r of roles) {
@@ -5699,7 +5712,7 @@ async function reconfigureOllamaServer() {
5699
5712
 
5700
5713
  const settings = await withPendingStatus("reading your model settings", fetchWorkerSettings);
5701
5714
  if (!settings) {
5702
- console.log(red(" couldn't reach the API to read your current settings.\n"));
5715
+ console.log(red(" couldn't reach the API to read your current settings.\n"));
5703
5716
  return;
5704
5717
  }
5705
5718
 
@@ -5728,7 +5741,7 @@ async function reconfigureOllamaServer() {
5728
5741
  console.log(dim(` ${hint()}`));
5729
5742
  // askSecret: masked, kept out of history; Esc throws PromptCancelled → /model is cancelled
5730
5743
  // with nothing changed, which is what "skip all" means here.
5731
- const k = (await askSecret(` API key for ${host} (Enter for none): `)).trim();
5744
+ const k = (await askSecret(` API key for ${host} (Enter for none): `)).trim();
5732
5745
  typedKeys.set(host, k);
5733
5746
  console.log(GUTTER + dim(ui.keyConfirmation(host, k)));
5734
5747
  return k;
@@ -5744,7 +5757,7 @@ async function reconfigureOllamaServer() {
5744
5757
  // instruction, leaving the reader unsure whether their own address needs it. It does not
5745
5758
  // — normalizeOllamaUrl adds it — and displayUrl round-trips, so what is shown can be fed
5746
5759
  // straight back as the default.
5747
- console.log(dim(` ${label} — Enter keeps ${os.displayUrl(dflt)}`));
5760
+ console.log(dim(` ${label} — Enter keeps ${os.displayUrl(dflt)}`));
5748
5761
  console.log(dim(` ${hint()}`));
5749
5762
  // THE ROW MUST SAY WHICH SERVER IT IS ASKING FOR. It said a bare "server:" — chosen to
5750
5763
  // fit the frozen row, which traded away the one thing the label was for. Two questions
@@ -5760,7 +5773,7 @@ async function reconfigureOllamaServer() {
5760
5773
  // other. See urlCandidates; the load decides, not the heuristic.
5761
5774
  const candidates = os.urlCandidates(typed || dflt);
5762
5775
  if (!candidates.length) {
5763
- console.log(dim(" that is not a server address — try 192.168.1.40 or a full URL"));
5776
+ console.log(dim(" that is not a server address — try 192.168.1.40 or a full URL"));
5764
5777
  continue;
5765
5778
  }
5766
5779
  let n = null;
@@ -5775,7 +5788,7 @@ async function reconfigureOllamaServer() {
5775
5788
  try {
5776
5789
  models = await load(cand, key);
5777
5790
  n = cand;
5778
- if (candidates.length > 1) console.log(dim(` using ${os.displayUrl(cand.url)}`));
5791
+ if (candidates.length > 1) console.log(dim(` using ${os.displayUrl(cand.url)}`));
5779
5792
  return;
5780
5793
  } catch (err) {
5781
5794
  lastErr = err;
@@ -5785,31 +5798,31 @@ async function reconfigureOllamaServer() {
5785
5798
  await tryAll();
5786
5799
  // Refused for want of credentials: ask now, whatever the address looked like, and list again.
5787
5800
  if (!n && ui.needsKeyAfterStatus(lastErr?.httpStatus) && !key) {
5788
- console.log(yellow(` ${candidates[0].host} needs an API key to list its models.`));
5801
+ console.log(yellow(` ${candidates[0].host} needs an API key to list its models.`));
5789
5802
  typedKeys.delete(candidates[0].host);
5790
5803
  key = await askKeyFor(candidates[0].host);
5791
5804
  if (key) await tryAll();
5792
5805
  }
5793
5806
  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" : ""}.`));
5807
+ console.log(red(` ${candidates[0].host} refused the request (HTTP ${lastErr.httpStatus})${key ? " — that key was not accepted" : ""}.`));
5795
5808
  continue;
5796
5809
  }
5797
5810
  if (!n) {
5798
5811
  if (lastErr?.httpStatus === 404) {
5799
5812
  // IT ANSWERED — it is simply not an Ollama (#43). "Couldn't reach … is Ollama listening"
5800
5813
  // sent people to debug a server that was working.
5801
- for (const l of am.describeNotOllama(candidates[0].host, role)) console.log(yellow(` ${l}`));
5814
+ for (const l of am.describeNotOllama(candidates[0].host, role)) console.log(yellow(` ${l}`));
5802
5815
  continue;
5803
5816
  }
5804
5817
  // One address typed, one failure reported — naming every candidate would be two errors
5805
5818
  // 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."));
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."));
5808
5821
  continue;
5809
5822
  }
5810
5823
  const usable = os.usableModelList(models);
5811
5824
  if (!usable.ok) {
5812
- for (const l of os.describeUnusableList(usable.why, n.host)) console.log(dim(` ${l}`));
5825
+ for (const l of os.describeUnusableList(usable.why, n.host)) console.log(dim(` ${l}`));
5813
5826
  continue;
5814
5827
  }
5815
5828
  if (require) {
@@ -5818,13 +5831,13 @@ async function reconfigureOllamaServer() {
5818
5831
  // rather than the server.
5819
5832
  const need = os.checkRequired(models);
5820
5833
  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)"));
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)"));
5823
5836
  continue;
5824
5837
  }
5825
- console.log(dim(` ✓ ${n.host} has the ${os.REQUIRED_AGENT_PREFIX} family and the embedder`));
5838
+ console.log(dim(` ✓ ${n.host} has the ${os.REQUIRED_AGENT_PREFIX} family and the embedder`));
5826
5839
  }
5827
- console.log(dim(` found ${models.length} model${models.length === 1 ? "" : "s"} on ${n.host}`));
5840
+ console.log(dim(` found ${models.length} model${models.length === 1 ? "" : "s"} on ${n.host}`));
5828
5841
  return { normalized: n, models };
5829
5842
  }
5830
5843
  return null;
@@ -5841,11 +5854,11 @@ async function reconfigureOllamaServer() {
5841
5854
  let agent = null;
5842
5855
  if (agentOnProvider) {
5843
5856
  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."));
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."));
5846
5859
  } 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."));
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."));
5849
5862
  agent = await askServer("Agent + embedding server", curAgent, {
5850
5863
  require: true,
5851
5864
  short: "agent + embed server",
@@ -5865,7 +5878,7 @@ async function reconfigureOllamaServer() {
5865
5878
  }
5866
5879
 
5867
5880
  if (!agent && !coder) {
5868
- console.log(dim(" nothing set — models stay as they are.\n"));
5881
+ console.log(dim(" nothing set — models stay as they are.\n"));
5869
5882
  return;
5870
5883
  }
5871
5884
 
@@ -5888,13 +5901,13 @@ async function reconfigureOllamaServer() {
5888
5901
  const size = am.formatSize(m.sizeBytes);
5889
5902
  return `${m.model}${size ? dim(` ${size}`) : ""}${m.model === agentDefault ? dim(" (current)") : ""}`;
5890
5903
  });
5891
- console.log(dim(` AGENT model — routes every turn, writes the plain replies (${PICK_HINT}):`));
5904
+ console.log(dim(` AGENT model — routes every turn, writes the plain replies (${PICK_HINT}):`));
5892
5905
  const pick = await pickFromList(
5893
5906
  labels,
5894
5907
  Math.max(0, cands.findIndex((m) => m.model === agentDefault))
5895
5908
  );
5896
5909
  if (pick < 0) {
5897
- console.log(dim(" (cancelled — nothing changed)\n"));
5910
+ console.log(dim(" (cancelled — nothing changed)\n"));
5898
5911
  return;
5899
5912
  }
5900
5913
  chat = cands[pick].model;
@@ -5913,10 +5926,10 @@ async function reconfigureOllamaServer() {
5913
5926
  const kind = am.isLikelyEmbedder(m) ? dim(" · embedder") : "";
5914
5927
  return `${m.model}${size ? dim(` ${size}`) : ""}${kind}${m.model === roles.code ? dim(" (current)") : ""}`;
5915
5928
  });
5916
- console.log(dim(` CODING model — writes the code; worth waiting for (${PICK_HINT}):`));
5929
+ console.log(dim(` CODING model — writes the code; worth waiting for (${PICK_HINT}):`));
5917
5930
  const idx = await pickFromList(labels, Math.max(0, coder.models.findIndex((m) => m.model === roles.code)));
5918
5931
  if (idx < 0) {
5919
- console.log(dim(" (cancelled — nothing changed)\n"));
5932
+ console.log(dim(" (cancelled — nothing changed)\n"));
5920
5933
  return;
5921
5934
  }
5922
5935
  code = coder.models[idx].model;
@@ -5931,7 +5944,7 @@ async function reconfigureOllamaServer() {
5931
5944
  .flatMap((b) => (Array.isArray(b.models) ? b.models : []))
5932
5945
  );
5933
5946
  console.log(
5934
- dim(` Also offer these in /model? (up to ${MAX_ALT_CODERS} · ${PICK_MANY_HINT}):`)
5947
+ dim(` Also offer these in /model? (up to ${MAX_ALT_CODERS} · ${PICK_MANY_HINT}):`)
5935
5948
  );
5936
5949
  const ticks = await pickManyFromList(
5937
5950
  rest.map((m) => `${m.model}${am.formatSize(m.sizeBytes) ? dim(` ${am.formatSize(m.sizeBytes)}`) : ""}`),
@@ -5940,7 +5953,7 @@ async function reconfigureOllamaServer() {
5940
5953
  );
5941
5954
  // null is Escape and cancels the command; an EMPTY list is a legitimate "offer none".
5942
5955
  if (ticks === null) {
5943
- console.log(dim(" (cancelled — nothing changed)\n"));
5956
+ console.log(dim(" (cancelled — nothing changed)\n"));
5944
5957
  return;
5945
5958
  }
5946
5959
  extras = ticks.map((i) => rest[i].model);
@@ -5986,7 +5999,7 @@ async function reconfigureOllamaServer() {
5986
5999
  md.saveWorkerSettings(apiBase, workerKey, patch)
5987
6000
  );
5988
6001
  if (!saved) {
5989
- console.log(red(" couldn't save that to this machine — nothing changed."));
6002
+ console.log(red(" couldn't save that to this machine — nothing changed."));
5990
6003
  console.log(GUTTER + dim("if this machine's local API is old, restart it with `gu api restart`.\n"));
5991
6004
  return;
5992
6005
  }
@@ -6006,13 +6019,13 @@ async function reconfigureOllamaServer() {
6006
6019
  const url = used.find((u) => hostnameOf(u) === host);
6007
6020
  const found = ps.providerRowForUrl(ps.existingBackendRows(fresh ?? {}), url);
6008
6021
  if (!found) {
6009
- console.log(yellow(` no server row holds ${host} yet — add its key with ctrl+k in /model.`));
6022
+ console.log(yellow(` no server row holds ${host} yet — add its key with ctrl+k in /model.`));
6010
6023
  continue;
6011
6024
  }
6012
6025
  const ok = await withPendingStatus("saving the key", () => addCodingKey(host, k, found.id));
6013
6026
  console.log(ok
6014
6027
  ? 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.`));
6028
+ : red(` couldn't store the key for ${host} — add it with ctrl+k.`));
6016
6029
  }
6017
6030
  }
6018
6031
  }
@@ -6030,11 +6043,11 @@ async function reconfigureOllamaServer() {
6030
6043
  for (const u of cloudUrls) {
6031
6044
  const found = ps.providerRowForUrl(ps.existingBackendRows(fresh ?? {}), u);
6032
6045
  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).`));
6046
+ console.log(yellow(` ${hostOf(u)} is the Ollama cloud — it needs its own API key (the model list worked without one).`));
6034
6047
  // The shared key prompt (task #245); the server's first key becomes its default.
6035
6048
  const { name, secret } = await askServerKey({ host: hostOf(u) });
6036
6049
  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"));
6050
+ console.log(red(" couldn't save the key — add it with ctrl+k.\n"));
6038
6051
  }
6039
6052
  }
6040
6053
  }
@@ -6070,7 +6083,7 @@ async function reconfigureOllamaServer() {
6070
6083
  if (g !== null) globalOutputTokens = g;
6071
6084
  }
6072
6085
  paintBar(); // …and show all of it now, not at the next keystroke
6073
- console.log(accent(" ✓ models saved"));
6086
+ console.log(accent(" ✓ models saved"));
6074
6087
  if (agent) {
6075
6088
  console.log(dim(` chat ${chat} @ ${agent.normalized.host}`));
6076
6089
  console.log(dim(` RAG ${os.defaultEmbedModel?.(agent.models) ?? os.REQUIRED_EMBED_MODEL} @ ${agent.normalized.host}`));
@@ -6088,9 +6101,9 @@ async function reconfigureOllamaServer() {
6088
6101
  // falsely scream "changed" while the seeded value is exactly what settings now hold.
6089
6102
  const seededEmbed = agent ? (os.defaultEmbedModel?.(agent.models) ?? os.REQUIRED_EMBED_MODEL) : "";
6090
6103
  if (agent && String(settings.ragEmbedModel ?? "") !== seededEmbed) {
6091
- console.log(dim(" the embedder changed — existing RAG indexes need rebuilding (/rag)."));
6104
+ console.log(dim(" the embedder changed — existing RAG indexes need rebuilding (/rag)."));
6092
6105
  }
6093
- console.log(dim(" takes effect on your next question.\n"));
6106
+ console.log(dim(" takes effect on your next question.\n"));
6094
6107
  }
6095
6108
 
6096
6109
  // ---------- fetch the ready-to-run agent payload from user settings ----------
@@ -6141,7 +6154,7 @@ async function reconfigureOllamaServer() {
6141
6154
  */
6142
6155
  async function pickExtraModels(rest, preChecked = []) {
6143
6156
  if (rest.length === 0) return [];
6144
- console.log(dim(` Also offer these in /model? (up to ${MAX_ALT_CODERS} · ${PICK_MANY_HINT}):`));
6157
+ console.log(dim(` Also offer these in /model? (up to ${MAX_ALT_CODERS} · ${PICK_MANY_HINT}):`));
6145
6158
  const already = new Set(preChecked);
6146
6159
  const ticks = await pickManyFromList(
6147
6160
  rest,
@@ -6187,7 +6200,7 @@ async function chooseCodingModelFrom(ps, all, question, current) {
6187
6200
  const idx = await pickFromList(rows, at >= 0 ? at : 0);
6188
6201
  if (idx < 0) return null;
6189
6202
  if (idx === list.length) {
6190
- const typed = (await ask(" model name: ")).trim();
6203
+ const typed = (await ask(" model name: ")).trim();
6191
6204
  return typed || null;
6192
6205
  }
6193
6206
  if (idx === list.length + 1) {
@@ -6215,11 +6228,11 @@ async function chooseCodingModelFrom(ps, all, question, current) {
6215
6228
  async function chooseFromModelList(all, question, current = 0) {
6216
6229
  const rows = [...all, "type a model name…"];
6217
6230
  const at = typeof current === "string" ? all.indexOf(current) : Number(current) || 0;
6218
- console.log(dim(` ${question} (${PICK_HINT})`));
6231
+ console.log(dim(` ${question} (${PICK_HINT})`));
6219
6232
  const idx = await pickFromList(rows, at >= 0 ? at : 0);
6220
6233
  if (idx < 0) return null;
6221
6234
  if (idx === all.length) {
6222
- const typed = (await ask(" model name: ")).trim();
6235
+ const typed = (await ask(" model name: ")).trim();
6223
6236
  return typed || null;
6224
6237
  }
6225
6238
  return all[idx];
@@ -6229,7 +6242,7 @@ async function manageCodingProviders() {
6229
6242
  const ps = await import("./coding-provider-setup.mjs");
6230
6243
  const settings = await withPendingStatus("reading your providers", fetchWorkerSettings);
6231
6244
  if (!settings) {
6232
- console.log(red(" couldn't reach the API to read your current settings.\n"));
6245
+ console.log(red(" couldn't reach the API to read your current settings.\n"));
6233
6246
  return;
6234
6247
  }
6235
6248
  const rows = ps.existingBackendRows(settings);
@@ -6246,10 +6259,10 @@ async function manageCodingProviders() {
6246
6259
  );
6247
6260
  const ADD = labels.length;
6248
6261
  labels.push("add another provider — Claude API, OpenAI, Kimi…");
6249
- console.log(dim(` Providers — pick one to change it, or add another (${PICK_HINT}):`));
6262
+ console.log(dim(` Providers — pick one to change it, or add another (${PICK_HINT}):`));
6250
6263
  const idx = await pickFromList(labels, 0);
6251
6264
  if (idx < 0) {
6252
- console.log(dim(" (cancelled)\n"));
6265
+ console.log(dim(" (cancelled)\n"));
6253
6266
  return;
6254
6267
  }
6255
6268
  if (idx === ADD) {
@@ -6264,7 +6277,7 @@ async function manageCodingProviders() {
6264
6277
  1
6265
6278
  );
6266
6279
  if (action <= 0) {
6267
- console.log(dim(" (nothing changed)\n"));
6280
+ console.log(dim(" (nothing changed)\n"));
6268
6281
  return;
6269
6282
  }
6270
6283
  if (action === 1) await changeProviderModel(ps, rows, p);
@@ -6323,12 +6336,12 @@ async function changeProviderModel(ps, rows, p) {
6323
6336
  if (models.length) {
6324
6337
  model = await chooseCodingModelFrom(ps, models, `Which model should ${p.host} use?`, p.model);
6325
6338
  if (model === null) {
6326
- console.log(dim(" (nothing changed)\n"));
6339
+ console.log(dim(" (nothing changed)\n"));
6327
6340
  return;
6328
6341
  }
6329
6342
  } else {
6330
6343
  console.log(GUTTER + dim(`Enter keeps ${p.model || "it unset"}`));
6331
- model = (await ask(" model name: ")).trim();
6344
+ model = (await ask(" model name: ")).trim();
6332
6345
  }
6333
6346
  if (!model) {
6334
6347
  console.log(GUTTER + dim("(nothing changed)\n"));
@@ -6352,7 +6365,7 @@ async function changeProviderModel(ps, rows, p) {
6352
6365
  })
6353
6366
  );
6354
6367
  if (!saved) {
6355
- console.log(red(" couldn't save that — nothing changed.\n"));
6368
+ console.log(red(" couldn't save that — nothing changed.\n"));
6356
6369
  return;
6357
6370
  }
6358
6371
  // THE FOLDER'S PICK NAMES THE OLD MODEL. Leaving it would send a name this provider no longer
@@ -6366,14 +6379,14 @@ async function changeProviderModel(ps, rows, p) {
6366
6379
  await syncActiveCoderWhere();
6367
6380
  console.log(GUTTER + dim("this folder's pick named the old model — cleared it."));
6368
6381
  }
6369
- console.log(accent(` ✓ ${p.host} → ${model}`) + dim(" · pick it in /model\n"));
6382
+ console.log(accent(` ✓ ${p.host} → ${model}`) + dim(" · pick it in /model\n"));
6370
6383
  }
6371
6384
 
6372
6385
  /** Point a provider at a different URL, keeping its id so saved picks still resolve. */
6373
6386
  async function changeProviderUrl(ps, rows, p) {
6374
6387
  const md = await import("./model-doctor.mjs");
6375
6388
  console.log("\n" + GUTTER + dim(`Enter keeps ${p.url}`));
6376
- const typed = (await ask(" provider URL: ")).trim();
6389
+ const typed = (await ask(" provider URL: ")).trim();
6377
6390
  if (!typed || typed === p.url) {
6378
6391
  console.log(GUTTER + dim("(nothing changed)\n"));
6379
6392
  return;
@@ -6398,12 +6411,12 @@ async function changeProviderUrl(ps, rows, p) {
6398
6411
  })
6399
6412
  );
6400
6413
  if (!saved) {
6401
- console.log(red(" couldn't save that — nothing changed.\n"));
6414
+ console.log(red(" couldn't save that — nothing changed.\n"));
6402
6415
  return;
6403
6416
  }
6404
6417
  // The KEY still belongs to this row, because the row id did not change — which is the whole
6405
6418
  // reason an id is not derived from the URL.
6406
- console.log(accent(` ✓ ${p.host} → ${r.host}`) + dim(" · its key still applies\n"));
6419
+ console.log(accent(` ✓ ${p.host} → ${r.host}`) + dim(" · its key still applies\n"));
6407
6420
  }
6408
6421
 
6409
6422
  /** Remove a provider, and the keys that would be stranded by its removal. */
@@ -6436,7 +6449,7 @@ async function removeCodingProvider(ps, rows, p) {
6436
6449
  })
6437
6450
  );
6438
6451
  if (!saved) {
6439
- console.log(red(" couldn't save that — nothing was removed.\n"));
6452
+ console.log(red(" couldn't save that — nothing was removed.\n"));
6440
6453
  return;
6441
6454
  }
6442
6455
  // THE ROW GOES FIRST. If a key delete fails afterwards the key is merely orphaned, which is
@@ -6460,7 +6473,7 @@ async function removeCodingProvider(ps, rows, p) {
6460
6473
  console.log(GUTTER + dim("this folder was using it — cleared the pick.\n"));
6461
6474
  }
6462
6475
  console.log(
6463
- accent(` ✓ removed ${p.host}`) +
6476
+ accent(` ✓ removed ${p.host}`) +
6464
6477
  (stranded.length ? dim(` · ${deleted} key${deleted === 1 ? "" : "s"} deleted`) : "") +
6465
6478
  "\n"
6466
6479
  );
@@ -6501,7 +6514,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
6501
6514
 
6502
6515
  const settings = await withPendingStatus("reading your model settings", fetchWorkerSettings);
6503
6516
  if (!settings) {
6504
- console.log(red(" couldn't reach the API to read your current settings.\n"));
6517
+ console.log(red(" couldn't reach the API to read your current settings.\n"));
6505
6518
  return;
6506
6519
  }
6507
6520
  // EVERY ROW THAT IS ALREADY THERE, including the synthesized one a pre-#123 account keeps in
@@ -6527,10 +6540,10 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
6527
6540
  // without Claude, which serves no embeddings).
6528
6541
  const presets = ps.providerPresets(role);
6529
6542
  const rowsShown = [...presets.map((p) => `${p.name}${dim(` · ${p.url}`)}`), "another URL…"];
6530
- console.log(dim(` Which provider? (${PICK_HINT})`));
6543
+ console.log(dim(` Which provider? (${PICK_HINT})`));
6531
6544
  const pi = await pickFromList(rowsShown, 0);
6532
6545
  if (pi < 0) {
6533
- console.log(dim(" (nothing saved)\n"));
6546
+ console.log(dim(" (nothing saved)\n"));
6534
6547
  return;
6535
6548
  }
6536
6549
  if (pi < presets.length) target = as.serverTarget("openai", presets[pi].url);
@@ -6538,12 +6551,12 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
6538
6551
  if (!target && !presetUrl) {
6539
6552
  const dflt = as.defaultServerUrl(role, dialect, settings, existing);
6540
6553
  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, …"));
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, …"));
6543
6556
  // The address goes ABOVE, in full, and Enter's meaning is said out loud (46d1524e).
6544
6557
  console.log(dim(dflt ? ` Enter uses ${dflt} · Esc cancels\n` : " Esc cancels.\n"));
6545
6558
  for (let attempt = 0; attempt < 3 && !target; attempt++) {
6546
- const typed = await ask(" server URL: ");
6559
+ const typed = await ask(" server URL: ");
6547
6560
  if (typed === null || as.isCancelWord(typed)) break;
6548
6561
  const raw = typed.trim() || dflt;
6549
6562
  if (!raw) break;
@@ -6560,7 +6573,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
6560
6573
  }
6561
6574
  }
6562
6575
  if (!target) {
6563
- console.log(dim(" (nothing saved)\n"));
6576
+ console.log(dim(" (nothing saved)\n"));
6564
6577
  return;
6565
6578
  }
6566
6579
  const host = target.host;
@@ -6582,7 +6595,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
6582
6595
  // THE ROW CAP BEFORE ANY QUESTION about a new server: exceeding it is a silent 200 with no row,
6583
6596
  // and asking for a key and a model first would be the worst order to discover that in.
6584
6597
  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`));
6598
+ console.log(yellow(` ${ps.MAX_BACKENDS} servers is the limit — remove one in /model → manage providers first.\n`));
6586
6599
  return;
6587
6600
  }
6588
6601
  const need = as.keyRequirement(target);
@@ -6627,7 +6640,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
6627
6640
  let probe = await list();
6628
6641
  // THE STORED KEY WAS REFUSED — ask for the right one once, rather than saving nothing.
6629
6642
  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.`));
6643
+ console.log(yellow(` ${host} refused the stored key (HTTP ${probe.status}) — enter the right one.`));
6631
6644
  if (!(await askKey()) || !key) return;
6632
6645
  probe = await list();
6633
6646
  }
@@ -6662,11 +6675,11 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
6662
6675
  ? await chooseCodingModelFrom(ps, models, R.question, 0)
6663
6676
  : await chooseFromModelList(models, R.question, Math.max(0, models.indexOf(current)));
6664
6677
  if (model === null) {
6665
- console.log(dim(" (cancelled — nothing was saved)\n"));
6678
+ console.log(dim(" (cancelled — nothing was saved)\n"));
6666
6679
  return;
6667
6680
  }
6668
6681
  } else {
6669
- model = String((await ask(" model name: ")) ?? "").trim();
6682
+ model = String((await ask(" model name: ")) ?? "").trim();
6670
6683
  }
6671
6684
  if (!model) {
6672
6685
  console.log(GUTTER + dim("(cancelled — nothing was saved)\n"));
@@ -6683,7 +6696,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
6683
6696
  if (!sw.changed) return;
6684
6697
  plan = sw.plan;
6685
6698
  } 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`));
6699
+ console.log(dim(` already embedding with ${model} on ${host} — nothing changed.\n`));
6687
6700
  return;
6688
6701
  }
6689
6702
  }
@@ -6712,7 +6725,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
6712
6725
  } else {
6713
6726
  const r = as.rowForSave(role, { existing: found.row, model });
6714
6727
  if (r.full) {
6715
- console.log(yellow(` ${host} already offers ${ps.MAX_MODELS_PER_SERVER} models — remove one in /model → manage providers first.\n`));
6728
+ console.log(yellow(` ${host} already offers ${ps.MAX_MODELS_PER_SERVER} models — remove one in /model → manage providers first.\n`));
6716
6729
  return;
6717
6730
  }
6718
6731
  if (r.changed) rows = existing.map((b) => (b === found.row ? r.row : b));
@@ -6720,7 +6733,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
6720
6733
  if (rows !== existing) {
6721
6734
  const ok = await withPendingStatus(`saving ${host}`, () => md.saveWorkerSettings(apiBase, workerKey, { agentCodingBackends: rows }));
6722
6735
  if (!ok) {
6723
- console.log(red(` couldn't save ${host} — nothing changed.`));
6736
+ console.log(red(` couldn't save ${host} — nothing changed.`));
6724
6737
  console.log(GUTTER + dim("if this machine's local API is old, restart it with `gu api restart`.\n"));
6725
6738
  return;
6726
6739
  }
@@ -6729,7 +6742,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
6729
6742
  if (typedKey) {
6730
6743
  const id = await withPendingStatus("saving the key", () => addCodingKey(keyName, key, rowId));
6731
6744
  if (!id) {
6732
- console.log(yellow(` ${host} was saved, but its key was not — nothing else changed.`));
6745
+ console.log(yellow(` ${host} was saved, but its key was not — nothing else changed.`));
6733
6746
  console.log(GUTTER + dim("try again, or add the key with ctrl+k.\n"));
6734
6747
  return;
6735
6748
  }
@@ -6747,7 +6760,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
6747
6760
  console.log(GUTTER + dim("the banner shows the new model next time gu starts.\n"));
6748
6761
  } else if (rows !== existing || typedKey) {
6749
6762
  // 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.`));
6763
+ console.log(yellow(` ${host} was saved, but the agent model was not pointed at it.`));
6751
6764
  console.log(GUTTER + dim("run /agent-model to finish, or check the API is reachable.\n"));
6752
6765
  }
6753
6766
  return;
@@ -6760,7 +6773,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
6760
6773
  if (!(await saveEmbedSettings(fields))) return;
6761
6774
  if (plan) announceEmbedSaved({ chosen: model, plan, url: target.roleUrl("embed") });
6762
6775
  else {
6763
- console.log(accent(` ✓ embedding server → ${host}`));
6776
+ console.log(accent(` ✓ embedding server → ${host}`));
6764
6777
  console.log(dim(` ${model} did not change, so no knowledge base rebuilds — the same model just answers from ${host}.\n`));
6765
6778
  }
6766
6779
  return;
@@ -6788,7 +6801,7 @@ async function useCoderInThisFolder({ ps, rows, beforeChoices, url, host, locali
6788
6801
  (c) => c.model === model && (c.backendId ? c.backendId === backendId : sameHost(c.url, url))
6789
6802
  );
6790
6803
  if (!offered) {
6791
- console.log(yellow(` ${host} was saved, but this API is not offering ${model} yet.`));
6804
+ console.log(yellow(` ${host} was saved, but this API is not offering ${model} yet.`));
6792
6805
  console.log(GUTTER + dim("run /model — if it is missing there too, the API may be older than"));
6793
6806
  console.log(GUTTER + dim("this feature; restart it with `gu api restart`.\n"));
6794
6807
  return;
@@ -6802,7 +6815,7 @@ async function useCoderInThisFolder({ ps, rows, beforeChoices, url, host, locali
6802
6815
  ])
6803
6816
  );
6804
6817
  if (lost.length) {
6805
- console.log(yellow(` ⚠ this API dropped ${lost.length === 1 ? "a provider" : `${lost.length} providers`} when saving:`));
6818
+ console.log(yellow(` ⚠ this API dropped ${lost.length === 1 ? "a provider" : `${lost.length} providers`} when saving:`));
6806
6819
  for (const u of lost) console.log(GUTTER + dim(` ${u}`));
6807
6820
  if (ps.apiKeysBackendsByKind(after?.codingChoices)) {
6808
6821
  console.log(GUTTER + dim("this API identifies a backend by its KIND, so it holds ONE"));
@@ -6826,8 +6839,8 @@ async function useCoderInThisFolder({ ps, rows, beforeChoices, url, host, locali
6826
6839
  // The key question for this coder is answered — leaving the flag set would warn about a key
6827
6840
  // that was just entered, which reads as the save having failed.
6828
6841
  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`));
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`));
6831
6844
  // A HOSTED SERVER NEEDS ITS WINDOW SET: with nothing set gu assumes 24,000 prompt tokens and
6832
6845
  // compacts there — on a large hosted model that throws history away every turn.
6833
6846
  if (ps.shouldMentionContextWindow(locality)) {
@@ -6847,7 +6860,7 @@ async function chooseModel() {
6847
6860
  fetchAgentPayload([{ role: "user", content: "ping" }])
6848
6861
  );
6849
6862
  } catch (err) {
6850
- console.log(red(` ${describeApiFailure(err, "couldn't load models")}\n`));
6863
+ console.log(red(` ${describeApiFailure(err, "couldn't load models")}\n`));
6851
6864
  return;
6852
6865
  }
6853
6866
  const allowed = Array.isArray(probe?.codingAllowed) ? probe.codingAllowed : [];
@@ -6868,7 +6881,7 @@ async function chooseModel() {
6868
6881
  // choices while still occupying its URL, so sending this branch straight to the ADD flow
6869
6882
  // would meet "already configured here" and dead-end. The manager fixes such a row, and falls
6870
6883
  // through to add when there is genuinely nothing there.
6871
- console.log(dim(" No coding model configured."));
6884
+ console.log(dim(" No coding model configured."));
6872
6885
  const go = await pickFromList(
6873
6886
  [dim("leave it"), "add or change a provider — Claude API, OpenAI, Kimi…", "change the Ollama server this machine uses…"],
6874
6887
  1
@@ -6886,7 +6899,7 @@ async function chooseModel() {
6886
6899
  // symptom of being pointed at the wrong box, and the old message sent people to a web app a
6887
6900
  // Client Expert machine does not have.
6888
6901
  if (choices.length === 1) {
6889
- console.log(dim(` Only one coding model is available: ${choices[0].model}.`));
6902
+ console.log(dim(` Only one coding model is available: ${choices[0].model}.`));
6890
6903
  // APPENDED AFTER the existing entry, here and below, so that entry keeps the index anyone
6891
6904
  // who has used it already reaches for.
6892
6905
  const go = await pickFromList(
@@ -6934,10 +6947,10 @@ async function chooseModel() {
6934
6947
  // enough in name to have already been mixed up. The fix is to say which one THIS is — not to
6935
6948
  // print the other two here: the agent model and the embedder are independent settings with
6936
6949
  // 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}):`));
6950
+ console.log(dim(` Choose the CODING model — the one that writes code (${PICK_HINT}):`));
6938
6951
  const idx = await pickFromList(labels, activeIdx >= 0 ? activeIdx : 0);
6939
6952
  if (idx < 0) {
6940
- console.log(dim(" (cancelled)\n"));
6953
+ console.log(dim(" (cancelled)\n"));
6941
6954
  return;
6942
6955
  }
6943
6956
  // The appended entries sit past the last model, so they are the only indices without a choice.
@@ -6967,7 +6980,7 @@ async function chooseModel() {
6967
6980
  const where = showTag && !activeCoderWhere ? ` (${tags[idx]})` : "";
6968
6981
  console.log(
6969
6982
  accent(
6970
- ` ✓ coding model → ${coderDisplayName(chosen.model, activeCoderWhere)}${where}${chosen.id === def ? " (default)" : ""}\n`
6983
+ ` ✓ coding model → ${coderDisplayName(chosen.model, activeCoderWhere)}${where}${chosen.id === def ? " (default)" : ""}\n`
6971
6984
  )
6972
6985
  );
6973
6986
  // DOES THIS ONE NEED AN API KEY? (task #181) Reported live on Windows 11 / Client Expert:
@@ -7020,7 +7033,7 @@ async function chooseModel() {
7020
7033
  host: ks.provider,
7021
7034
  });
7022
7035
  console.log(
7023
- dim(" API key ") + accent(line.text) +
7036
+ dim(" API key ") + accent(line.text) +
7024
7037
  (line.note ? orange(` · ${line.note}`) : "") +
7025
7038
  dim(` · ctrl+k to ${line.verb} it`)
7026
7039
  );
@@ -7029,7 +7042,7 @@ async function chooseModel() {
7029
7042
  // the more common one — "how do I change the model" — with no visible answer at all. Reported
7030
7043
  // three times from exactly this view, twice after being told where the picker entry was.
7031
7044
  // /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"));
7045
+ console.log(dim(" to change this provider's model or URL, or remove it: /model → add or change a provider\n"));
7033
7046
  }
7034
7047
  // Task #125: the GLOBAL lifetime total is per-backend, so re-fetch it for the newly
7035
7048
  // selected backend NOW — otherwise the footer would keep showing the previous backend's
@@ -7383,13 +7396,13 @@ async function runSwitchSkillCommand(line) {
7383
7396
  }
7384
7397
  } else {
7385
7398
  const options = ["claude", "gu"];
7386
- console.log(dim(` Whose skills, MCP servers and hooks this folder uses (${PICK_HINT}):`));
7399
+ console.log(dim(` Whose skills, MCP servers and hooks this folder uses (${PICK_HINT}):`));
7387
7400
  const idx = await pickFromList(
7388
7401
  options.map((o) => sourceLabel(o) + dim(` · ${where(o)}`) + (o === current ? dim(" · in use") : "")),
7389
7402
  Math.max(0, options.indexOf(current))
7390
7403
  );
7391
7404
  if (idx < 0) {
7392
- console.log(dim(" (cancelled)\n"));
7405
+ console.log(dim(" (cancelled)\n"));
7393
7406
  paintBar();
7394
7407
  return;
7395
7408
  }
@@ -7407,7 +7420,7 @@ async function runSwitchSkillCommand(line) {
7407
7420
  _skillScan = { at: 0, skills: [] }; // the "/" menu and Tab follow at once
7408
7421
  const say = (l) => console.log(GUTTER + l);
7409
7422
  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)}`));
7423
+ say(dim(` skills, MCP servers and hooks are read from ${where(pick)}`));
7411
7424
  // What was found there — and what was not, said rather than shown as an empty list.
7412
7425
  try {
7413
7426
  const sk = discoverSkills({ cwd: process.cwd(), reserved: COMMAND_NAMES });
@@ -7419,20 +7432,20 @@ async function runSwitchSkillCommand(line) {
7419
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")}`));
7420
7433
  // Nothing there at all is a normal state, not a fault: say so, and that gu needs none of it.
7421
7434
  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."));
7435
+ say(dim(" Nothing is set up there yet — gu works the same without any of them."));
7423
7436
  }
7424
7437
  for (const w of [...sk.warnings, ...mcp.warnings, ...hk.warnings]) say(yellow(" " + w));
7425
7438
  const unsupported = mcp.servers.filter((x) => x.error);
7426
7439
  if (unsupported.length) say(yellow(` ${count(unsupported.length, "MCP server")} cannot run here: ${unsupported.map((x) => `${x.name} (${x.error})`).join("; ")}`));
7427
7440
  const waiting = mcp.servers.filter((x) => x.scope === "project" && !x.approved && !x.error);
7428
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>`));
7429
- if (hk.projectPending) say(yellow(` this folder's ${hk.projectFile.replace(/^\.\//, "")} has hooks that will not run until you approve them — /hooks approve`));
7442
+ if (hk.projectPending) say(yellow(` this folder's ${hk.projectFile.replace(/^\.\//, "")} has hooks that will not run until you approve them — /hooks approve`));
7430
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`));
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."));
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."));
7432
7445
  } catch (err) {
7433
- say(yellow(` (could not read it back: ${err.message})`));
7446
+ say(yellow(` (could not read it back: ${err.message})`));
7434
7447
  }
7435
- say(dim(` It holds from your next question. ${pick === "claude" ? "/switch-skill gu" : "/switch-skill claude"} switches back.`));
7448
+ say(dim(` It holds from your next question. ${pick === "claude" ? "/switch-skill gu" : "/switch-skill claude"} switches back.`));
7436
7449
  console.log("");
7437
7450
  if (barPinned()) paintBar();
7438
7451
  }
@@ -7581,7 +7594,7 @@ async function runUsePresetsCommand(line) {
7581
7594
  console.log(GUTTER + dim("/save-preset pastes one in — copy it from another computer's /open-presets.") + "\n");
7582
7595
  return;
7583
7596
  }
7584
- console.log(dim(` Presets (${ed.PICKER_HINT}):`));
7597
+ console.log(dim(` Presets (${ed.PICKER_HINT}):`));
7585
7598
  const pick = await pickFromList(found, 0, { keys: ed.PICKER_KEYS });
7586
7599
  if (!pick || pick.index < 0) {
7587
7600
  console.log(GUTTER + dim("nothing changed.") + "\n");
@@ -7660,7 +7673,7 @@ async function runSaveSettingsCommand(line) {
7660
7673
  // have to type is a name — and Enter keeps gu-settings.json, as before.
7661
7674
  let typed = settingsArg(line, "/save-settings");
7662
7675
  if (!typed) {
7663
- const answer = await ask(` file name in ${home} [${st.DEFAULT_SETTINGS_NAME}]: `);
7676
+ const answer = await ask(` file name in ${home} [${st.DEFAULT_SETTINGS_NAME}]: `);
7664
7677
  if (answer === null) {
7665
7678
  console.log(GUTTER + dim(" (cancelled — nothing was written)\n"));
7666
7679
  return;
@@ -7720,7 +7733,7 @@ async function runLoadSettingsCommand(line) {
7720
7733
  if (!typed) {
7721
7734
  const found = st.settingsFilesIn(listJsonFiles(home));
7722
7735
  if (found.length > 1) {
7723
- console.log(dim(` Settings files in ${home} (${PICK_HINT}):`));
7736
+ console.log(dim(` Settings files in ${home} (${PICK_HINT}):`));
7724
7737
  const i = await pickFromList(found, 0);
7725
7738
  if (i < 0) {
7726
7739
  console.log(GUTTER + dim(" (cancelled — nothing changed)\n"));
@@ -8070,11 +8083,11 @@ async function runClearLogs() {
8070
8083
  const question = (q) => {
8071
8084
  const hint = `(${PICK_HINT}):`;
8072
8085
  console.log("");
8073
- if (2 + q.length + 2 + hint.length < width) console.log(dim(` ${q} ${hint}`));
8074
- else console.log(dim(` ${q}\n ${hint}`));
8086
+ if (2 + q.length + 2 + hint.length < width) console.log(dim(` ${q} ${hint}`));
8087
+ else console.log(dim(` ${q}\n ${hint}`));
8075
8088
  };
8076
8089
  const cancelled = () => {
8077
- console.log(dim(` (stopped — nothing ${freed.length ? "further " : ""}removed)\n`));
8090
+ console.log(dim(` (stopped — nothing ${freed.length ? "further " : ""}removed)\n`));
8078
8091
  paintBar();
8079
8092
  };
8080
8093
 
@@ -8175,7 +8188,7 @@ async function saveEmbedSettings(fields) {
8175
8188
  const saved = await withPendingStatus("saving your embedder", () =>
8176
8189
  md.saveWorkerSettings(apiBase, workerKey, fields)
8177
8190
  );
8178
- if (!saved) console.log(red(" couldn't save that to your account — nothing changed.\n"));
8191
+ if (!saved) console.log(red(" couldn't save that to your account — nothing changed.\n"));
8179
8192
  return saved;
8180
8193
  }
8181
8194
 
@@ -8193,7 +8206,7 @@ async function confirmEmbedSwitch(ec, current, chosen) {
8193
8206
  }
8194
8207
  const plan = ec.embedChangePlan({ current, chosen, indexes });
8195
8208
  if (!plan.changed) {
8196
- console.log(dim(` already using ${current} — nothing changed.\n`));
8209
+ console.log(dim(` already using ${current} — nothing changed.\n`));
8197
8210
  return { changed: false, plan: null };
8198
8211
  }
8199
8212
  // "Are you sure?" with nothing behind it is a prompt people learn to dismiss; the four projects
@@ -8201,7 +8214,7 @@ async function confirmEmbedSwitch(ec, current, chosen) {
8201
8214
  for (const l of ec.describeEmbedChange(plan)) console.log(GUTTER + dim(l));
8202
8215
  const go = await pickFromList([`switch to ${chosen}`, `keep ${current}`], plan.stale.length ? 1 : 0);
8203
8216
  if (go !== 0) {
8204
- console.log(dim(" (kept)\n"));
8217
+ console.log(dim(" (kept)\n"));
8205
8218
  return { changed: false, plan: null };
8206
8219
  }
8207
8220
  return { changed: true, plan };
@@ -8211,13 +8224,13 @@ async function confirmEmbedSwitch(ec, current, chosen) {
8211
8224
  // shared recognition step. Returns { changed, chosen, plan }.
8212
8225
  async function pickEmbedModel(ec, current) {
8213
8226
  const rows = ec.embedChoiceRows(current);
8214
- console.log(dim(` Choose the embedder (${PICK_HINT}):`));
8227
+ console.log(dim(` Choose the embedder (${PICK_HINT}):`));
8215
8228
  const labels = rows.map(
8216
8229
  (r) => `${r.model}${r.dims ? dim(` · ${r.dims}d`) : ""}${dim(` · ${r.note}`)}` + (r.current ? accent(" (current)") : "")
8217
8230
  );
8218
8231
  const idx = await pickFromList(labels, Math.max(0, rows.findIndex((r) => r.current)));
8219
8232
  if (idx < 0) {
8220
- console.log(dim(" (cancelled)\n"));
8233
+ console.log(dim(" (cancelled)\n"));
8221
8234
  return { changed: false, chosen: "", plan: null };
8222
8235
  }
8223
8236
  const chosen = rows[idx].model;
@@ -8231,21 +8244,21 @@ async function pickEmbedModel(ec, current) {
8231
8244
  // a model that was never pulled fails at the first search, not here. Say it once rather than let
8232
8245
  // it surface as "RAG is broken".
8233
8246
  function announceEmbedSaved({ chosen, plan, url }) {
8234
- console.log(accent(` ✓ embedding model → ${chosen}` + (url ? ` on ${hostOf(url)}` : "")));
8247
+ console.log(accent(` ✓ embedding model → ${chosen}` + (url ? ` on ${hostOf(url)}` : "")));
8235
8248
  if (plan?.stale.length) {
8236
8249
  console.log(
8237
8250
  dim(` ${plan.stale.length} knowledge base${plan.stale.length === 1 ? "" : "s"} ` +
8238
8251
  "will rebuild on the next search — the first one will be slower.")
8239
8252
  );
8240
8253
  }
8241
- console.log(dim(` Make sure it is pulled there: ollama pull ${chosen}\n`));
8254
+ console.log(dim(` Make sure it is pulled there: ollama pull ${chosen}\n`));
8242
8255
  }
8243
8256
 
8244
8257
  async function chooseEmbedModel() {
8245
8258
  const ec = await import("./embed-model-command.mjs");
8246
8259
  const settings = await withPendingStatus("reading your embedder", fetchWorkerSettings);
8247
8260
  if (!settings) {
8248
- console.log(red(" couldn't reach the API to read your current embedder.\n"));
8261
+ console.log(red(" couldn't reach the API to read your current embedder.\n"));
8249
8262
  return;
8250
8263
  }
8251
8264
  const current = ec.currentEmbedModel(settings);
@@ -8258,11 +8271,11 @@ async function chooseEmbedModel() {
8258
8271
  const keyRequired = isOllamaCloudUrl(currentUrl) || /\/v1\/?$/i.test(currentUrl) ||
8259
8272
  Boolean(String(settings.ragEmbedKeyId ?? "").trim());
8260
8273
  const menu = ec.embedMenuOptions({ current, host, keyRequired });
8261
- console.log(dim(` Embedding model (currently ${current} on ${host || currentUrl})`));
8274
+ console.log(dim(` Embedding model (currently ${current} on ${host || currentUrl})`));
8262
8275
  const mi = await pickFromList(menu.map((o) => o.label), 0);
8263
8276
  const action = mi >= 0 ? menu[mi].action : "cancel";
8264
8277
  if (action === "cancel") {
8265
- console.log(dim(" (cancelled)\n"));
8278
+ console.log(dim(" (cancelled)\n"));
8266
8279
  return;
8267
8280
  }
8268
8281
 
@@ -8303,12 +8316,12 @@ async function saveAgentModel(agentUrl, model, host) {
8303
8316
  md.saveWorkerSettings(apiBase, workerKey, { agentModelUrl: agentUrl, agentModelName: model, agentModel: model })
8304
8317
  );
8305
8318
  if (!ok) {
8306
- console.log(red(` couldn't save the agent model — nothing changed.`));
8319
+ console.log(red(` couldn't save the agent model — nothing changed.`));
8307
8320
  console.log(GUTTER + dim("if this machine's local API is old, restart it with `gu api restart`.\n"));
8308
8321
  return false;
8309
8322
  }
8310
- console.log(accent(` ✓ agent model → ${model}`) + dim(` · ${host}`));
8311
- console.log(dim(" takes effect on your next question.\n"));
8323
+ console.log(accent(` ✓ agent model → ${model}`) + dim(` · ${host}`));
8324
+ console.log(dim(" takes effect on your next question.\n"));
8312
8325
  return true;
8313
8326
  }
8314
8327
 
@@ -8321,7 +8334,7 @@ async function askKeyName(prompt = " key name: ") {
8321
8334
  for (let attempt = 0; attempt < 2; attempt++) {
8322
8335
  const name = (await ask(prompt)).trim();
8323
8336
  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."));
8337
+ console.log(yellow(" that looks like the API key itself — the NAME is shown on screen, so it must not be the key."));
8325
8338
  console.log(GUTTER + dim('type a short label like "work", or press Enter for none — the key comes next.'));
8326
8339
  }
8327
8340
  return "";
@@ -8342,7 +8355,7 @@ async function askServerKey({ host = "", nameHint = '"work", "personal"', option
8342
8355
  console.log("\n" + GUTTER + dim(`A name just tells keys apart — ${nameHint}.`));
8343
8356
  const name = (await askKeyName()).trim();
8344
8357
  console.log(GUTTER + dim(SECRET_KEY_HINT));
8345
- const secret = (await askSecret(` API key${host ? ` for ${host}` : ""}${name ? ` (${name})` : ""}: `)).trim();
8358
+ const secret = (await askSecret(` API key${host ? ` for ${host}` : ""}${name ? ` (${name})` : ""}: `)).trim();
8346
8359
  if (!secret) {
8347
8360
  console.log(GUTTER + dim(optional ? "(no key — the server will be called anonymously)" : "(cancelled — nothing was saved)\n"));
8348
8361
  return { name, secret: "" };
@@ -8377,15 +8390,15 @@ async function chooseAgentProvider() {
8377
8390
  const am = await import("./agent-model-command.mjs");
8378
8391
  const settings = await withPendingStatus("reading your providers", fetchWorkerSettings);
8379
8392
  if (!settings) {
8380
- console.log(red(" couldn't reach the API to read your providers.\n"));
8393
+ console.log(red(" couldn't reach the API to read your providers.\n"));
8381
8394
  return;
8382
8395
  }
8383
8396
  const choices = am.agentProviderChoices(settings);
8384
8397
  if (choices.length === 1) return addServer({ role: "agent", dialect: "openai" });
8385
- console.log(dim(` Which OpenAI-compatible provider should the agent use? (${PICK_HINT})`));
8398
+ console.log(dim(` Which OpenAI-compatible provider should the agent use? (${PICK_HINT})`));
8386
8399
  const idx = await pickFromList(choices.map((c) => c.label), 0);
8387
8400
  if (idx < 0) {
8388
- console.log(dim(" (cancelled)\n"));
8401
+ console.log(dim(" (cancelled)\n"));
8389
8402
  return;
8390
8403
  }
8391
8404
  const pick = choices[idx];
@@ -8407,7 +8420,7 @@ async function chooseAgentModel() {
8407
8420
  const am = await import("./agent-model-command.mjs");
8408
8421
  const settings = await withPendingStatus("reading your agent model", fetchWorkerSettings);
8409
8422
  if (!settings) {
8410
- console.log(red(" couldn't reach the API to read your current agent model.\n"));
8423
+ console.log(red(" couldn't reach the API to read your current agent model.\n"));
8411
8424
  return;
8412
8425
  }
8413
8426
  const current = am.currentAgentModel(settings);
@@ -8493,12 +8506,12 @@ async function chooseAgentModel() {
8493
8506
  // WHAT TO CHANGE, FIRST (#44). The ways to use an Ollama server or another provider used to sit
8494
8507
  // after every model on this one — 130 rows down on api.openai.com — which read as "OpenAI models
8495
8508
  // 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}`));
8509
+ console.log(dim(` Agent model — the one that TALKS: ${current || "unset"} on ${listHost}`));
8497
8510
  const menu = am.agentMenuOptions({ host: listHost, modelCount: models.length, needsKey });
8498
8511
  const first = await pickFromList(menu.map((o) => o.label), 0);
8499
8512
  const firstAction = first >= 0 ? menu[first].action : "cancel";
8500
8513
  if (firstAction === "cancel") {
8501
- console.log(dim(" (cancelled)\n"));
8514
+ console.log(dim(" (cancelled)\n"));
8502
8515
  return;
8503
8516
  }
8504
8517
  if (firstAction === "ollama") return addServer({ role: "agent", dialect: "ollama" });
@@ -8512,7 +8525,7 @@ async function chooseAgentModel() {
8512
8525
  }
8513
8526
 
8514
8527
  const rows = am.agentModelRows(models, current);
8515
- console.log(dim(` Agent model — the one that TALKS (currently ${current || "unset"})`));
8528
+ console.log(dim(` Agent model — the one that TALKS (currently ${current || "unset"})`));
8516
8529
  // Strip whichever list endpoint was asked, so the header names the SERVER rather than the path.
8517
8530
  console.log(
8518
8531
  dim(` ${models.length} on ${url.replace(/\/api\/tags$/, "").replace(/\/models$/, "")} (${PICK_HINT}):`)
@@ -8541,12 +8554,12 @@ async function chooseAgentModel() {
8541
8554
  // where they can be seen, rather than after every model.
8542
8555
  const idx = await pickFromList(labels, Math.max(0, rows.findIndex((r) => r.current)));
8543
8556
  if (idx < 0) {
8544
- console.log(dim(" (cancelled)\n"));
8557
+ console.log(dim(" (cancelled)\n"));
8545
8558
  return;
8546
8559
  }
8547
8560
  const chosen = rows[idx].model;
8548
8561
  if (chosen === current) {
8549
- console.log(dim(` already using ${current} — nothing changed.\n`));
8562
+ console.log(dim(` already using ${current} — nothing changed.\n`));
8550
8563
  return;
8551
8564
  }
8552
8565
  if (rows[idx].embedder) {
@@ -8557,7 +8570,7 @@ async function chooseAgentModel() {
8557
8570
  console.log(GUTTER + yellow("produce nonsense rather than an error."));
8558
8571
  const go = await pickFromList([`use it anyway`, `pick something else`], 1);
8559
8572
  if (go !== 0) {
8560
- console.log(dim(" (kept)\n"));
8573
+ console.log(dim(" (kept)\n"));
8561
8574
  return;
8562
8575
  }
8563
8576
  }
@@ -8570,15 +8583,15 @@ async function chooseAgentModel() {
8570
8583
  agentModel: chosen,
8571
8584
  });
8572
8585
  if (!saved) {
8573
- console.log(red(` couldn't save that to your account — still using ${current || "unset"}.`));
8586
+ console.log(red(` couldn't save that to your account — still using ${current || "unset"}.`));
8574
8587
  // The likeliest cause by far, and it is invisible otherwise: this field only became writable
8575
8588
  // through the worker route in task #169, so an older local API silently ignores it.
8576
8589
  console.log(GUTTER + dim("if this machine's local API predates /agent-model, restart it with"));
8577
8590
  console.log(GUTTER + dim("`gu api restart` to pick up the current version.\n"));
8578
8591
  return;
8579
8592
  }
8580
- console.log(accent(` ✓ agent model → ${chosen}`));
8581
- console.log(dim(" takes effect on your next question.\n"));
8593
+ console.log(accent(` ✓ agent model → ${chosen}`));
8594
+ console.log(dim(" takes effect on your next question.\n"));
8582
8595
  }
8583
8596
 
8584
8597
  // Probe whether SSH KEY auth already works for a server (so we can tell the user to run
@@ -8603,7 +8616,7 @@ async function checkServerKeyAuth(s) {
8603
8616
  if (ok) console.log(dim(" ✓ key auth works — deploys will connect without a password."));
8604
8617
  else
8605
8618
  console.log(
8606
- yellow(" key auth not set up yet.") +
8619
+ yellow(" key auth not set up yet.") +
8607
8620
  dim(` Run once in your terminal: `) +
8608
8621
  `ssh-copy-id ${s.user}@${s.host}`
8609
8622
  );
@@ -8803,20 +8816,20 @@ async function runS3Command(line) {
8803
8816
  // and, for the secret, does not have to know it.
8804
8817
  const keep = (typed, was) => (String(typed ?? "").trim() ? String(typed).trim() : was);
8805
8818
  const location = keep(
8806
- await ask(` Bucket (ARN, s3://… or name)${current.location ? ` [${current.location}]` : ""}: `),
8819
+ await ask(` Bucket (ARN, s3://… or name)${current.location ? ` [${current.location}]` : ""}: `),
8807
8820
  current.location
8808
8821
  );
8809
8822
  const region = keep(
8810
- await ask(` Region [${current.region || "ap-southeast-1"}]: `),
8823
+ await ask(` Region [${current.region || "ap-southeast-1"}]: `),
8811
8824
  current.region || "ap-southeast-1"
8812
8825
  );
8813
8826
  const accessKeyId = keep(
8814
- await ask(` AWS access key id${current.accessKeyId ? ` [${s3.maskKeyId(current.accessKeyId)}]` : ""}: `),
8827
+ await ask(` AWS access key id${current.accessKeyId ? ` [${s3.maskKeyId(current.accessKeyId)}]` : ""}: `),
8815
8828
  current.accessKeyId
8816
8829
  );
8817
8830
  // askSecret, never ask: the echo is muted and the value never reaches the scrollback (#127).
8818
8831
  const typedSecret = String(
8819
- await askSecret(` AWS secret${current.secretConfigured ? " [Enter keeps the stored one]" : ""}:`)
8832
+ await askSecret(` AWS secret${current.secretConfigured ? " [Enter keeps the stored one]" : ""}:`)
8820
8833
  ).trim();
8821
8834
 
8822
8835
  if (!location || !accessKeyId || !(typedSecret || current.secretConfigured)) {
@@ -9013,16 +9026,16 @@ async function chooseServer() {
9013
9026
  );
9014
9027
  const data = await res.json().catch(() => ({}));
9015
9028
  if (!res.ok) {
9016
- console.log(red(` couldn't load servers: ${data?.error || `HTTP ${res.status}`}\n`));
9029
+ console.log(red(` couldn't load servers: ${data?.error || `HTTP ${res.status}`}\n`));
9017
9030
  return;
9018
9031
  }
9019
9032
  servers = Array.isArray(data?.servers) ? data.servers : [];
9020
9033
  } catch (err) {
9021
- console.log(red(` ${describeApiFailure(err, "couldn't load servers")}\n`));
9034
+ console.log(red(` ${describeApiFailure(err, "couldn't load servers")}\n`));
9022
9035
  return;
9023
9036
  }
9024
9037
  if (servers.length === 0) {
9025
- console.log(dim(" No deployment servers yet — add them in the web app → Servers.\n"));
9038
+ console.log(dim(" No deployment servers yet — add them in the web app → Servers.\n"));
9026
9039
  return;
9027
9040
  }
9028
9041
  // Arrow-key picker: ↑/↓ to move, Enter to choose (no number typing). The list ends with
@@ -9030,21 +9043,21 @@ async function chooseServer() {
9030
9043
  const labels = servers.map((s) => `${s.name} (${s.user}@${s.host})`);
9031
9044
  labels.push("none (clear selection)");
9032
9045
  const activeIdx = servers.findIndex((s) => s.host === selectedServer?.host);
9033
- console.log(dim(` Choose a deployment server (${PICK_HINT}):`));
9046
+ console.log(dim(` Choose a deployment server (${PICK_HINT}):`));
9034
9047
  const idx = await pickFromList(labels, activeIdx >= 0 ? activeIdx : 0);
9035
9048
  if (idx < 0) {
9036
- console.log(dim(" (cancelled)\n"));
9049
+ console.log(dim(" (cancelled)\n"));
9037
9050
  return;
9038
9051
  }
9039
9052
  if (idx === servers.length) {
9040
9053
  // the trailing "none" row
9041
9054
  selectedServer = null;
9042
- console.log(dim(" ✓ deployment server cleared.\n"));
9055
+ console.log(dim(" ✓ deployment server cleared.\n"));
9043
9056
  return;
9044
9057
  }
9045
9058
  const s = servers[idx];
9046
9059
  selectedServer = { name: s.name, host: s.host, user: s.user };
9047
- console.log(accent(` ✓ deploy target → ${s.name} (${s.user}@${s.host})`));
9060
+ console.log(accent(` ✓ deploy target → ${s.name} (${s.user}@${s.host})`));
9048
9061
  await checkServerKeyAuth(selectedServer);
9049
9062
  console.log("");
9050
9063
  }
@@ -9136,10 +9149,10 @@ async function showOtherAgents() {
9136
9149
  return;
9137
9150
  }
9138
9151
  console.log("");
9139
- console.log(dim(` Your other running agents — pick one to act on (${PICK_HINT}):`));
9152
+ console.log(dim(` Your other running agents — pick one to act on (${PICK_HINT}):`));
9140
9153
  const which = await pickFromList(items.map((i) => i.label), 0);
9141
9154
  if (which < 0) {
9142
- console.log(dim(" (cancelled)\n"));
9155
+ console.log(dim(" (cancelled)\n"));
9143
9156
  paintBar();
9144
9157
  return;
9145
9158
  }
@@ -9162,10 +9175,10 @@ async function showOtherAgents() {
9162
9175
  { id: "stop", label: "Stop it", note: "ends the run — its unsaved work is lost" },
9163
9176
  ];
9164
9177
  console.log("");
9165
- console.log(dim(` ${shortName} (${PICK_HINT}):`));
9178
+ console.log(dim(` ${shortName} (${PICK_HINT}):`));
9166
9179
  const act = await pickFromList(actions.map((a) => a.label + dim(` · ${a.note}`)), 0);
9167
9180
  if (act < 0) {
9168
- console.log(dim(" (cancelled)\n"));
9181
+ console.log(dim(" (cancelled)\n"));
9169
9182
  paintBar();
9170
9183
  return;
9171
9184
  }
@@ -9463,9 +9476,9 @@ async function promptNewCodingKey(backendId = "", provider = "") {
9463
9476
  const id = await addCodingKey(name, secret, backendId);
9464
9477
  console.log(
9465
9478
  id
9466
- ? accent(` ✓ added ${name || "Unknown"}`) +
9479
+ ? accent(` ✓ added ${name || "Unknown"}`) +
9467
9480
  (backendId && provider ? dim(` · for ${provider}`) : "") + "\n"
9468
- : red(" couldn't save the key — try again, or set it in web Settings → Agent\n")
9481
+ : red(" couldn't save the key — try again, or set it in web Settings → Agent\n")
9469
9482
  );
9470
9483
  return id;
9471
9484
  }
@@ -9558,9 +9571,9 @@ async function replaceCodingKey() {
9558
9571
  console.log(
9559
9572
  ok
9560
9573
  ? 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")
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")
9564
9577
  );
9565
9578
  paintBar();
9566
9579
  }
@@ -9590,7 +9603,7 @@ async function manageServerKeys({ role = "coding", backendId, host, model = "" }
9590
9603
  backendId ? fetchServerKeys(backendId) : fetchCodingKeys()
9591
9604
  );
9592
9605
  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`));
9606
+ console.log(red(` couldn't read the keys for ${host} — is this machine's API up to date? (\`gu api restart\`)\n`));
9594
9607
  paintBar();
9595
9608
  return "";
9596
9609
  }
@@ -9635,7 +9648,7 @@ async function manageServerKeys({ role = "coding", backendId, host, model = "" }
9635
9648
  const fresh = await fetchServerKeys(backendId);
9636
9649
  const added = fresh?.find((k) => k.id === id);
9637
9650
  if (added?.default) {
9638
- console.log(accent(` ✓ ${added.name} is ${host}'s default key\n`));
9651
+ console.log(accent(` ✓ ${added.name} is ${host}'s default key\n`));
9639
9652
  paintBar();
9640
9653
  return id;
9641
9654
  }
@@ -9647,7 +9660,7 @@ async function manageServerKeys({ role = "coding", backendId, host, model = "" }
9647
9660
  if (!key.backendId && backendId) {
9648
9661
  const ok = await withPendingStatus(`tying ${key.name} to ${host}`, () => bindKeyToServer(key.id, backendId));
9649
9662
  if (!ok) {
9650
- console.log(red(` couldn't tie ${key.name} to ${host} — nothing changed.\n`));
9663
+ console.log(red(` couldn't tie ${key.name} to ${host} — nothing changed.\n`));
9651
9664
  paintBar();
9652
9665
  return "";
9653
9666
  }
@@ -9666,10 +9679,10 @@ async function applyKeyPick({ role, backendId, host, key }) {
9666
9679
  let scope = "folder";
9667
9680
  if (!backendId) {
9668
9681
  await saveFolderKeyPick(role, key.id);
9669
- console.log(accent(` ✓ ${folderLabel()}'s ${roleLabel} → ${key.name}`) + dim(` ····${key.tail}\n`));
9682
+ console.log(accent(` ✓ ${folderLabel()}'s ${roleLabel} → ${key.name}`) + dim(` ····${key.tail}\n`));
9670
9683
  } else if (key.default) {
9671
9684
  await saveFolderKeyPick(role, "");
9672
- console.log(accent(` ✓ ${folderLabel()}'s ${roleLabel} → ${key.name}`) + dim(` ····${key.tail} · ${host}'s default\n`));
9685
+ console.log(accent(` ✓ ${folderLabel()}'s ${roleLabel} → ${key.name}`) + dim(` ····${key.tail} · ${host}'s default\n`));
9673
9686
  } else {
9674
9687
  const choices = sk.scopeChoices({ host, role });
9675
9688
  console.log("\n" + GUTTER + dim(`Use ${key.name} for…`));
@@ -9683,16 +9696,16 @@ async function applyKeyPick({ role, backendId, host, key }) {
9683
9696
  if (scope === "default") {
9684
9697
  const ok = await withPendingStatus(`making ${key.name} the default for ${host}`, () => setServerDefaultKey(backendId, key.id));
9685
9698
  if (!ok) {
9686
- console.log(red(" couldn't change the default — nothing changed.\n"));
9699
+ console.log(red(" couldn't change the default — nothing changed.\n"));
9687
9700
  paintBar();
9688
9701
  return "";
9689
9702
  }
9690
9703
  // This folder follows the default now, rather than pinning a copy of it.
9691
9704
  await saveFolderKeyPick(role, "");
9692
- console.log(accent(` ✓ ${key.name} is now ${host}'s default`) + dim(` ····${key.tail} · every folder without its own pick\n`));
9705
+ console.log(accent(` ✓ ${key.name} is now ${host}'s default`) + dim(` ····${key.tail} · every folder without its own pick\n`));
9693
9706
  } else {
9694
9707
  await saveFolderKeyPick(role, key.id);
9695
- console.log(accent(` ✓ ${folderLabel()}'s ${roleLabel} → ${key.name}`) + dim(` ····${key.tail} · this folder only\n`));
9708
+ console.log(accent(` ✓ ${folderLabel()}'s ${roleLabel} → ${key.name}`) + dim(` ····${key.tail} · this folder only\n`));
9696
9709
  }
9697
9710
  }
9698
9711
  if (role === "coding") {
@@ -9746,7 +9759,7 @@ async function deleteServerKeyFlow({ role, key, host, folderKeyId, lastOfServer
9746
9759
  }
9747
9760
  const out = await deleteCodingKey(key.id);
9748
9761
  if (!out.ok) {
9749
- console.log(red(" couldn't delete the key — try again, or remove it in web Settings → Agent\n"));
9762
+ console.log(red(" couldn't delete the key — try again, or remove it in web Settings → Agent\n"));
9750
9763
  paintBar();
9751
9764
  return;
9752
9765
  }
@@ -9755,7 +9768,7 @@ async function deleteServerKeyFlow({ role, key, host, folderKeyId, lastOfServer
9755
9768
  const mine = r === "agent" ? sessionAgentKeyId : r === "embed" ? sessionEmbedKeyId : sessionCodingKeyId;
9756
9769
  if (mine === key.id) await saveFolderKeyPick(r, "");
9757
9770
  }
9758
- console.log(accent(` ✓ deleted ${out.name || key.name}`) + dim(` ····${key.tail} · ${out.remaining} key(s) left\n`));
9771
+ console.log(accent(` ✓ deleted ${out.name || key.name}`) + dim(` ····${key.tail} · ${out.remaining} key(s) left\n`));
9759
9772
  paintBar();
9760
9773
  }
9761
9774
 
@@ -10023,7 +10036,7 @@ async function approvalPrompt(command) {
10023
10036
  ok = (await runS3Command("/s3")) === true;
10024
10037
  } catch (e) {
10025
10038
  if (!wasPromptCancelled(e)) throw e;
10026
- console.log(dim(" (cancelled — the file is saved on this computer)\n"));
10039
+ console.log(dim(" (cancelled — the file is saved on this computer)\n"));
10027
10040
  } finally {
10028
10041
  following = wasFollowing;
10029
10042
  }
@@ -12531,7 +12544,7 @@ async function main() {
12531
12544
  // meaningfully refuse.
12532
12545
  const steps = await import("./first-run-steps.mjs");
12533
12546
  const step = steps.stepById("daemon");
12534
- console.log(dim(` ${step.doing}…`));
12547
+ console.log(dim(` ${step.doing}…`));
12535
12548
  let started = false;
12536
12549
  let why = "";
12537
12550
  try {
@@ -12539,7 +12552,7 @@ async function main() {
12539
12552
  started = !!r.started;
12540
12553
  localDaemonRunning = started || localDaemonRunning;
12541
12554
  if (started) {
12542
- console.log(dim(` started background worker (pid ${r.pid}) · logs ${ctl.LOG_FILE}`));
12555
+ console.log(dim(` started background worker (pid ${r.pid}) · logs ${ctl.LOG_FILE}`));
12543
12556
  } else {
12544
12557
  why = r.reason || "the worker did not report a pid";
12545
12558
  }
@@ -12553,7 +12566,7 @@ async function main() {
12553
12566
  await recordStepOutcome("daemon", started, why);
12554
12567
  if (!started) {
12555
12568
  console.log(
12556
- yellow(" the background worker did not start") +
12569
+ yellow(" the background worker did not start") +
12557
12570
  dim(` — ${why}\n ${step.why}.\n Try \`gu start\` to see the error in full.\n`)
12558
12571
  );
12559
12572
  }
@@ -12906,7 +12919,7 @@ async function main() {
12906
12919
  } else {
12907
12920
  await saveSessionSkip(resolve(process.cwd()), "skipDepsPrompt", true);
12908
12921
  console.log(
12909
- dim(" Skipped — install later with `gu doctor`") +
12922
+ dim(" Skipped — install later with `gu doctor`") +
12910
12923
  dim(clientModeBanner ? " --client" : "") +
12911
12924
  dim("; I won't ask again in this folder.\n")
12912
12925
  );
@@ -12940,7 +12953,7 @@ async function main() {
12940
12953
  );
12941
12954
  if (skipSetup) {
12942
12955
  console.log(
12943
- dim(" Set-up skipped (remembered for this folder) — `gu doctor` runs it ") +
12956
+ dim(" Set-up skipped (remembered for this folder) — `gu doctor` runs it ") +
12944
12957
  dim("any time.\n")
12945
12958
  );
12946
12959
  }
@@ -12968,7 +12981,7 @@ async function main() {
12968
12981
  await ctl2.setClientMode(true);
12969
12982
  const st = await ctl2.daemonStatus();
12970
12983
  console.log(
12971
- dim(" Skipped — this machine won't SERVE models; the agent still runs here and\n") +
12984
+ dim(" Skipped — this machine won't SERVE models; the agent still runs here and\n") +
12972
12985
  dim(" calls your model machine over the network. Point the Agent model URL at\n") +
12973
12986
  dim(" that machine (not 127.0.0.1) — `gu status` shows what it resolves to.\n") +
12974
12987
  dim(" `gu doctor` sets models up here · `/host-mode` undoes this.\n")
@@ -13146,8 +13159,8 @@ async function main() {
13146
13159
  const started = await tryStartLocalApi();
13147
13160
  console.error(
13148
13161
  started
13149
- ? dim(" Started it — run `gu` again.\n")
13150
- : dim(" Start it with: gu-cli api start") +
13162
+ ? dim(" Started it — run `gu` again.\n")
13163
+ : dim(" Start it with: gu-cli api start") +
13151
13164
  dim("\n If that fails, check MongoDB is running and see ~/.gonext/local-api.log\n")
13152
13165
  );
13153
13166
  }
@@ -13596,7 +13609,7 @@ async function main() {
13596
13609
  const direct = line === "/rag-local" ? "local" : line === "/rag-cloud" ? "both" : null;
13597
13610
  let chosen = direct;
13598
13611
  if (!chosen) {
13599
- console.log(dim(` Where this folder's RAG knowledge base lives (${PICK_HINT}):`));
13612
+ console.log(dim(` Where this folder's RAG knowledge base lives (${PICK_HINT}):`));
13600
13613
  const idx = await pickFromList(
13601
13614
  RAG_STORAGE.map(
13602
13615
  (r) =>
@@ -13607,7 +13620,7 @@ async function main() {
13607
13620
  Math.max(0, RAG_STORAGE.findIndex((r) => r.id === sessionRagStorage))
13608
13621
  );
13609
13622
  if (idx < 0) {
13610
- console.log(dim(" (cancelled)\n"));
13623
+ console.log(dim(" (cancelled)\n"));
13611
13624
  continue;
13612
13625
  }
13613
13626
  chosen = RAG_STORAGE[idx].id;
@@ -13623,7 +13636,7 @@ async function main() {
13623
13636
  // switching to cloud leaves the local shards on disk unread, and switching to local
13624
13637
  // leaves the S3 ones untouched — either way the new store is built by the next index.
13625
13638
  console.log(
13626
- accent(` ✓ RAG storage → ${row.label}`) +
13639
+ accent(` ✓ RAG storage → ${row.label}`) +
13627
13640
  dim(`\n ${row.note}`) +
13628
13641
  dim("\n Existing shards are not moved — the new store fills as this folder is re-indexed.\n")
13629
13642
  );
@@ -13730,7 +13743,7 @@ async function main() {
13730
13743
  if (!wasPromptCancelled(e)) throw e;
13731
13744
  // The same words the pickers print, for the same event — a cancel should not read as one
13732
13745
  // thing when the answer was typed and another when it was arrowed to.
13733
- console.log(dim(" (cancelled)\n"));
13746
+ console.log(dim(" (cancelled)\n"));
13734
13747
  continue;
13735
13748
  }
13736
13749
  // SWITCH TO ANOTHER AGENT (task #211) — chosen with the mouse in showOtherAgents, delivered