@tiens.nguyen/gu-cli 1.0.857 → 1.0.859
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 +4 -1
- package/gu-cli.mjs +11 -0
- package/gu-repl.mjs +8 -8
- package/package.json +2 -1
- package/support-report.mjs +161 -0
package/client-expert-run.mjs
CHANGED
|
@@ -887,7 +887,10 @@ export async function downloadApiPackage(manifest, { onProgress = () => {} } = {
|
|
|
887
887
|
const tmpTar = join(staging, "package.tar.gz");
|
|
888
888
|
await writeFile(tmpTar, bytes);
|
|
889
889
|
onProgress(`unpacking ${bytes.length} bytes…`);
|
|
890
|
-
|
|
890
|
+
// RELATIVE NAMES, run from inside the staging dir. GNU tar (Git Bash / MSYS on Windows, often first
|
|
891
|
+
// on PATH) reads "C:\\x\\package.tar.gz" as host:file and tries to reach a machine called "C", which
|
|
892
|
+
// ends in "gzip: stdin: unexpected end of file". A path with no drive letter works for GNU and bsdtar.
|
|
893
|
+
await run("tar", ["xzf", "package.tar.gz", "-C", "."], { cwd: staging });
|
|
891
894
|
await rm(tmpTar, { force: true });
|
|
892
895
|
await rm(dir, { recursive: true, force: true });
|
|
893
896
|
await mkdir(join(homedir(), ".gonext", "api"), { recursive: true });
|
package/gu-cli.mjs
CHANGED
|
@@ -139,6 +139,7 @@ Usage:
|
|
|
139
139
|
gu-cli workspace revert [runId] # undo an agent run's file edits from its backups
|
|
140
140
|
gu-cli mongo migrate-from-docker # macOS: move a Docker MongoDB to native (Homebrew)
|
|
141
141
|
gu-cli clear [--scratch|--keep-history] [--yes] # back to a fresh machine (asks first)
|
|
142
|
+
gu-cli support # write a masked health report for the support team
|
|
142
143
|
gu-cli fix-path [--check] # put npm's global folder on PATH (Windows: check, then ask)
|
|
143
144
|
# (what the install does, if npm skipped it)
|
|
144
145
|
gu-cli fix-powershell [--check] # Windows: drop PowerShell's built-in \`gu\` alias
|
|
@@ -1235,6 +1236,16 @@ if (args[0] === "fix-powershell") {
|
|
|
1235
1236
|
process.exit(0);
|
|
1236
1237
|
}
|
|
1237
1238
|
|
|
1239
|
+
if (args[0] === "support") {
|
|
1240
|
+
// One redacted report for the support team (see support-report.mjs). Changes nothing but its own file.
|
|
1241
|
+
const sr = await import("./support-report.mjs");
|
|
1242
|
+
const { lines, file } = await sr.writeSupportReport({ version: WORKER_VERSION });
|
|
1243
|
+
for (const l of lines) console.log(l);
|
|
1244
|
+
console.log("");
|
|
1245
|
+
console.log(` Saved to ${file} — send that file (or this text) to the support team. Secrets are masked.`);
|
|
1246
|
+
process.exit(0);
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1238
1249
|
if (args[0] === "clear" || args[0] === "reset") {
|
|
1239
1250
|
const cs = await import("./clear-state.mjs");
|
|
1240
1251
|
const scratchOnly = args.includes("--scratch");
|
package/gu-repl.mjs
CHANGED
|
@@ -586,7 +586,7 @@ const argv = process.argv.slice(2);
|
|
|
586
586
|
// process.argv and exits inside its own subcommand branch, before it ever starts polling.
|
|
587
587
|
const CLI_SUBCOMMANDS = [
|
|
588
588
|
"status", "start", "stop", "restart", "logs", "doctor", "login", "set", "rag",
|
|
589
|
-
"clear", "reset", "api", "token-usage", "tokens", "token", "full-log", "mcp", "hooks",
|
|
589
|
+
"clear", "reset", "api", "token-usage", "tokens", "token", "full-log", "mcp", "hooks", "support",
|
|
590
590
|
];
|
|
591
591
|
// `gu logz` USED TO START THE REPL (task #181). Silently: an unrecognised word was not in
|
|
592
592
|
// the list, so it fell through to "no subcommand given" and a full agent session opened in that
|
|
@@ -3694,7 +3694,7 @@ const paintThoughtRows = (block, hovered, collapsed = block.collapsed) => {
|
|
|
3694
3694
|
if (block.kind === "command") {
|
|
3695
3695
|
// #204: the Allow prompt's command — wrapped under its own indent, long ones collapsed.
|
|
3696
3696
|
const paint = (row) => (hovered ? hoverThought(row) : dim(row));
|
|
3697
|
-
return commandBlockRows(block.text, { width: commandWidth(), collapsed, maxRows: COMMAND_COLLAPSED_ROWS }).map(paint);
|
|
3697
|
+
return commandBlockRows(block.text, { width: commandWidth(), indent: GUTTER, collapsed, maxRows: COMMAND_COLLAPSED_ROWS }).map(paint);
|
|
3698
3698
|
}
|
|
3699
3699
|
if (block.kind === "steps") {
|
|
3700
3700
|
// Activity (look-ups, finished commands): dim, like a ✓ row; bright while hovered.
|
|
@@ -10733,7 +10733,7 @@ function printApprovalCommand(shown, { toggle = true } = {}) {
|
|
|
10733
10733
|
const canToggle = toggle && barPinned();
|
|
10734
10734
|
const block = { kind: "command", from: 0, to: 0, text: shown, collapsed: canToggle };
|
|
10735
10735
|
const painted = paintThoughtRows(block, false);
|
|
10736
|
-
process.stdout.write("\n" + yellow("⚠ Allow running this command?") + "\n" + painted.join("\n") + "\n");
|
|
10736
|
+
process.stdout.write("\n" + markLine(yellow("⚠")) + yellow("Allow running this command?") + "\n" + painted.join("\n") + "\n");
|
|
10737
10737
|
if (!canToggle || !blockCanToggle(block)) return null;
|
|
10738
10738
|
block.to = transcript.dropped() + transcript.length() - 2;
|
|
10739
10739
|
block.from = block.to - painted.length + 1;
|
|
@@ -10870,7 +10870,7 @@ async function approvalPrompt(command) {
|
|
|
10870
10870
|
if (!process.stdin.isTTY) return { allow: false, dontAsk: false };
|
|
10871
10871
|
process.stdout.write(
|
|
10872
10872
|
"\n" + yellow("🌐 Allow gu to use a web browser?") + dim(" (no answer in ~3 min = Yes, this time)") + "\n" +
|
|
10873
|
-
indentBlock(`It wants to: ${what || "open a page"}`, { paint: dim }) + "\n" +
|
|
10873
|
+
indentBlock(`It wants to: ${what || "open a page"}`, { indent: GUTTER, paint: dim }) + "\n" +
|
|
10874
10874
|
dim(" In your Chrome if the gu extension is there, else in gu's own Chrome window.") + "\n"
|
|
10875
10875
|
);
|
|
10876
10876
|
// "Yes, this time" is THE default (#380, GitHub #167): highlighted, so Enter allows it once, and
|
|
@@ -10891,8 +10891,8 @@ async function approvalPrompt(command) {
|
|
|
10891
10891
|
const [server = "", tool = "", preview = ""] = command.slice(MCPTOOL_PREFIX.length).split("\t");
|
|
10892
10892
|
if (!process.stdin.isTTY) return { allow: false, dontAsk: false };
|
|
10893
10893
|
process.stdout.write(
|
|
10894
|
-
"\n" + yellow(
|
|
10895
|
-
(preview ? indentBlock(preview, { paint: dim }) + "\n" : "")
|
|
10894
|
+
"\n" + markLine(yellow("⚙")) + yellow(`Allow the MCP tool ${server}/${tool}?`) + dim(" (no answer in ~3 min = No)") + "\n" +
|
|
10895
|
+
(preview ? indentBlock(preview, { indent: GUTTER, paint: dim }) + "\n" : "")
|
|
10896
10896
|
);
|
|
10897
10897
|
const idx = await pickFromList(
|
|
10898
10898
|
["Yes", `Yes, and always allow ${server}/${tool}`, `Yes, and always allow every ${server} tool`, "No"],
|
|
@@ -10937,7 +10937,7 @@ async function approvalPrompt(command) {
|
|
|
10937
10937
|
// "yes" must never cover those.
|
|
10938
10938
|
if (process.stdin.isTTY && editMode === "manual" && why.startsWith("accept edits is off")) {
|
|
10939
10939
|
printApprovalCommand(shown, { toggle: false }); // a list picker follows — shown whole, wrapped (#204)
|
|
10940
|
-
process.stdout.write(indentBlock(why, { paint: dim }) + "\n");
|
|
10940
|
+
process.stdout.write(indentBlock(why, { indent: GUTTER, paint: dim }) + "\n");
|
|
10941
10941
|
const idx = await pickFromList(
|
|
10942
10942
|
["Yes", "Yes for all later ones (turns accept edits on)", "No"],
|
|
10943
10943
|
0,
|
|
@@ -10956,7 +10956,7 @@ async function approvalPrompt(command) {
|
|
|
10956
10956
|
return;
|
|
10957
10957
|
}
|
|
10958
10958
|
approvalCommandBlock = printApprovalCommand(shown);
|
|
10959
|
-
if (why) process.stdout.write(indentBlock(why, { paint: dim }) + "\n");
|
|
10959
|
+
if (why) process.stdout.write(indentBlock(why, { indent: GUTTER, paint: dim }) + "\n");
|
|
10960
10960
|
approvalSel = 0;
|
|
10961
10961
|
approvalResolve = resolve;
|
|
10962
10962
|
approvalActive = true;
|
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.859",
|
|
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",
|
|
@@ -88,6 +88,7 @@
|
|
|
88
88
|
"poll-errors.mjs",
|
|
89
89
|
"node-version.mjs",
|
|
90
90
|
"clear-state.mjs",
|
|
91
|
+
"support-report.mjs",
|
|
91
92
|
"clear-logs.mjs",
|
|
92
93
|
"platform-tools.mjs",
|
|
93
94
|
"job-pools.mjs",
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gu support` — one redacted report a customer can send to the support team.
|
|
3
|
+
*
|
|
4
|
+
* Written after a fresh Ubuntu install got no free token and no clue why (#17): the cause (an empty
|
|
5
|
+
* /etc/machine-id, so no trial request was ever made) was invisible from the customer's side. This
|
|
6
|
+
* collects what support would otherwise ask for one question at a time: version and OS, the machine
|
|
7
|
+
* id (present or not, NEVER its value), whether the gu service answers, the token state, the local
|
|
8
|
+
* API and MongoDB, and the tail of the logs.
|
|
9
|
+
*
|
|
10
|
+
* NOTHING SECRET LEAVES: tokens, worker keys, passwords in URLs and key=value secrets are masked
|
|
11
|
+
* before anything is printed or written. It does not claim a trial or change any file but its own report.
|
|
12
|
+
* Everything with a side effect on the outside is injectable so the tests run offline.
|
|
13
|
+
*/
|
|
14
|
+
import { readFile, readdir, stat, writeFile, mkdir } from "node:fs/promises";
|
|
15
|
+
import { homedir, platform as osPlatform, release, arch } from "node:os";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { redactSecrets } from "./proxy-config.mjs";
|
|
18
|
+
|
|
19
|
+
export const SUPPORT_REPORT_FILE = "support-report.txt";
|
|
20
|
+
export const SUPPORT_LOGS = ["worker.log", "local-api.log", "model.log", "embed.log"];
|
|
21
|
+
|
|
22
|
+
/** Mask tokens, keys, passwords and URL credentials in any text. */
|
|
23
|
+
export function redactReport(text) {
|
|
24
|
+
return redactSecrets(String(text ?? ""))
|
|
25
|
+
.replace(/\bgu1\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, "gu1.***")
|
|
26
|
+
.replace(/\b(sk|pk|ghp|gho|xox[abp])[-_][A-Za-z0-9_-]{12,}/g, "$1-***")
|
|
27
|
+
.replace(/(["']?[A-Za-z_]*(?:key|token|secret|password|passwd)[A-Za-z_]*["']?\s*[:=]\s*["']?)([^\s"',}]{6,})/gi, "$1***")
|
|
28
|
+
.replace(/(Authorization:\s*Bearer\s+)\S+/gi, "$1***");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const lastLines = (text, n) => String(text ?? "").split(/\r?\n/).filter((l, i, a) => l || i < a.length - 1).slice(-n);
|
|
32
|
+
|
|
33
|
+
async function fileInfo(path) {
|
|
34
|
+
try {
|
|
35
|
+
const s = await stat(path);
|
|
36
|
+
return { size: s.size, mtime: s.mtime.toISOString() };
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** → the report as an array of lines (already redacted). Never throws. */
|
|
43
|
+
export async function buildSupportReport({
|
|
44
|
+
home = homedir(),
|
|
45
|
+
version = "unknown",
|
|
46
|
+
platform = osPlatform(),
|
|
47
|
+
env = process.env,
|
|
48
|
+
now = new Date(),
|
|
49
|
+
logLines = 40,
|
|
50
|
+
fetchImpl = fetch,
|
|
51
|
+
token = null, // the cli-token module, injectable
|
|
52
|
+
api = null, // the client-expert-api module, injectable
|
|
53
|
+
run = null, // the client-expert-run module, injectable
|
|
54
|
+
} = {}) {
|
|
55
|
+
const dir = join(home, ".gonext");
|
|
56
|
+
const out = [];
|
|
57
|
+
const section = (t) => out.push("", `== ${t} ==`);
|
|
58
|
+
const line = (l) => out.push(l);
|
|
59
|
+
const safe = async (label, fn) => {
|
|
60
|
+
try {
|
|
61
|
+
await fn();
|
|
62
|
+
} catch (e) {
|
|
63
|
+
line(`${label}: could not check (${e?.message || e})`);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
line(`gu support report — ${now.toISOString()}`);
|
|
68
|
+
line("Secrets are masked. Send this file to the support team.");
|
|
69
|
+
|
|
70
|
+
section("This computer");
|
|
71
|
+
line(`gu ${version} node ${process.version}`);
|
|
72
|
+
line(`os ${platform} ${release()} ${arch()}`);
|
|
73
|
+
|
|
74
|
+
const ct = token ?? (await import("./cli-token.mjs"));
|
|
75
|
+
|
|
76
|
+
section("Machine id (the free trial needs one)");
|
|
77
|
+
await safe("machine id", async () => {
|
|
78
|
+
const id = await ct.osMachineId({ platform });
|
|
79
|
+
if (id) line(`OK — the operating system gives this computer an id (${id.length} characters; not shown).`);
|
|
80
|
+
else {
|
|
81
|
+
line("MISSING — no operating-system machine id, so gu makes NO free-trial request.");
|
|
82
|
+
if (platform === "linux") line(" /etc/machine-id is empty or absent. Fix: sudo systemd-machine-id-setup (then run gu again)");
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
section("gu service (free token, account)");
|
|
87
|
+
const site = ct.siteUrl(env);
|
|
88
|
+
await safe("site", async () => {
|
|
89
|
+
try {
|
|
90
|
+
const res = await fetchImpl(site, { method: "GET", signal: AbortSignal.timeout(8000) });
|
|
91
|
+
line(`${site} → HTTP ${res.status}`);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
line(`${site} → cannot be reached: ${ct.netErrorDetail(err, 8000)}`);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
const workerEnv = await readFile(join(dir, "worker.env"), "utf8").catch(() => null);
|
|
97
|
+
if (workerEnv === null) line("worker.env: absent (not signed in / Local mode)");
|
|
98
|
+
else {
|
|
99
|
+
const keys = workerEnv.split(/\r?\n/).map((l) => l.trim()).filter((l) => l && !l.startsWith("#") && l.includes("=")).map((l) => l.split("=")[0].trim());
|
|
100
|
+
line(`worker.env: present, settings: ${keys.join(", ") || "(none)"}`);
|
|
101
|
+
const base = /^GONEXT_API_BASE=(.*)$/m.exec(workerEnv)?.[1]?.trim();
|
|
102
|
+
if (base) line(`API base: ${base}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
section("Token");
|
|
106
|
+
await safe("token", async () => {
|
|
107
|
+
const st = await ct.readState(dir);
|
|
108
|
+
if (!st.token) line("No token saved.");
|
|
109
|
+
else {
|
|
110
|
+
const pk = ct.publicKey(env);
|
|
111
|
+
const check = ct.verifyCliToken(st.token, pk);
|
|
112
|
+
line(check.ok ? "A token is saved and valid." : `A token is saved but not valid here: ${check.why}`);
|
|
113
|
+
}
|
|
114
|
+
if (st.lastAttemptAt) line(`last automatic fetch: ${st.lastAttemptAt}${st.lastAttemptWhy ? ` — failed: ${st.lastAttemptWhy}` : ""}`);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
section("Local API and MongoDB");
|
|
118
|
+
await safe("local api", async () => {
|
|
119
|
+
const ce = api ?? (await import("./client-expert-api.mjs"));
|
|
120
|
+
const probe = await ce.probeLocalApi();
|
|
121
|
+
line(probe.ok ? `API up on ${ce.LOCAL_API_BASE}${probe.body?.version ? ` (running ${probe.body.version})` : ""}` : `API not usable: ${probe.why}`);
|
|
122
|
+
const uri = ce.localMongoUri({ env, home });
|
|
123
|
+
line(`MongoDB uri: ${redactSecrets(uri)}`);
|
|
124
|
+
try {
|
|
125
|
+
const r = run ?? (await import("./client-expert-run.mjs"));
|
|
126
|
+
const down = await r.localMongoDown(uri);
|
|
127
|
+
line(down ? `MongoDB does NOT answer at ${down}` : "MongoDB answers.");
|
|
128
|
+
} catch (e) {
|
|
129
|
+
line(`MongoDB: could not check (${e?.message || e})`);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
section("Files in ~/.gonext");
|
|
134
|
+
await safe("files", async () => {
|
|
135
|
+
const names = (await readdir(dir).catch(() => [])).sort();
|
|
136
|
+
if (!names.length) line("(folder is empty or absent — a totally fresh install)");
|
|
137
|
+
for (const n of names) {
|
|
138
|
+
const info = await fileInfo(join(dir, n));
|
|
139
|
+
if (info) line(`${n} ${info.size} bytes ${info.mtime}`);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
for (const name of SUPPORT_LOGS) {
|
|
144
|
+
const text = await readFile(join(dir, name), "utf8").catch(() => null);
|
|
145
|
+
if (text === null) continue;
|
|
146
|
+
section(`${name} (last ${logLines} lines)`);
|
|
147
|
+
for (const l of lastLines(text, logLines)) line(l);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return out.map(redactReport);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Build, write ~/.gonext/support-report.txt (owner-only) and return { lines, file }. */
|
|
154
|
+
export async function writeSupportReport(opts = {}) {
|
|
155
|
+
const home = opts.home ?? homedir();
|
|
156
|
+
const lines = await buildSupportReport(opts);
|
|
157
|
+
const file = join(home, ".gonext", SUPPORT_REPORT_FILE);
|
|
158
|
+
await mkdir(join(home, ".gonext"), { recursive: true });
|
|
159
|
+
await writeFile(file, lines.join("\n") + "\n", { encoding: "utf8", mode: 0o600 });
|
|
160
|
+
return { lines, file };
|
|
161
|
+
}
|