@tiens.nguyen/gu-cli 1.0.863 → 1.0.864
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/README.md +7 -3
- package/cli-token.mjs +28 -2
- package/client-expert-run.mjs +22 -7
- package/env-check.mjs +228 -0
- package/gu-cli.mjs +47 -10
- package/gu-repl.mjs +144 -8
- package/install-path.mjs +10 -6
- package/mongo-native.mjs +15 -0
- package/package.json +4 -1
- package/support-bundle.mjs +389 -0
- package/support-command.mjs +106 -0
- package/terminal-layout.mjs +41 -1
package/gu-repl.mjs
CHANGED
|
@@ -105,6 +105,8 @@ import {
|
|
|
105
105
|
physicalRows,
|
|
106
106
|
inputPhysicalRows,
|
|
107
107
|
buildFooterRows,
|
|
108
|
+
buildFooter,
|
|
109
|
+
SUPPORT_LINK_TEXT,
|
|
108
110
|
pasteHint,
|
|
109
111
|
hintRows as questionHintRows,
|
|
110
112
|
footerDrawSeq,
|
|
@@ -707,6 +709,33 @@ let openPresetsAtStart = false;
|
|
|
707
709
|
// #132 follow-up: a hosted provider picked in first-run setup; its key and model are asked once the terminal exists.
|
|
708
710
|
let addProviderAtStart = null;
|
|
709
711
|
|
|
712
|
+
// #220: a gu that STOPS ON AN ERROR says where help is — a start that failed (exit 1 before the
|
|
713
|
+
// prompt: no key, API unreachable, MongoDB down…) or a crash. One dim line, never during a session
|
|
714
|
+
// that started fine, where /support is in the command list.
|
|
715
|
+
let supportHintAtExit = true;
|
|
716
|
+
const SUPPORT_HINT = "Need help? Run `gu support` — it sends the support team gu's logs (secrets masked).";
|
|
717
|
+
process.on("exit", (code) => {
|
|
718
|
+
if (code !== 1 || !supportHintAtExit) return;
|
|
719
|
+
try {
|
|
720
|
+
process.stderr.write("\n" + dim(SUPPORT_HINT) + "\n");
|
|
721
|
+
} catch {
|
|
722
|
+
/* stderr gone: nothing to tell */
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
function crashed(e) {
|
|
726
|
+
try {
|
|
727
|
+
releaseScreenAtExit();
|
|
728
|
+
} catch {
|
|
729
|
+
/* the screen may not be ours yet */
|
|
730
|
+
}
|
|
731
|
+
console.error(e?.stack || String(e));
|
|
732
|
+
supportHintAtExit = true;
|
|
733
|
+
process.exit(1);
|
|
734
|
+
}
|
|
735
|
+
process.on("uncaughtException", crashed);
|
|
736
|
+
process.on("unhandledRejection", crashed);
|
|
737
|
+
|
|
738
|
+
|
|
710
739
|
if (!workerKey || !apiBase) {
|
|
711
740
|
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
712
741
|
try {
|
|
@@ -977,6 +1006,7 @@ const COMMANDS = [
|
|
|
977
1006
|
{ name: "/shells", desc: "long commands the agent left running in the background — status, runtime, command, output; stop one" },
|
|
978
1007
|
{ name: "/budget", desc: "short view of your API provider budgets and balances (Pro) — the same items as the web /providers page" },
|
|
979
1008
|
{ name: "/tokens", desc: "break down the last prompt sent to the coding model — how much of it was RAG" },
|
|
1009
|
+
{ name: "/support", desc: "send the gu support team a request with gu's logs attached (secrets masked) — shows what goes first, then asks" },
|
|
980
1010
|
{ 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" },
|
|
981
1011
|
{ 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" },
|
|
982
1012
|
// ONE name, /new (user, #278). /reset and /clear still work (HIDDEN_COMMANDS) for habit and
|
|
@@ -1573,6 +1603,12 @@ function applyClick({ row, col }) {
|
|
|
1573
1603
|
void runClearLogs();
|
|
1574
1604
|
return;
|
|
1575
1605
|
}
|
|
1606
|
+
// The footer's "? support" link (#220): the footer is the last rows of the screen, so count from the bottom.
|
|
1607
|
+
if (supportLinkTarget && !zonePanel && row === viewportRows() - supportLinkTarget.fromEnd + 1 &&
|
|
1608
|
+
col - 1 >= supportLinkTarget.from && col - 1 < supportLinkTarget.to) {
|
|
1609
|
+
void runSupport();
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1576
1612
|
if (agentsRowTarget && row === updateRowAbs &&
|
|
1577
1613
|
col - 1 >= agentsRowTarget.from && col - 1 < agentsRowTarget.to) {
|
|
1578
1614
|
void showOtherAgents(); // async now — it offers stop / switch-to (task #211)
|
|
@@ -2868,8 +2904,12 @@ const compactLockNote = () =>
|
|
|
2868
2904
|
at: lastPromptBreakdown?.compactedAt,
|
|
2869
2905
|
})
|
|
2870
2906
|
: "";
|
|
2871
|
-
|
|
2872
|
-
|
|
2907
|
+
// #220: where the footer's "? support" link is on screen (set by frozenZone, read by applyClick).
|
|
2908
|
+
let supportLinkTarget = null;
|
|
2909
|
+
const footerBuilt = () =>
|
|
2910
|
+
buildFooter({
|
|
2911
|
+
link: SUPPORT_LINK_TEXT,
|
|
2912
|
+
linkPaint: (t) => dim("\x1b[4m" + t + "\x1b[24m"),
|
|
2873
2913
|
width: process.stdout.columns || 80,
|
|
2874
2914
|
gutter: GUTTER,
|
|
2875
2915
|
rag: REPL_VERBOSE && (lastRagFile || lastRagState) ? ragLineText() : "",
|
|
@@ -2895,6 +2935,7 @@ const footerRows = () =>
|
|
|
2895
2935
|
// where the red rows above mean something is not set up at all.
|
|
2896
2936
|
missing: [...missingNoticeRows().map((r) => red(r)), ...fallbackNoticeRows().map((r) => yellow(r))],
|
|
2897
2937
|
});
|
|
2938
|
+
const footerRows = () => footerBuilt().rows;
|
|
2898
2939
|
// An ask()'s hint lines (#305), drawn in yellow under its input while it waits. Plain text.
|
|
2899
2940
|
let askHint = [];
|
|
2900
2941
|
const askHintRows = (width) => questionHintRows(askHint, { width, gutter: GUTTER, paint: yellow });
|
|
@@ -3008,9 +3049,11 @@ function zoneNotice(width) {
|
|
|
3008
3049
|
const up = updateRow({ ...availableUpdates, width: 400 });
|
|
3009
3050
|
if (up) items.push({ key: "update", clickable: true, paint: accent, text: up.text.slice(up.from) });
|
|
3010
3051
|
}
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
if (
|
|
3052
|
+
// While the token question is open it is the ONLY thing on the screen (#222): the housekeeping
|
|
3053
|
+
// notices (new prices, new token limits) wait, and the token notice would repeat the question.
|
|
3054
|
+
if (priceUpdate && !tokenQuestionOpen) items.push({ key: "prices", clickable: true, paint: accent, text: `${priceUpdateText()} (click)` });
|
|
3055
|
+
if (limitsUpdate && !tokenQuestionOpen) items.push({ key: "limits", clickable: true, paint: accent, text: `${limitsUpdate.label} (click)` });
|
|
3056
|
+
if (tokenNotice?.text && !tokenQuestionOpen) items.push({ key: "token", paint: tokenNotice.urgent ? yellow : dim, text: tokenNotice.text });
|
|
3014
3057
|
const agents = agentsIndicator(otherAgents);
|
|
3015
3058
|
if (agents) items.push({ key: "agents", clickable: true, paint: accent, text: `${agents} running (click)` });
|
|
3016
3059
|
const shells = shellsNotice(shellEntries, { cwd: process.cwd() });
|
|
@@ -3125,7 +3168,10 @@ function frozenZone() {
|
|
|
3125
3168
|
selection: secretInput ? null : inputSelection,
|
|
3126
3169
|
highlight: highlightRow,
|
|
3127
3170
|
});
|
|
3128
|
-
const
|
|
3171
|
+
const footer = footerBuilt();
|
|
3172
|
+
const rows = status.concat(notice ? [notice.row] : [], zone.rows, askHintRows(width), footer.rows);
|
|
3173
|
+
// The footer is the LAST rows of the zone, so the link's row counts from the end.
|
|
3174
|
+
supportLinkTarget = footer.link ? { fromEnd: footer.rows.length - footer.link.rowIndex, from: footer.link.from, to: footer.link.to } : null;
|
|
3129
3175
|
// The separator is `status.length (+1 for the notice row)` rows down — everything that writes to
|
|
3130
3176
|
// or hit-tests a row by index counts from here.
|
|
3131
3177
|
const sep = status.length + (notice ? 1 : 0);
|
|
@@ -8587,6 +8633,83 @@ async function refreshLogsSize() {
|
|
|
8587
8633
|
}
|
|
8588
8634
|
}
|
|
8589
8635
|
|
|
8636
|
+
// /support (#220): gu's logs, masked and zipped, sent to the support team with a reference back.
|
|
8637
|
+
// What is collected and how it is sent is support-bundle.mjs; this is the terminal half only.
|
|
8638
|
+
let supportBusy = false;
|
|
8639
|
+
async function runSupport() {
|
|
8640
|
+
if (following) {
|
|
8641
|
+
console.log("\n" + GUTTER + dim("A turn is running - /support is available when it finishes (Ctrl-C stops it)."));
|
|
8642
|
+
return;
|
|
8643
|
+
}
|
|
8644
|
+
if (supportBusy || listPickerActive || approvalActive || askWaiting) return;
|
|
8645
|
+
supportBusy = true;
|
|
8646
|
+
try {
|
|
8647
|
+
const sb = await import("./support-bundle.mjs");
|
|
8648
|
+
const width = Math.max(20, process.stdout.columns || 80);
|
|
8649
|
+
const tilde = (p) => (p.startsWith(homedir()) ? "~" + p.slice(homedir().length) : p);
|
|
8650
|
+
const note = (l) => console.log(GUTTER + l);
|
|
8651
|
+
console.log("\n" + GUTTER + dim("Collecting gu's logs (secrets are masked)…"));
|
|
8652
|
+
const bundle = await sb.prepareSupportBundle({ version: REPL_VERSION });
|
|
8653
|
+
const where = tilde(bundle.file);
|
|
8654
|
+
const fallback = (opts = {}) => sb.supportFallbackLines({ file: where, ...opts });
|
|
8655
|
+
console.log("");
|
|
8656
|
+
note(bold("What would be sent") + dim(` · ${sb.bundleSize(bundle)} zip`));
|
|
8657
|
+
for (const row of sb.bundleSummaryLines(bundle)) note(" " + dim(row));
|
|
8658
|
+
note(dim("saved at ") + dim(where));
|
|
8659
|
+
console.log("");
|
|
8660
|
+
// The answers are typed in the input row, which empties after Enter: echo them so the
|
|
8661
|
+
// scrollback still shows what was sent.
|
|
8662
|
+
const typedMessage = (await ask("What went wrong? (Enter to skip): ")).trim();
|
|
8663
|
+
const message = sb.supportMessage(typedMessage);
|
|
8664
|
+
note(dim("what went wrong: ") + (typedMessage || dim("(not said)")));
|
|
8665
|
+
let email = await sb.defaultReplyEmail();
|
|
8666
|
+
const typed = (await ask(`Email for the answer${email ? ` [${email}]` : ""}: `)).trim();
|
|
8667
|
+
if (typed) email = typed;
|
|
8668
|
+
if (!sb.isEmail(email)) {
|
|
8669
|
+
note(yellow("No reply email — the support team could not answer you. Nothing sent."));
|
|
8670
|
+
for (const l of fallback()) note(dim(l));
|
|
8671
|
+
console.log("");
|
|
8672
|
+
return;
|
|
8673
|
+
}
|
|
8674
|
+
note(dim("answer to: ") + email);
|
|
8675
|
+
// A question never wraps: at 80 columns over SSH the picker hint goes on a row of its own.
|
|
8676
|
+
const q = `Send it to the gu support team (${sb.SUPPORT_EMAIL})?`;
|
|
8677
|
+
const hint = `(${PICK_HINT}):`;
|
|
8678
|
+
console.log("");
|
|
8679
|
+
if (3 + q.length + 2 + hint.length < width) console.log(dim(` ${q} ${hint}`));
|
|
8680
|
+
else console.log(dim(` ${q}\n ${hint}`));
|
|
8681
|
+
const go = await pickFromList(["Yes — send the request and the logs", "No — keep the zip, I will email it myself"], 0);
|
|
8682
|
+
if (go !== 0) {
|
|
8683
|
+
for (const l of fallback()) note(dim(l));
|
|
8684
|
+
console.log("");
|
|
8685
|
+
return;
|
|
8686
|
+
}
|
|
8687
|
+
note(dim("Sending…"));
|
|
8688
|
+
const r = await sb.sendSupportRequest({ base: sb.supportApiBase(), message, email, version: REPL_VERSION, zip: bundle.zip });
|
|
8689
|
+
if (!r.ok) {
|
|
8690
|
+
const [first, ...rest] = fallback({ why: r.why });
|
|
8691
|
+
note(yellow(first));
|
|
8692
|
+
for (const l of rest) note(l);
|
|
8693
|
+
console.log("");
|
|
8694
|
+
return;
|
|
8695
|
+
}
|
|
8696
|
+
note(accent(`✓ Sent — your reference is ${bold(r.ref)}`));
|
|
8697
|
+
note(dim(`the answer goes to ${email}`));
|
|
8698
|
+
if (r.uploaded) note(dim(`logs attached · to add more, email ${sb.SUPPORT_EMAIL} with ${r.ref}`));
|
|
8699
|
+
else {
|
|
8700
|
+
note(yellow(`The logs did not go with it (${r.uploadWhy}).`));
|
|
8701
|
+
for (const l of fallback({ ref: r.ref })) note(l);
|
|
8702
|
+
}
|
|
8703
|
+
console.log("");
|
|
8704
|
+
} catch (e) {
|
|
8705
|
+
if (wasPromptCancelled(e)) console.log(dim(" (cancelled — nothing sent)\n"));
|
|
8706
|
+
else console.log(GUTTER + red(`couldn't finish — ${e?.message ?? e}`) + "\n");
|
|
8707
|
+
} finally {
|
|
8708
|
+
supportBusy = false;
|
|
8709
|
+
if (barPinned()) paintBar();
|
|
8710
|
+
}
|
|
8711
|
+
}
|
|
8712
|
+
|
|
8590
8713
|
async function runClearLogs() {
|
|
8591
8714
|
// ONE AT A TIME, and never over a turn or another picker — reached from a fire-and-forget click
|
|
8592
8715
|
// (see showOtherAgents for what a second picker over the first one does).
|
|
@@ -11746,12 +11869,12 @@ async function localScaffoldState() {
|
|
|
11746
11869
|
/** Print the startup scaffold line — deferred while a turn owns the transcript. */
|
|
11747
11870
|
function announceScaffold() {
|
|
11748
11871
|
if (!scaffoldNotice || following) return;
|
|
11749
|
-
const { kind, text
|
|
11872
|
+
const { kind, text } = scaffoldNotice;
|
|
11750
11873
|
scaffoldNotice = null;
|
|
11751
11874
|
if (kind === "link") {
|
|
11752
11875
|
// A setup step, said calmly (#292): the headline, then the steps on their own lines.
|
|
11876
|
+
// ONE yellow line (#222): the steps are the token question's own, behind "?" there.
|
|
11753
11877
|
console.log(GUTTER + yellow("! " + text));
|
|
11754
|
-
for (const s of steps ?? []) console.log(GUTTER + " " + dim(s));
|
|
11755
11878
|
} else {
|
|
11756
11879
|
console.log(GUTTER + (kind === "ok" ? accent("✓ " + text) : kind === "warn" ? yellow("⚠ " + text) : red("✗ " + text)));
|
|
11757
11880
|
}
|
|
@@ -14225,6 +14348,15 @@ async function main() {
|
|
|
14225
14348
|
process.exit(1);
|
|
14226
14349
|
}
|
|
14227
14350
|
console.log(wrapGutter("Ask about this repo. Type / to see commands (Tab completes). Ctrl-C aborts a running turn.", dim) + "\n");
|
|
14351
|
+
// #221: gu runs, but its own npm folder is not on PATH (npm skipped the postinstall that adds it).
|
|
14352
|
+
try {
|
|
14353
|
+
const { pathNoticeLine } = await import("./env-check.mjs");
|
|
14354
|
+
const note = pathNoticeLine({ prefix: globalPrefixOf(DIR), pathEnv: process.env.PATH || "", ci: Boolean(process.env.CI) });
|
|
14355
|
+
if (note) console.log(wrapGutter(note, yellow) + "\n");
|
|
14356
|
+
} catch {
|
|
14357
|
+
/* a hint, never a reason to fail to start */
|
|
14358
|
+
}
|
|
14359
|
+
supportHintAtExit = false; // started: from here /support is in the command list
|
|
14228
14360
|
// Ask whether anything is newer — in the BACKGROUND, so a slow registry never delays the
|
|
14229
14361
|
// prompt (task #181). The answer paints itself onto the separator row when it arrives, and
|
|
14230
14362
|
// the interval keeps a long-lived session honest. unref'd: a pending timer must not be the
|
|
@@ -14793,6 +14925,10 @@ async function main() {
|
|
|
14793
14925
|
await runClearLogs();
|
|
14794
14926
|
continue;
|
|
14795
14927
|
}
|
|
14928
|
+
if (line === "/support") {
|
|
14929
|
+
await runSupport();
|
|
14930
|
+
continue;
|
|
14931
|
+
}
|
|
14796
14932
|
if (line === "/budget") {
|
|
14797
14933
|
// #205: the short terminal version of web-v2's /providers budgets. The server judges Pro,
|
|
14798
14934
|
// the account email and "has items" (api/src/providers/terminalBudget.ts).
|
package/install-path.mjs
CHANGED
|
@@ -30,6 +30,7 @@ import { appendFileSync, existsSync, readFileSync, chownSync, writeFileSync } fr
|
|
|
30
30
|
import { homedir, platform, tmpdir } from "node:os";
|
|
31
31
|
import { join, resolve } from "node:path";
|
|
32
32
|
import { fileURLToPath } from "node:url";
|
|
33
|
+
import { stableBinDir } from "./env-check.mjs";
|
|
33
34
|
|
|
34
35
|
const BEGIN = "# >>> gu-cli PATH >>>";
|
|
35
36
|
const END = "# <<< gu-cli PATH <<<";
|
|
@@ -121,12 +122,15 @@ function main() {
|
|
|
121
122
|
const binDir = join(prefix, "bin");
|
|
122
123
|
if (resolve(binDir).startsWith(resolve(tmpdir()))) return; // preflight's throwaway prefix
|
|
123
124
|
|
|
124
|
-
|
|
125
|
+
// On PATH under EITHER spelling: Homebrew's npm can report its prefix as the versioned Cellar
|
|
126
|
+
// folder while PATH holds the stable <brew prefix>/bin — the same folder (#221).
|
|
127
|
+
const stable = stableBinDir(binDir);
|
|
128
|
+
const onPath = (process.env.PATH || "").split(":").some((p) => p === binDir || p === stable);
|
|
125
129
|
if (onPath) return; // nothing to fix — most installs
|
|
126
130
|
|
|
127
131
|
const home = realUserHome();
|
|
128
132
|
const targets = rcFilesFor(home);
|
|
129
|
-
const block = `${BEGIN}\n# npm's global bin dir was not on PATH; added by @tiens.nguyen/gu-cli postinstall.\nexport PATH="${
|
|
133
|
+
const block = `${BEGIN}\n# npm's global bin dir was not on PATH; added by @tiens.nguyen/gu-cli postinstall.\nexport PATH="${stable}:$PATH"\n${END}\n`;
|
|
130
134
|
|
|
131
135
|
let wrote = null;
|
|
132
136
|
for (const rc of targets) {
|
|
@@ -134,12 +138,12 @@ function main() {
|
|
|
134
138
|
}
|
|
135
139
|
|
|
136
140
|
if (wrote) {
|
|
137
|
-
console.log(`gu-cli: added ${
|
|
141
|
+
console.log(`gu-cli: added ${stable} to PATH in ${wrote}.`);
|
|
138
142
|
console.log(`gu-cli: open a NEW terminal to use \`gu\`, or run this once in the current one:`);
|
|
139
|
-
console.log(` export PATH="${
|
|
143
|
+
console.log(` export PATH="${stable}:$PATH"`);
|
|
140
144
|
} else if (targets.length === 0) {
|
|
141
|
-
console.log(`gu-cli: ${
|
|
142
|
-
console.log(` export PATH="${
|
|
145
|
+
console.log(`gu-cli: ${stable} is not on your PATH and no shell profile was found to update.`);
|
|
146
|
+
console.log(` export PATH="${stable}:$PATH"`);
|
|
143
147
|
}
|
|
144
148
|
}
|
|
145
149
|
|
package/mongo-native.mjs
CHANGED
|
@@ -246,6 +246,21 @@ export function captureOutput(cmd, args, timeoutMs = 10000) {
|
|
|
246
246
|
});
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
+
/**
|
|
250
|
+
* Which of Homebrew's output lines are worth showing (#221). A MongoDB install prints ~100 lines
|
|
251
|
+
* (every library it pours, a mug per bottle) over several minutes; shown whole they bury the one
|
|
252
|
+
* that matters. Kept: the `==>` stage lines ("Installing mongodb-community", "Downloading…"), what
|
|
253
|
+
* gu itself prints (`$ brew …`), and anything that looks like a failure or warning.
|
|
254
|
+
*/
|
|
255
|
+
export function brewLineWorthShowing(line) {
|
|
256
|
+
const l = String(line ?? "").trim();
|
|
257
|
+
if (!l) return false;
|
|
258
|
+
if (l.startsWith("$ ")) return true;
|
|
259
|
+
if (/\b(error|fail(ed|ure)?|warning|cannot|denied|not found|conflict)\b/i.test(l)) return true;
|
|
260
|
+
if (l.startsWith("==>")) return !/^==> (Pouring|Fetching downloads? for)\b/.test(l);
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
|
|
249
264
|
/**
|
|
250
265
|
* Homebrew steps, one after another, output shown. → true when all succeeded. HOMEBREW_NO_AUTO_UPDATE
|
|
251
266
|
* keeps `brew install` from first updating every formula on the machine — minutes nobody asked for.
|
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.864",
|
|
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",
|
|
@@ -89,6 +89,9 @@
|
|
|
89
89
|
"node-version.mjs",
|
|
90
90
|
"clear-state.mjs",
|
|
91
91
|
"support-report.mjs",
|
|
92
|
+
"env-check.mjs",
|
|
93
|
+
"support-bundle.mjs",
|
|
94
|
+
"support-command.mjs",
|
|
92
95
|
"clear-logs.mjs",
|
|
93
96
|
"platform-tools.mjs",
|
|
94
97
|
"job-pools.mjs",
|