@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/client-expert-run.mjs +274 -47
- package/client-expert-setup.mjs +56 -7
- package/gonext_agent_chat.py +14 -252
- package/gu-cli.mjs +16 -3
- package/gu-repl.mjs +48 -207
- package/mongo-native.mjs +27 -2
- package/package.json +1 -2
- package/terminal-layout.mjs +0 -59
- package/clear-logs.mjs +0 -352
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
|
|
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
|
-
|
|
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 (
|
|
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 =
|
|
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 && !(
|
|
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 && !(
|
|
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
|
-
|
|
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.
|
|
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",
|
package/terminal-layout.mjs
CHANGED
|
@@ -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
|
*
|