@tiens.nguyen/gu-cli 1.0.788 → 1.0.790

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
@@ -63,8 +63,6 @@ import {
63
63
  ragSegmentLabel,
64
64
  searchSegmentLabel,
65
65
  runningDetailRows,
66
- runningCommandLabel,
67
- runningCommandRows,
68
66
  formatThinkTime,
69
67
  plainAnswer,
70
68
  summarizeError,
@@ -130,7 +128,6 @@ import {
130
128
  } from "./terminal-layout.mjs";
131
129
  import { completedFlush, splitLiveStream } from "./completed-flush.mjs";
132
130
  import { isScaffoldStep, isGroupableStep, stepGroupRows, relativizePaths } from "./step-group.mjs";
133
- import { clearLogsNoticeText } from "./clear-logs.mjs";
134
131
  import { discoverSkills, matchSkillCommand } from "./skills.mjs";
135
132
  import { appendNew, archivePathFor, archivedWorkspaceKey, markReset, seedArchive } from "./conversation-archive.mjs";
136
133
  import { addAllowRule, listServers as listMcpServers } from "./mcp-config.mjs";
@@ -894,7 +891,6 @@ const COMMANDS = [
894
891
  { name: "/use-presets", usage: "/use-presets [name]", desc: "pick a saved preset and apply it — shows what changes, backs up your setup, asks first" },
895
892
  { name: "/doctor", desc: "check every configured model ANSWERS — especially the embedder, which a turn only reaches after several steps" },
896
893
  { name: "/tokens", desc: "break down the last prompt sent to the coding model — how much of it was RAG" },
897
- { name: "/clear-logs", desc: "show what gu's logs, RAG indexes and old API versions take on disk, then ask what to clear — the logs first" },
898
894
  { name: "/prices", usage: "/prices [update]", desc: "the price table and the cache table, and what the last turn and this session cost — \"update\" applies gu's newest published tables" },
899
895
  // ONE name, /new (user, #278). /reset and /clear still work (HIDDEN_COMMANDS) for habit and
900
896
  // for Claude Code users — not to be confused with `gu clear` in a shell, which wipes gu's state.
@@ -1466,11 +1462,6 @@ function applyClick({ row, col }) {
1466
1462
  void runPricesUpdate();
1467
1463
  return;
1468
1464
  }
1469
- if (logsRowTarget && row === updateRowAbs &&
1470
- col - 1 >= logsRowTarget.from && col - 1 < logsRowTarget.to) {
1471
- void runClearLogs();
1472
- return;
1473
- }
1474
1465
  if (agentsRowTarget && row === updateRowAbs &&
1475
1466
  col - 1 >= agentsRowTarget.from && col - 1 < agentsRowTarget.to) {
1476
1467
  void showOtherAgents(); // async now — it offers stop / switch-to (task #211)
@@ -2689,9 +2680,6 @@ function zoneNotice(width) {
2689
2680
  if (tokenNotice?.text) items.push({ key: "token", paint: tokenNotice.urgent ? yellow : dim, text: tokenNotice.text });
2690
2681
  const agents = agentsIndicator(otherAgents);
2691
2682
  if (agents) items.push({ key: "agents", clickable: true, paint: accent, text: `${agents} running (click)` });
2692
- // LAST, so it is the first to give way on a narrow window: housekeeping, never urgent (#329).
2693
- const logs = clearLogsNoticeText(logsBytes);
2694
- if (logs) items.push({ key: "logs", clickable: true, paint: dim, text: logs });
2695
2683
  return noticeRow(items, { width, gutter: GUTTER });
2696
2684
  }
2697
2685
 
@@ -2772,7 +2760,6 @@ function frozenZone() {
2772
2760
  updateRowTarget = notice?.spans.update ?? null;
2773
2761
  agentsRowTarget = notice?.spans.agents ?? null;
2774
2762
  pricesRowTarget = notice?.spans.prices ?? null;
2775
- logsRowTarget = notice?.spans.logs ?? null;
2776
2763
  jumpSpan = notice?.spans.jump ?? null;
2777
2764
  return { ...zone, rows, statusRows: sep, noticeRow: Boolean(notice) };
2778
2765
  }
@@ -2842,7 +2829,7 @@ function paintBar() {
2842
2829
  // THE NOTICE ROW is the one directly above the separator (#318) — never the separator itself.
2843
2830
  const noticeAbs = zone.noticeRow ? sepAbs - 1 : 0;
2844
2831
  jumpRowTarget = scrolledBack() && jumpSpan ? { row: noticeAbs, ...jumpSpan } : null;
2845
- updateRowAbs = updateRowTarget || agentsRowTarget || pricesRowTarget || logsRowTarget ? noticeAbs : 0;
2832
+ updateRowAbs = updateRowTarget || agentsRowTarget || pricesRowTarget ? noticeAbs : 0;
2846
2833
  // preserveCaret:false — the save slot holds the OUTPUT position (see regionUi), and the caret
2847
2834
  // is parked on the input line right after, so there is nothing to preserve here.
2848
2835
  ui(
@@ -3004,8 +2991,6 @@ let updateRowAbs = 0;
3004
2991
  let agentsRowTarget = null;
3005
2992
  // …and "new prices updated <date>" (#323).
3006
2993
  let pricesRowTarget = null;
3007
- // …and "clear logs · 175 MB" (#329), shown only once the logs are worth a row.
3008
- let logsRowTarget = null;
3009
2994
  // A terminal left with a scrolling region is a broken shell — a hard guarantee, like the
3010
2995
  // bracketed-paste and mouse-reporting teardowns.
3011
2996
  process.on("exit", () => {
@@ -7894,128 +7879,6 @@ async function runPricesUpdate() {
7894
7879
  }
7895
7880
  }
7896
7881
 
7897
- // ---------------------------------------------------------------------------------------------
7898
- // "CLEAR LOGS" (#329): a quiet item at the right of the notice row once gu's logs are worth a row,
7899
- // and /clear-logs for where there is no mouse. A click deletes nothing: it MEASURES — the logs,
7900
- // and beside them the two things that are actually big, the RAG indexes and the downloaded API
7901
- // versions — shows that with a total, then asks about each. What each "clear" may and may not
7902
- // touch is decided in clear-logs.mjs; this is the terminal half only.
7903
- // ---------------------------------------------------------------------------------------------
7904
- let logsBytes = 0; // what the notice shows; only the logs are measured for it (a handful of stats)
7905
- let clearLogsBusy = false;
7906
-
7907
- async function refreshLogsSize() {
7908
- try {
7909
- const { surveyLogs } = await import("./clear-logs.mjs");
7910
- const was = clearLogsNoticeText(logsBytes);
7911
- logsBytes = (await surveyLogs()).bytes;
7912
- if (clearLogsNoticeText(logsBytes) !== was && barPinned()) paintBar();
7913
- } catch {
7914
- /* a housekeeping notice is never worth an error */
7915
- }
7916
- }
7917
-
7918
- async function runClearLogs() {
7919
- // ONE AT A TIME, and never over a turn or another picker — reached from a fire-and-forget click
7920
- // (see showOtherAgents for what a second picker over the first one does).
7921
- if (clearLogsBusy || listPickerActive || approvalActive || following) return;
7922
- clearLogsBusy = true;
7923
- try {
7924
- const cl = await import("./clear-logs.mjs");
7925
- const { LOCAL_API_PORT } = await import("./client-expert-api.mjs");
7926
- const width = Math.max(20, process.stdout.columns || 80);
7927
- console.log("\n" + GUTTER + dim("Measuring ~/.gonext…"));
7928
- // Which API version is ANSWERING decides what "old" means — ask it, do not infer it.
7929
- const runningVersion = await fetch(`http://127.0.0.1:${LOCAL_API_PORT}/api/health`, { signal: AbortSignal.timeout(2000) })
7930
- .then((r) => r.json())
7931
- .then((j) => String(j?.version ?? ""))
7932
- .catch(() => "");
7933
- const survey = await cl.surveyDisk({ runningVersion });
7934
- console.log("");
7935
- for (const row of cl.summaryRows(survey, { width: width - GUTTER.length - 1 })) {
7936
- console.log(GUTTER + (row.startsWith("Total") ? bold(row) : row));
7937
- }
7938
- const freed = [];
7939
- // A question never wraps: at 80 columns over SSH the picker hint goes on a row of its own.
7940
- const question = (q) => {
7941
- const hint = `(${PICK_HINT}):`;
7942
- console.log("");
7943
- if (2 + q.length + 2 + hint.length < width) console.log(dim(` ${q} ${hint}`));
7944
- else console.log(dim(` ${q}\n ${hint}`));
7945
- };
7946
- const cancelled = () => {
7947
- console.log(dim(` (stopped — nothing ${freed.length ? "further " : ""}removed)\n`));
7948
- paintBar();
7949
- };
7950
-
7951
- // 1. THE LOGS — what was asked for, so Yes is the first row.
7952
- if (survey.logs.bytes > 0) {
7953
- question(`Clear the logs — ${cl.humanSize(survey.logs.bytes)}?`);
7954
- const go = await pickFromList(["Yes — clear the logs" + dim(" · the worker and the API keep running"), "No"], 0);
7955
- if (go < 0) return cancelled();
7956
- if (go === 0) {
7957
- const r = await cl.clearLogs(survey.logs);
7958
- freed.push({ what: "logs", ...r });
7959
- console.log(GUTTER + accent(`✓ logs cleared — ${cl.humanSize(r.freed)} freed`) +
7960
- (r.failed.length ? yellow(` (still in use, left alone: ${r.failed.join(", ")})`) : ""));
7961
- } else {
7962
- console.log(GUTTER + dim("logs kept"));
7963
- }
7964
- } else {
7965
- console.log("\n" + GUTTER + dim("No logs to clear."));
7966
- }
7967
-
7968
- // 2. RAG — costs a re-index, so No is the first row.
7969
- if (survey.rag.indexes.length) {
7970
- const gone = survey.rag.indexes.filter((i) => i.missing);
7971
- const goneBytes = gone.reduce((n, i) => n + i.bytes, 0);
7972
- const choices = [{ id: "no", label: "No — keep them" }];
7973
- if (gone.length && gone.length < survey.rag.indexes.length) {
7974
- choices.push({ id: "gone", label: `Only the ${gone.length} whose folder no longer exists — ${cl.humanSize(goneBytes)}` });
7975
- }
7976
- choices.push({ id: "all", label: `Yes — all ${survey.rag.indexes.length}` + dim(" · a folder is indexed again when it is next used") });
7977
- question(`Also clear the RAG indexes — ${cl.humanSize(survey.rag.bytes)}?`);
7978
- const pick = await pickFromList(choices.map((c) => c.label), 0);
7979
- if (pick < 0) return cancelled();
7980
- if (choices[pick].id !== "no") {
7981
- const r = await cl.clearRagIndexes(choices[pick].id === "gone" ? gone : survey.rag.indexes);
7982
- freed.push({ what: "rag", ...r });
7983
- console.log(GUTTER + accent(`✓ ${r.removed} RAG index${r.removed === 1 ? "" : "es"} cleared — ${cl.humanSize(r.freed)} freed`) +
7984
- (r.failed.length ? yellow(` (could not remove: ${r.failed.join(", ")})`) : ""));
7985
- } else {
7986
- console.log(GUTTER + dim("RAG indexes kept"));
7987
- }
7988
- }
7989
-
7990
- // 3. THE API — only ever the OLDER versions; the one in use is not on offer.
7991
- if (survey.api.old.length) {
7992
- const n = survey.api.old.length;
7993
- question(`Also remove the ${n} older API version${n === 1 ? "" : "s"} — ${cl.humanSize(survey.api.oldBytes)}?`);
7994
- const go = await pickFromList(["No — keep them", `Yes — remove the ${n} older one${n === 1 ? "" : "s"}` +
7995
- dim(survey.api.inUse ? ` · ${survey.api.inUse}, the one in use, stays` : "")], 0);
7996
- if (go < 0) return cancelled();
7997
- if (go === 1) {
7998
- console.log(GUTTER + dim("Removing…"));
7999
- const r = await cl.clearOldApiVersions(survey.api.old);
8000
- freed.push({ what: "api", ...r });
8001
- console.log(GUTTER + accent(`✓ ${r.removed} older API version${r.removed === 1 ? "" : "s"} removed — ${cl.humanSize(r.freed)} freed`) +
8002
- (r.failed.length ? yellow(` (could not remove: ${r.failed.join(", ")})`) : ""));
8003
- } else {
8004
- console.log(GUTTER + dim("older API versions kept"));
8005
- }
8006
- }
8007
-
8008
- const total = freed.reduce((n, f) => n + f.freed, 0);
8009
- console.log("\n" + GUTTER + (freed.length ? bold(`${cl.humanSize(total)} freed in all.`) : dim("Nothing removed.")) + "\n");
8010
- } catch (e) {
8011
- console.log(GUTTER + red(`couldn't finish — ${e?.message ?? e}`) + "\n");
8012
- } finally {
8013
- clearLogsBusy = false;
8014
- await refreshLogsSize();
8015
- if (barPinned()) paintBar();
8016
- }
8017
- }
8018
-
8019
7882
  /**
8020
7883
  * `/embed-model` — switch the RAG embedder, the embed server, or the embed key (task #169).
8021
7884
  *
@@ -9845,10 +9708,6 @@ const RAGSTATE_PREFIX = "__RAGSTATE__::";
9845
9708
  // The tool ABOUT to run: "<label>\t<call>" (task #177). Swallowed from scrollback like
9846
9709
  // "Running → …" — it belongs to the live block, and the finished bullet replaces it.
9847
9710
  const RUNTOOL_PREFIX = "__RUNTOOL__::";
9848
- // The command that is running, WHOLE (task #331): one line of JSON {cmd}, sent beside the
9849
- // 70-character "Running → …" line — and how many lines it has printed so far. Both swallowed.
9850
- const RUNCMD_PREFIX = "__RUNCMD__::";
9851
- const RUNOUT_PREFIX = "__RUNOUT__::";
9852
9711
  const MAXSTEP_PREFIX = "__MAXSTEP__::";
9853
9712
  const TIMEOUT_PREFIX = "__TIMEOUT__::";
9854
9713
  const COMPACT_PREFIX = "__COMPACT__::";
@@ -10983,12 +10842,6 @@ async function runAgentTurn(history, { attachJobId = "", ownJob = false } = {})
10983
10842
  let lastWordBucket = -1; // 12s window index → rotate the word once per window (task: 30-spinner)
10984
10843
  let almostDone = false; // set from the worker's "…almost completed thinking…" heartbeat
10985
10844
  let runningCmd = null; // the command currently executing (from a "Running → …" event)
10986
- // …the same command WHOLE, what it has printed so far, when it started, and whether the status
10987
- // row is hiding some of it (i.e. a click has something to open) — task #331.
10988
- let runningCmdFull = "";
10989
- let runningCmdLines = 0;
10990
- let runningCmdAt = 0;
10991
- let runningCmdMore = false;
10992
10845
  let runningTool = null; // { label, detail } for the tool executing right now (task #177)
10993
10846
  // The model's CURRENT Thought (task #64): while it streams its reasoning inside the
10994
10847
  // hidden ~~~ fence, show a one-clause summary on the blinking line so a 300s CPU-bound
@@ -11209,23 +11062,9 @@ async function runAgentTurn(history, { attachJobId = "", ownJob = false } = {})
11209
11062
  // to reveal). Line 1 (what's happening now): a live command > the model's current Thought >
11210
11063
  // "almost done" > "Thinking". When clickable we drive line 1 from the SAME fuller string, so
11211
11064
  // the grey expansion CONTINUES where line 1 was cut off instead of repeating the head.
11212
- // A RUNNING COMMAND is its own kind of line (#331): "$ <first line>… (17s · 5 lines)" on one
11213
- // row, and a click opens the whole command underneath instead of a thought's remainder.
11214
- const cmdView = runningCmd && !runningTool
11215
- ? runningCommandLabel(runningCmdFull || runningCmd, {
11216
- room: max,
11217
- secs: runningCmdAt ? Math.max(0, Math.round((now - runningCmdAt) / 1000)) : secs,
11218
- lines: runningCmdLines,
11219
- expanded: thoughtExpanded,
11220
- })
11221
- : null;
11222
- runningCmdMore = Boolean(cmdView?.more);
11223
- const clickable = cmdView ? cmdView.more : !!fullLiveThought && !runningCmd && fullLiveThought.length > max;
11065
+ const clickable = !!fullLiveThought && !runningCmd && fullLiveThought.length > max;
11224
11066
  let label, rest;
11225
- if (cmdView) {
11226
- label = cmdView.label;
11227
- rest = "";
11228
- } else if (clickable) {
11067
+ if (clickable) {
11229
11068
  // Split at a WORD boundary near the line-1 width so nothing is cut mid-word: the last
11230
11069
  // space at/before max-1 (unless that snaps back too far into the line — then hard-cut).
11231
11070
  // Line 1 shows the head; the grey expansion CONTINUES from the same point (no dup, no
@@ -11245,13 +11084,7 @@ async function runAgentTurn(history, { attachJobId = "", ownJob = false } = {})
11245
11084
  }
11246
11085
  // When clicked, unfold the remainder as up to 4 wrapped GREY lines BETWEEN the thought and
11247
11086
  // the playful-word line.
11248
- const expLines = cmdView
11249
- ? thoughtExpanded
11250
- ? runningCommandRows(runningCmdFull || runningCmd, {
11251
- width: Math.max(20, process.stdout.columns || 80) - 1 - GUTTER.length,
11252
- })
11253
- : []
11254
- : thoughtExpanded && rest ? wrapThought(rest, max, 4) : [];
11087
+ const expLines = thoughtExpanded && rest ? wrapThought(rest, max, 4) : [];
11255
11088
  // WHAT IS RUNNING, under the label (task #177). Capped at two rows and clipped, because
11256
11089
  // this block is redrawn by walking the caret over PHYSICAL rows — a wrapped row here is
11257
11090
  // the #109/#138 failure mode.
@@ -11382,14 +11215,14 @@ async function runAgentTurn(history, { attachJobId = "", ownJob = false } = {})
11382
11215
  if (row < top || row > top + statusContentLines - LEAD_ROWS - WORD_GAP - 2) return;
11383
11216
  // Only meaningful when there IS a fuller thought to reveal; otherwise ignore the click
11384
11217
  // so we don't flicker an empty expand.
11385
- if (!thoughtExpanded && !(runningCmd ? runningCmdMore : fullLiveThought)) return;
11218
+ if (!thoughtExpanded && !(fullLiveThought && !runningCmd)) return;
11386
11219
  toggleThought();
11387
11220
  };
11388
11221
  // Same expand/collapse, reachable without the mouse: Ctrl+T while a turn runs (see
11389
11222
  // toggleActiveThought). Ignored when there's nothing more to reveal, so it never flickers an
11390
11223
  // empty expansion.
11391
11224
  const toggleThought = () => {
11392
- if (!thoughtExpanded && !(runningCmd ? runningCmdMore : fullLiveThought)) return;
11225
+ if (!thoughtExpanded && !(fullLiveThought && !runningCmd)) return;
11393
11226
  thoughtExpanded = !thoughtExpanded;
11394
11227
  clearStatus(); // next tick (≤120ms) redraws at the new size; keeps cursor math correct
11395
11228
  };
@@ -11425,9 +11258,6 @@ async function runAgentTurn(history, { attachJobId = "", ownJob = false } = {})
11425
11258
  clearStatus();
11426
11259
  thinking = false;
11427
11260
  runningCmd = null;
11428
- runningCmdFull = "";
11429
- runningCmdLines = 0;
11430
- runningCmdAt = 0;
11431
11261
  runningTool = null;
11432
11262
  // A concrete action landed → the Thought that led to it is spent; clear it so the
11433
11263
  // next step's blinking line starts from "Thinking", not last step's stale clause.
@@ -11575,20 +11405,6 @@ async function runAgentTurn(history, { attachJobId = "", ownJob = false } = {})
11575
11405
  lastContentAt = Date.now();
11576
11406
  continue;
11577
11407
  }
11578
- if (line.startsWith(RUNCMD_PREFIX)) {
11579
- try {
11580
- runningCmdFull = String(JSON.parse(line.slice(RUNCMD_PREFIX.length))?.cmd ?? "");
11581
- } catch {
11582
- runningCmdFull = ""; // the short "Running → …" line still names it
11583
- }
11584
- lastContentAt = Date.now();
11585
- continue;
11586
- }
11587
- if (line.startsWith(RUNOUT_PREFIX)) {
11588
- runningCmdLines = Number(line.slice(RUNOUT_PREFIX.length).trim()) || 0;
11589
- lastContentAt = Date.now();
11590
- continue;
11591
- }
11592
11408
  if (line.startsWith(RUNTOOL_PREFIX)) {
11593
11409
  const [label, ...rest] = line.slice(RUNTOOL_PREFIX.length).split("\t");
11594
11410
  runningTool = { label: (label || "Working").trim(), detail: rest.join("\t").trim() };
@@ -11759,17 +11575,7 @@ async function runAgentTurn(history, { attachJobId = "", ownJob = false } = {})
11759
11575
  // So the pair collapses to: "◑ Running <cmd>…" while it runs, then "● <result>".
11760
11576
  if (!plainReplyFlow) {
11761
11577
  const mRun = /^Running → (.+)$/.exec(line.trim());
11762
- if (mRun) {
11763
- runningCmd = mRun[1];
11764
- // a NEW command: its own clock and count, and it starts as one row (#331)
11765
- runningCmdFull = "";
11766
- runningCmdLines = 0;
11767
- runningCmdAt = Date.now();
11768
- thoughtExpanded = false;
11769
- thinking = false;
11770
- lastContentAt = Date.now();
11771
- continue;
11772
- }
11578
+ if (mRun) { runningCmd = mRun[1]; thinking = false; lastContentAt = Date.now(); continue; }
11773
11579
  }
11774
11580
  // Skip the status-clear for a BUFFERED plain reply (STREAM_PLAIN_REPLY=false): nothing
11775
11581
  // prints yet, so the thinking ticker should keep animating until completion.
@@ -12271,6 +12077,29 @@ async function main() {
12271
12077
  // Asking an established machine to re-pick its mode on the first run after an upgrade
12272
12078
  // implies that choosing Host would do something. It would not. Reported live on an existing
12273
12079
  // machine that had been working for months.
12080
+ //
12081
+ // THE DATABASE FIRST, on a machine whose Local setup is finished (task #329). Every line below
12082
+ // talks to the API, and the API cannot answer without MongoDB — but its health check says "up"
12083
+ // regardless, so a missing mongod surfaced as two 15s timeouts and "the API is stuck". One
12084
+ // handshake here says what is actually wrong, and on a Mac offers to install or start it.
12085
+ // BEFORE the mode block on purpose: a database that had to be INSTALLED is empty, the steps
12086
+ // that filled it are re-opened, and the resume just below then runs them in this same start.
12087
+ if (isClientExpert()) {
12088
+ let mongoOk = true;
12089
+ try {
12090
+ const cer = await import("./client-expert-run.mjs");
12091
+ const { localMongoUri } = await import("./client-expert-api.mjs");
12092
+ const st0 = await cer.loadState();
12093
+ if (st0.mongo === "done") {
12094
+ const pre = await cer.preflightLocalMongo({ ask, isTty: IS_TTY, uri: localMongoUri() });
12095
+ mongoOk = pre.ok;
12096
+ if (pre.installed) await cer.saveState(cer.reopenAfterEmptyDatabase(st0));
12097
+ }
12098
+ } catch {
12099
+ /* a check, never a new way for gu to fail to start — the errors below still name the API */
12100
+ }
12101
+ if (!mongoOk) process.exit(1);
12102
+ }
12274
12103
  try {
12275
12104
  const { loadState, saveState, askMode } = await import("./client-expert-run.mjs");
12276
12105
  const { clientExpertAvailable, expertPlanPending, shouldAskMode, parseLicenseFlag, resolveFirstRunMode, promptForLicenseToken } =
@@ -12952,7 +12781,25 @@ async function main() {
12952
12781
  dim("\n Local mode keeps your conversations in MongoDB on this machine, and that") +
12953
12782
  dim("\n API is what reads it. It does not survive a reboot on its own.\n")
12954
12783
  );
12784
+ // ASK MONGODB ITSELF before telling the reader to (task #329). It can die AFTER the startup
12785
+ // check passed, and "is mongod running?" is a question this process answers in milliseconds.
12786
+ let mongoDown = "";
12955
12787
  if (hung) {
12788
+ try {
12789
+ const { localMongoDown } = await import("./client-expert-run.mjs");
12790
+ const { localMongoUri } = await import("./client-expert-api.mjs");
12791
+ mongoDown = await localMongoDown(localMongoUri());
12792
+ } catch {
12793
+ /* unknown — the general advice below still applies */
12794
+ }
12795
+ }
12796
+ if (mongoDown) {
12797
+ console.error(
12798
+ dim(` The API is running; MongoDB is not answering at ${mongoDown}, and the API waits on it.`) +
12799
+ dim("\n Start MongoDB (run `gu` again — it checks the database first and says how),") +
12800
+ dim("\n no API restart needed.\n")
12801
+ );
12802
+ } else if (hung) {
12956
12803
  // Do NOT try to start it — the port is taken by the process that is failing to answer.
12957
12804
  console.error(
12958
12805
  dim(" Something IS listening there but did not reply within ") +
@@ -12997,8 +12844,6 @@ async function main() {
12997
12844
  // #323: gu's newest published price + cache tables — same cadence, same silent failure.
12998
12845
  void refreshPriceUpdate();
12999
12846
  setInterval(() => void refreshPriceUpdate(), UPDATE_CHECK_MS).unref();
13000
- void refreshLogsSize();
13001
- setInterval(() => void refreshLogsSize(), UPDATE_CHECK_MS).unref();
13002
12847
  // The gu token (#232): same arrangement — background, never delays the prompt, unref'd.
13003
12848
  startupTokenRefresh = refreshCliToken();
13004
12849
  setInterval(() => void refreshCliToken(), TOKEN_TICK_MS).unref();
@@ -13459,10 +13304,6 @@ async function main() {
13459
13304
  await runPricesCommand(line);
13460
13305
  continue;
13461
13306
  }
13462
- if (line === "/clear-logs") {
13463
- await runClearLogs();
13464
- continue;
13465
- }
13466
13307
  if (line === "/tokens") {
13467
13308
  // Task #142: WHERE the last request's prompt went. The bar answers "how much is RAG";
13468
13309
  // this answers "and what displaced it", which is the part that tells you whether RAG is
package/mongo-native.mjs CHANGED
@@ -144,6 +144,28 @@ export const BREW_INSTALL_STEPS = [
144
144
  ];
145
145
  export const BREW_START_STEPS = [["services", "start", "mongodb/brew/mongodb-community"]];
146
146
 
147
+ /**
148
+ * HOMEBREW 7 REFUSES AN UNTRUSTED TAP (task #329, seen live 2026-10-02 on a fresh Homebrew 7.0.7):
149
+ * Error: Refusing to load formula mongodb/brew/mongodb-database-tools from untrusted tap
150
+ * mongodb/brew. Run … `brew trust mongodb/brew` to trust it.
151
+ * so tap → install, the steps that worked two days earlier, stopped at the install on exactly the
152
+ * machine a first install runs on. An older Homebrew has no `trust` command and must not be
153
+ * shown one, so the step is added only where the command exists.
154
+ */
155
+ export const BREW_TRUST_STEP = ["trust", "mongodb/brew"];
156
+ export function brewInstallSteps({ trust = false } = {}) {
157
+ return trust ? [BREW_INSTALL_STEPS[0], BREW_TRUST_STEP, ...BREW_INSTALL_STEPS.slice(1)] : BREW_INSTALL_STEPS;
158
+ }
159
+ /** Does this Homebrew have `brew trust`? (`brew trust --help` exits 0 only where it exists.) */
160
+ export async function brewHasTrust(brew, { run = runStreaming } = {}) {
161
+ if (!brew) return false;
162
+ return (await run(brew, ["trust", "--help"], { env: { ...process.env, HOMEBREW_NO_AUTO_UPDATE: "1" } })) === 0;
163
+ }
164
+
165
+ /** Homebrew's own one-line installer (https://brew.sh) — it asks for the password itself. */
166
+ export const HOMEBREW_INSTALL_COMMAND =
167
+ '/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"';
168
+
147
169
  /** Where Homebrew is, or "". Apple Silicon first, then Intel. */
148
170
  export function findBrew({ exists = existsSync } = {}) {
149
171
  for (const p of ["/opt/homebrew/bin/brew", "/usr/local/bin/brew"]) if (exists(p)) return p;
@@ -153,8 +175,11 @@ export function findBrew({ exists = existsSync } = {}) {
153
175
  /** The lines a Mac with no Homebrew is shown. */
154
176
  export function noBrewLines() {
155
177
  return [
156
- "MongoDB is installed with Homebrew on a Mac, and Homebrew is not here.",
157
- "Install Homebrew (https://brew.sh), then run gu again — setup installs MongoDB from there.",
178
+ "MongoDB is installed with Homebrew on a Mac, and Homebrew is not here. Install Homebrew first:",
179
+ ` ${HOMEBREW_INSTALL_COMMAND}`,
180
+ "then run gu again — it installs MongoDB from there with:",
181
+ // A Homebrew installed today is 7+, which needs the tap trusted.
182
+ ...brewInstallSteps({ trust: true }).map((s) => ` brew ${s.join(" ")}`),
158
183
  "Or install MongoDB Community yourself: https://www.mongodb.com/try/download/community",
159
184
  ];
160
185
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gu-cli",
3
- "version": "1.0.788",
3
+ "version": "1.0.790",
4
4
  "description": "Gu CLI — the gu terminal plus the local worker that runs agent / OCR / PDF / embedding jobs on your Mac (Ollama / MLX / OpenAI-compatible).",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -77,7 +77,6 @@
77
77
  "poll-errors.mjs",
78
78
  "node-version.mjs",
79
79
  "clear-state.mjs",
80
- "clear-logs.mjs",
81
80
  "platform-tools.mjs",
82
81
  "job-pools.mjs",
83
82
  "agent-stop.mjs",
@@ -2586,65 +2586,6 @@ export function runningDetailRows(detail, { width = 80, indent = 5, maxRows = 2
2586
2586
  ];
2587
2587
  }
2588
2588
 
2589
- /**
2590
- * THE COMMAND THAT IS RUNNING, on the live status row (task #331): "$ find . -type f … (17s · 5 lines)".
2591
- *
2592
- * It used to read "Running <first 70 characters>… (17s)" with every newline squashed to a space,
2593
- * so a heredoc was one run-on line and a long pipeline stopped mid-flag. Now: the shell's "$", the
2594
- * command's FIRST LINE (a heredoc's first line is what names it), and at the right what the user
2595
- * is waiting to learn — how long it has run and how much it has printed.
2596
- *
2597
- * ONE ROW, ALWAYS — the status block is redrawn in place and a taller block moves the input line.
2598
- * The rest of the command is a click (or Ctrl+T) away: see runningCommandRows. → { label, more }
2599
- * `more` = there is more of the command than this row shows, i.e. the click has something to open.
2600
- */
2601
- export function runningCommandLabel(command, { room = 66, secs = 0, lines = 0, expanded = false } = {}) {
2602
- const all = String(command ?? "").replace(/\r/g, "").split("\n").map((l) => l.replace(/\t/g, " ").replace(/\s+$/, "")).filter((l) => l.trim());
2603
- const n = Math.max(0, Number(lines) || 0);
2604
- const suffix = `(${Math.max(0, secs | 0)}s${n ? ` · ${n} line${n === 1 ? "" : "s"}` : ""})`;
2605
- const multi = all.length > 1;
2606
- // opened: the rows underneath ARE the command, so the row above them just says what they are
2607
- if (expanded) return { label: `Running command ${suffix}`, more: true };
2608
- const head = `$ ${(all[0] ?? "").trim()}`;
2609
- const space = Math.max(8, (room | 0) - suffix.length - 2);
2610
- const cut = head.length + (multi ? 2 : 0) > space;
2611
- const shown = cut ? head.slice(0, Math.max(1, space - 1)).replace(/\s+$/, "") + "…" : head + (multi ? " …" : "");
2612
- return { label: `${shown} ${suffix}`, more: cut || multi };
2613
- }
2614
-
2615
- /**
2616
- * The whole command, opened under the status row: "└ $ …" with a hanging indent, its own line
2617
- * breaks kept (a heredoc reads as the script it is), long lines wrapped at a space, and a cap —
2618
- * past it the last row ends in "…". Rows NEVER exceed `width`: the block is redrawn by walking
2619
- * physical rows, and a row that wraps on its own breaks every row under it (#109, #138).
2620
- */
2621
- export function runningCommandRows(command, { width = 76, maxRows = 5 } = {}) {
2622
- const lead = "└ $ ";
2623
- const pad = " ".repeat(lead.length);
2624
- const room = Math.max(12, (width | 0) - lead.length);
2625
- const out = [];
2626
- for (const raw of String(command ?? "").replace(/\r/g, "").split("\n")) {
2627
- let rest = raw.replace(/\t/g, " ").replace(/\s+$/, "");
2628
- if (!rest.trim()) continue;
2629
- while (rest.length > room) {
2630
- const sp = rest.lastIndexOf(" ", room);
2631
- // …but never break inside the leading indentation, or a wrapped line loses its shape
2632
- const cut = sp > room * 0.5 ? sp : room;
2633
- out.push(rest.slice(0, cut));
2634
- rest = rest.slice(cut).replace(/^ /, "");
2635
- }
2636
- out.push(rest);
2637
- }
2638
- if (!out.length) return [];
2639
- const cap = Math.max(1, maxRows | 0);
2640
- const rows = out.slice(0, cap);
2641
- if (out.length > cap) {
2642
- const last = rows[cap - 1];
2643
- rows[cap - 1] = (last.length >= room ? last.slice(0, room - 1) : last) + "…";
2644
- }
2645
- return rows.map((r, i) => (i ? pad : lead) + r);
2646
- }
2647
-
2648
2589
  /**
2649
2590
  * How long the turn took, for the line above the answer (task #178).
2650
2591
  *