@tiens.nguyen/gu-cli 1.0.896 → 1.0.898
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 +28 -12
- package/agent-model-command.mjs +15 -0
- package/agent-model-label.mjs +1 -1
- package/client-expert-run.mjs +92 -7
- package/coding-provider-setup.mjs +1 -1
- package/daemon-control.mjs +2 -1
- package/doctor-check.mjs +66 -1
- package/embed-model-command.mjs +2 -2
- package/gonext_agent_chat.py +4 -2
- package/gu-cli.mjs +8 -2
- package/gu-repl.mjs +349 -120
- package/mongo-mac.mjs +166 -0
- package/node-version.mjs +25 -3
- package/package.json +3 -2
- package/platform-tools.mjs +10 -3
- package/server-keys.mjs +0 -1
- package/terminal-layout.mjs +40 -34
package/mongo-mac.mjs
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MONGODB ON macOS 13 AND OLDER (GH #403).
|
|
3
|
+
*
|
|
4
|
+
* Homebrew's mongodb-community is 9.0, and every MongoDB from 8.0.6 on is built for macOS 14+
|
|
5
|
+
* (LC_BUILD_VERSION minos 14.0 — checked with otool on the 8.0.6 … 9.0.4 downloads). On macOS 13
|
|
6
|
+
* it installs fine and then cannot start: "dyld: Symbol not found … built for macOS 14.0".
|
|
7
|
+
* 8.0.5 is the last release built for older Macs (minos 10.14 on Intel, 11.0 on Apple Silicon).
|
|
8
|
+
*
|
|
9
|
+
* So on those Macs gu runs its own mongod 8.0.5, like the Windows one (#210): no Homebrew, no
|
|
10
|
+
* service, no sudo. Everything lives under ~/.gonext/mongodb and goes away with that folder:
|
|
11
|
+
* bin/mongod data/ mongod.log launch.log version.txt
|
|
12
|
+
*
|
|
13
|
+
* The .tgz is gzip, so unlike the Windows zip one file cannot be fetched on its own: the whole
|
|
14
|
+
* archive (~80-90 MB) is downloaded, checked against MongoDB's published SHA-256, and only
|
|
15
|
+
* bin/mongod is kept.
|
|
16
|
+
*/
|
|
17
|
+
import { createHash } from "node:crypto";
|
|
18
|
+
import { execFile, execFileSync } from "node:child_process";
|
|
19
|
+
import { createWriteStream } from "node:fs";
|
|
20
|
+
import { mkdir, rename, rm, stat, writeFile, mkdtemp, chmod } from "node:fs/promises";
|
|
21
|
+
import { homedir, release } from "node:os";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
|
|
24
|
+
export const MONGO_MAC_OLD_VERSION = "8.0.5";
|
|
25
|
+
/** Homebrew's MongoDB (and every release after 8.0.5) needs this macOS or newer. */
|
|
26
|
+
export const MONGO_MAC_BREW_MIN_MAJOR = 14;
|
|
27
|
+
|
|
28
|
+
// From https://downloads.mongodb.org/full.json, and re-checked by downloading both (2026-11-07).
|
|
29
|
+
export const MONGO_MAC_DOWNLOADS = {
|
|
30
|
+
x64: {
|
|
31
|
+
url: `https://fastdl.mongodb.org/osx/mongodb-macos-x86_64-${MONGO_MAC_OLD_VERSION}.tgz`,
|
|
32
|
+
sha256: "eb528846663ef106003b3c491dfe3722f918cb75d056670b23a620b339f7f6ec",
|
|
33
|
+
},
|
|
34
|
+
arm64: {
|
|
35
|
+
url: `https://fastdl.mongodb.org/osx/mongodb-macos-arm64-${MONGO_MAC_OLD_VERSION}.tgz`,
|
|
36
|
+
sha256: "71e2510f0455ff24e1961aa0507eb0660b300c84f6da9899052be409090fb990",
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** "13.7.1" → 13. Darwin's kernel number is the fallback (Darwin 20-24 = macOS 11-15, 25 = 26). */
|
|
41
|
+
export function macosMajor({ productVersion = null, darwin = release(), platform = process.platform } = {}) {
|
|
42
|
+
if (productVersion == null && platform !== "darwin") return 0; // not a Mac: no answer, not a guess
|
|
43
|
+
let v = productVersion;
|
|
44
|
+
if (v == null) {
|
|
45
|
+
try { v = execFileSync("sw_vers", ["-productVersion"], { encoding: "utf8", timeout: 5000 }); } catch { v = ""; }
|
|
46
|
+
}
|
|
47
|
+
const m = /^(\d+)/.exec(String(v).trim());
|
|
48
|
+
if (m) return Number(m[1]);
|
|
49
|
+
const d = parseInt(String(darwin), 10);
|
|
50
|
+
if (!Number.isFinite(d)) return 0;
|
|
51
|
+
return d >= 25 ? d + 1 : d >= 20 ? d - 9 : 10;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Does this Mac need gu's own MongoDB? 0 (unknown) keeps Homebrew — the path that worked before. */
|
|
55
|
+
export function macNeedsOwnMongo(major) {
|
|
56
|
+
return major > 0 && major < MONGO_MAC_BREW_MIN_MAJOR;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Where gu keeps its own mongod on a Mac — the same layout as Windows. `home` for tests. */
|
|
60
|
+
export function mongoMacPaths(home = homedir()) {
|
|
61
|
+
const dir = join(home, ".gonext", "mongodb");
|
|
62
|
+
return {
|
|
63
|
+
dir,
|
|
64
|
+
exe: join(dir, "bin", "mongod"),
|
|
65
|
+
data: join(dir, "data"),
|
|
66
|
+
log: join(dir, "mongod.log"),
|
|
67
|
+
launchLog: join(dir, "launch.log"),
|
|
68
|
+
version: join(dir, "version.txt"),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Is gu's own mongod there? */
|
|
73
|
+
export async function macMongoInstalled(paths = mongoMacPaths()) {
|
|
74
|
+
try { return (await stat(paths.exe)).isFile(); } catch { return false; }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The download for this CPU, or null for one MongoDB does not build. */
|
|
78
|
+
export function macMongoDownload(arch = process.arch) {
|
|
79
|
+
return MONGO_MAC_DOWNLOADS[arch] ?? null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Download the archive, check its SHA-256, keep only bin/mongod. → { bytes }.
|
|
84
|
+
* Throws a plain reason; nothing half-written is left at paths.exe.
|
|
85
|
+
* `fetchFn` and `extract` are injectable so tests never touch the network or tar.
|
|
86
|
+
*/
|
|
87
|
+
export async function downloadMongodMac({
|
|
88
|
+
paths = mongoMacPaths(),
|
|
89
|
+
arch = process.arch,
|
|
90
|
+
fetchFn = fetch,
|
|
91
|
+
extract = extractMongod,
|
|
92
|
+
log = () => {},
|
|
93
|
+
version = MONGO_MAC_OLD_VERSION,
|
|
94
|
+
dl = macMongoDownload(arch),
|
|
95
|
+
} = {}) {
|
|
96
|
+
if (!dl) throw new Error(`MongoDB has no macOS build for ${arch}`);
|
|
97
|
+
await mkdir(paths.dir, { recursive: true });
|
|
98
|
+
const tgz = join(paths.dir, "mongodb.tgz.part");
|
|
99
|
+
const res = await fetchFn(dl.url);
|
|
100
|
+
if (!res.ok || !res.body) throw new Error(`download failed (HTTP ${res.status})`);
|
|
101
|
+
const total = Number(res.headers?.get?.("content-length")) || 0;
|
|
102
|
+
const mb = (n) => (n / 1048576).toFixed(0);
|
|
103
|
+
const hash = createHash("sha256");
|
|
104
|
+
const out = createWriteStream(tgz);
|
|
105
|
+
let got = 0;
|
|
106
|
+
let shown = -1;
|
|
107
|
+
try {
|
|
108
|
+
const reader = res.body.getReader();
|
|
109
|
+
for (;;) {
|
|
110
|
+
const { done, value } = await reader.read();
|
|
111
|
+
if (done) break;
|
|
112
|
+
hash.update(value);
|
|
113
|
+
got += value.length;
|
|
114
|
+
if (!out.write(value)) await new Promise((r) => out.once("drain", r));
|
|
115
|
+
if (total) {
|
|
116
|
+
const pct = Math.floor((got / total) * 10) * 10;
|
|
117
|
+
if (pct !== shown) { shown = pct; log(` … ${pct}% (${mb(got)} of ${mb(total)} MB)`); }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} finally {
|
|
121
|
+
await new Promise((r) => out.end(r));
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
if (hash.digest("hex") !== dl.sha256) throw new Error("the download is damaged (SHA-256 does not match) — run gu again");
|
|
125
|
+
const tmp = await mkdtemp(join(paths.dir, "unpack-"));
|
|
126
|
+
try {
|
|
127
|
+
const mongod = await extract(tgz, tmp);
|
|
128
|
+
await mkdir(join(paths.dir, "bin"), { recursive: true });
|
|
129
|
+
await rm(paths.exe, { force: true });
|
|
130
|
+
await rename(mongod, paths.exe);
|
|
131
|
+
await chmod(paths.exe, 0o755);
|
|
132
|
+
} finally {
|
|
133
|
+
await rm(tmp, { recursive: true, force: true });
|
|
134
|
+
}
|
|
135
|
+
} finally {
|
|
136
|
+
await rm(tgz, { force: true });
|
|
137
|
+
}
|
|
138
|
+
await writeFile(paths.version, `${version}\n`);
|
|
139
|
+
return { bytes: got };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** tar -x of only the archive's bin/mongod into `dir` → the extracted file's path. */
|
|
143
|
+
export function extractMongod(tgz, dir) {
|
|
144
|
+
return new Promise((resolve, reject) => {
|
|
145
|
+
execFile("tar", ["-xzf", tgz, "-C", dir, "--strip-components", "2", "*/bin/mongod"], { timeout: 300000 }, (err, _o, stderr) => {
|
|
146
|
+
if (err) reject(new Error(`could not unpack the archive: ${String(stderr || err.message).trim().split("\n").pop()}`));
|
|
147
|
+
else resolve(join(dir, "mongod"));
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Can this mongod run here at all? `mongod --version` stops at once with a dyld error when the
|
|
154
|
+
* build is for a newer macOS — the failure this module exists for — so ask it before starting it
|
|
155
|
+
* detached, where that would only show up as "not answering". → { ok, why }.
|
|
156
|
+
*/
|
|
157
|
+
export function mongodRunsMac(exe, { execFn = execFile, timeoutMs = 20000 } = {}) {
|
|
158
|
+
return new Promise((resolve) => {
|
|
159
|
+
execFn(exe, ["--version"], { timeout: timeoutMs }, (err, stdout, stderr) => {
|
|
160
|
+
if (!err) return resolve({ ok: true, why: "" });
|
|
161
|
+
const text = `${stderr || ""}\n${stdout || ""}`;
|
|
162
|
+
const newer = /built for macOS ([\d.]+) which is newer/.exec(text);
|
|
163
|
+
resolve({ ok: false, why: newer ? `it is built for macOS ${newer[1]} or newer` : (text.trim().split("\n")[0] || String(err.message)) });
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
}
|
package/node-version.mjs
CHANGED
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Checked at the TOP of both entry points, before any import that might itself fail.
|
|
14
14
|
*/
|
|
15
|
+
import { readdirSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
|
|
15
18
|
export const MIN_NODE_MAJOR = 18;
|
|
16
19
|
|
|
17
20
|
/** The message for a too-old runtime, or "" when this one is fine. Pure, so it is testable. */
|
|
@@ -52,11 +55,30 @@ export function nodeAtLeast(version, min) {
|
|
|
52
55
|
return true;
|
|
53
56
|
}
|
|
54
57
|
|
|
58
|
+
/**
|
|
59
|
+
* The Node versions nvm has installed when THIS node is one of nvm's, else []. Found on the 2017
|
|
60
|
+
* MacBook Pro (GH #406): nvm's default was 18 while 22 was already installed, and "install a new
|
|
61
|
+
* Node" was the wrong advice — one `nvm alias default` was the whole fix.
|
|
62
|
+
*/
|
|
63
|
+
export function nvmNodeVersions({ env = process.env, execPath = process.execPath, list = null } = {}) {
|
|
64
|
+
const dir = env.NVM_DIR;
|
|
65
|
+
if (!dir || !String(execPath).startsWith(dir)) {return [];}
|
|
66
|
+
let names = list;
|
|
67
|
+
if (!names) {
|
|
68
|
+
try { names = readdirSync(join(dir, "versions", "node")); } catch { return []; }
|
|
69
|
+
}
|
|
70
|
+
return names.filter((n) => /^v\d+\.\d+\.\d+$/.test(n));
|
|
71
|
+
}
|
|
72
|
+
|
|
55
73
|
/** Why Local mode cannot run on this node, with what to do — or "" when it can. Pure. */
|
|
56
|
-
export function localModeNodeProblem(version = process.version, platform = process.platform) {
|
|
74
|
+
export function localModeNodeProblem(version = process.version, platform = process.platform, { nvmVersions = [] } = {}) {
|
|
57
75
|
if (nodeAtLeast(version, LOCAL_API_MIN_NODE)) {return "";}
|
|
58
|
-
|
|
59
|
-
|
|
76
|
+
// nvm already has a new enough Node: switch to it, nothing to install.
|
|
77
|
+
const ready = nvmVersions.filter((v) => nodeAtLeast(v, LOCAL_API_MIN_NODE))
|
|
78
|
+
.sort((a, b) => (nodeAtLeast(a, b) ? -1 : 1))[0];
|
|
79
|
+
const how = ready
|
|
80
|
+
? [` nvm already has ${ready}. Make it the default, then open a new terminal:`, ` nvm alias default ${ready.replace(/^v/, "")}`]
|
|
81
|
+
: platform === "win32"
|
|
60
82
|
? [" winget install OpenJS.NodeJS.LTS (or: nvm install lts with nvm-windows)"]
|
|
61
83
|
: platform === "darwin"
|
|
62
84
|
? [" brew install node (or: nvm install --lts)"]
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiens.nguyen/gu-cli",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "gu: the coding agent in your terminal. It reads your repo, makes the change and runs the tests, with your own models (Ollama or any OpenAI-compatible provider). Free installs
|
|
3
|
+
"version": "1.0.898",
|
|
4
|
+
"description": "gu: the coding agent in your terminal. It reads your repo, makes the change and runs the tests, with your own models (Ollama or any OpenAI-compatible provider). Free installs keep the agent, its API and its database on your machine. macOS, Linux and Windows.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
7
7
|
"coding-agent",
|
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
"turn-stop.mjs",
|
|
75
75
|
"workspace-revert.mjs",
|
|
76
76
|
"mongo-native.mjs",
|
|
77
|
+
"mongo-mac.mjs",
|
|
77
78
|
"mongo-windows.mjs",
|
|
78
79
|
"python-windows.mjs",
|
|
79
80
|
"terminal-hover.mjs",
|
package/platform-tools.mjs
CHANGED
|
@@ -30,8 +30,15 @@ const run = (cmd, args, opts = {}) =>
|
|
|
30
30
|
|
|
31
31
|
/* ------------------------------------------------------------------ process listing ----- */
|
|
32
32
|
|
|
33
|
-
/**
|
|
34
|
-
|
|
33
|
+
/**
|
|
34
|
+
* The command that lists every process with its full command line.
|
|
35
|
+
*
|
|
36
|
+
* `ownUser` (GH #396): only THIS user's processes, on macOS and Linux. Another account on the same machine has its
|
|
37
|
+
* own HOME, worker.env and daemon: counting its gu daemon as "ours is running" meant a second account's daemon
|
|
38
|
+
* was never started (found on a Mac shared by two accounts: "start it with `gu start`"), and it is not ours to stop.
|
|
39
|
+
* Windows keeps the full list: a daemon started by a scheduled task runs in a different session than the terminal.
|
|
40
|
+
*/
|
|
41
|
+
export function processListCommand(platform = process.platform, { ownUser = false, uid = process.getuid?.() } = {}) {
|
|
35
42
|
return platform === "win32"
|
|
36
43
|
? {
|
|
37
44
|
// CIM over WMIC: wmic is deprecated and absent from newer Windows 11 images, and
|
|
@@ -43,7 +50,7 @@ export function processListCommand(platform = process.platform) {
|
|
|
43
50
|
"Get-CimInstance Win32_Process | ForEach-Object { \"$($_.ProcessId) $($_.CommandLine)\" }",
|
|
44
51
|
],
|
|
45
52
|
}
|
|
46
|
-
: { cmd: "ps", args: ["-A", "-o", "pid=,command="] };
|
|
53
|
+
: { cmd: "ps", args: [...(ownUser && Number.isInteger(uid) ? ["-U", String(uid)] : ["-A"]), "-o", "pid=,command="] };
|
|
47
54
|
}
|
|
48
55
|
|
|
49
56
|
/**
|
package/server-keys.mjs
CHANGED
|
@@ -49,7 +49,6 @@ export function keyTags(k, { folderKeyId = "", inUseId = "" } = {}) {
|
|
|
49
49
|
if (k.id === folderKeyId) tags.push("this folder");
|
|
50
50
|
else if (k.id === inUseId) tags.push("in use");
|
|
51
51
|
if (k.default) tags.push("default");
|
|
52
|
-
if (k.first) tags.push("first key");
|
|
53
52
|
if (!k.backendId) tags.push("not tied to a server yet");
|
|
54
53
|
return tags;
|
|
55
54
|
}
|
package/terminal-layout.mjs
CHANGED
|
@@ -557,26 +557,50 @@ export function createMouseKeyWindow({ maxRun = 24 } = {}) {
|
|
|
557
557
|
* "^" is only held while the terminal is reporting the mouse (`plainLeads`), so typing them is
|
|
558
558
|
* not slowed down otherwise.
|
|
559
559
|
*/
|
|
560
|
-
const MOUSE_LEADS = ["\x1b[<", "^[[<", "[<"];
|
|
561
560
|
const MOUSE_BODY_DONE = /^(\d+;\d+;\d+[Mm])$/;
|
|
562
561
|
const MOUSE_BODY_PART = /^\d*(?:;\d*){0,2}$/;
|
|
562
|
+
// Windows "win32-input-mode" key records: ESC [ Vk ; Sc ; Uc ; Kd ; Cs ; Rc _ (six numbers).
|
|
563
|
+
const WIN32_BODY_DONE = /^\d+(?:;\d+){5}_$/;
|
|
564
|
+
const WIN32_BODY_PART = /^\d*(?:;\d*){0,5}$/;
|
|
565
|
+
/**
|
|
566
|
+
* What `text` (the held keys, ESC written as "\x1b") is so far. Leads: a real ESC, ESC spelled
|
|
567
|
+
* "^[", or no ESC at all. The last two only while the mouse is reported (`plainLeads`), and a
|
|
568
|
+
* bare "[" only for a MOUSE report: "[I" or "[1;2" are things people type.
|
|
569
|
+
* done + report "35;114;22M" a mouse report
|
|
570
|
+
* done + report null a terminal message with nothing to do: focus in/out ("I"/"O",
|
|
571
|
+
* sent when the window is minimised and restored — GH #365) or a
|
|
572
|
+
* win32 key record the console let through
|
|
573
|
+
*/
|
|
563
574
|
function mouseTextVerdict(text, plainLeads) {
|
|
564
|
-
for (const lead of
|
|
565
|
-
if (!plainLeads && lead !== "\x1b[
|
|
575
|
+
for (const lead of ["\x1b[", "^[[", "["]) {
|
|
576
|
+
if (!plainLeads && lead !== "\x1b[") continue;
|
|
566
577
|
if (lead.startsWith(text)) return { kind: "partial" };
|
|
567
578
|
if (!text.startsWith(lead)) continue;
|
|
568
579
|
const body = text.slice(lead.length);
|
|
569
|
-
if (
|
|
570
|
-
|
|
580
|
+
if (body.startsWith("<")) {
|
|
581
|
+
const m = body.slice(1);
|
|
582
|
+
if (MOUSE_BODY_DONE.test(m)) return { kind: "done", report: m };
|
|
583
|
+
if (m.length <= 20 && MOUSE_BODY_PART.test(m)) return { kind: "partial" };
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
if (lead === "[") {
|
|
587
|
+
if (body === "") return { kind: "partial" };
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
if (body === "I" || body === "O" || WIN32_BODY_DONE.test(body)) return { kind: "done", report: null };
|
|
591
|
+
if (body.length <= 40 && WIN32_BODY_PART.test(body)) return { kind: "partial" };
|
|
571
592
|
}
|
|
572
593
|
return { kind: "bad" };
|
|
573
594
|
}
|
|
574
595
|
export function createMouseReportReader() {
|
|
575
596
|
let held = null; // [str, key] pairs
|
|
597
|
+
// A CSI node could not name ("\x1b[I", or "\x1b[17;29" with the rest still to come) is held
|
|
598
|
+
// by its sequence; a named one (an arrow, F3, Ctrl+Up) is a key and never starts a report.
|
|
599
|
+
const unnamedCsi = (key) => key?.name === "undefined" && /^\x1b\[/.test(key?.sequence ?? "");
|
|
576
600
|
const textOf = (str, key) =>
|
|
577
|
-
key?.code === "[<" ? "\x1b[<" : key?.name === "escape" ? "\x1b" : typeof str === "string" ? str : "";
|
|
601
|
+
key?.code === "[<" ? "\x1b[<" : unnamedCsi(key) ? key.sequence : key?.name === "escape" ? "\x1b" : typeof str === "string" ? str : "";
|
|
578
602
|
const startsOne = (str, key, plainLeads) =>
|
|
579
|
-
key?.code === "[<" || key?.name === "escape" || (plainLeads && (str === "[" || str === "^"));
|
|
603
|
+
key?.code === "[<" || unnamedCsi(key) || key?.name === "escape" || (plainLeads && (str === "[" || str === "^"));
|
|
580
604
|
return {
|
|
581
605
|
holding: () => held !== null,
|
|
582
606
|
/**
|
|
@@ -595,6 +619,8 @@ export function createMouseReportReader() {
|
|
|
595
619
|
const v = mouseTextVerdict(textOf(str, key), plainLeads);
|
|
596
620
|
if (v.kind === "partial") return { action: "hold" };
|
|
597
621
|
held = null;
|
|
622
|
+
// Whole in one key ("\x1b[O"): node would drop it anyway, but it is ours to drop.
|
|
623
|
+
if (v.kind === "done") return { action: "consume", report: v.report };
|
|
598
624
|
return { action: "pass" };
|
|
599
625
|
}
|
|
600
626
|
held.push([str, key]);
|
|
@@ -626,7 +652,9 @@ export function createMouseReportReader() {
|
|
|
626
652
|
|
|
627
653
|
/** Last line of defence at submit: a report that got past every reader is not a question. */
|
|
628
654
|
export function stripLeakedMouseReports(line) {
|
|
629
|
-
return String(line)
|
|
655
|
+
return String(line)
|
|
656
|
+
.replace(/(?:\x1b|\^\[)?\[<\d+;\d+;\d+[Mm]/g, "")
|
|
657
|
+
.replace(/(?:\x1b|\^\[)\[(?:[IO]|\d+(?:;\d+){5}_)/g, "");
|
|
630
658
|
}
|
|
631
659
|
|
|
632
660
|
/**
|
|
@@ -2762,29 +2790,6 @@ export function resizeRepaintTarget({ barPinned, modal, busy, scrolledBack }) {
|
|
|
2762
2790
|
return scrolledBack ? "scrollback" : "live";
|
|
2763
2791
|
}
|
|
2764
2792
|
|
|
2765
|
-
/**
|
|
2766
|
-
* Should the workspace "sync chat history to the cloud?" question be ASKED? (task #154)
|
|
2767
|
-
*
|
|
2768
|
-
* Not on a Client Expert machine. There is no cloud there: the API and the database are on
|
|
2769
|
-
* this box, "your account" is a local row, and there is no web app to see it on — so every
|
|
2770
|
-
* clause of the question is false. Asking a question whose premise is wrong is worse than not
|
|
2771
|
-
* asking, because the answer cannot mean what the reader thinks it means.
|
|
2772
|
-
*
|
|
2773
|
-
* The sync itself still happens — it writes to the local database, which is where that
|
|
2774
|
-
* machine's conversations already live — so the value is true and it is stated rather than
|
|
2775
|
-
* asked. Reported live: a Client Expert machine on Windows offering to send chat to a cloud
|
|
2776
|
-
* it does not have.
|
|
2777
|
-
*/
|
|
2778
|
-
export function workspaceSyncQuestion({ clientExpert }) {
|
|
2779
|
-
return clientExpert
|
|
2780
|
-
? {
|
|
2781
|
-
ask: false,
|
|
2782
|
-
value: true,
|
|
2783
|
-
note: "chat history is kept in the local database on this machine (Local mode)",
|
|
2784
|
-
}
|
|
2785
|
-
: { ask: true, value: null, note: "" };
|
|
2786
|
-
}
|
|
2787
|
-
|
|
2788
2793
|
/**
|
|
2789
2794
|
* The ONE first-run question for a new folder (GitHub #192). It used to be two or three separate
|
|
2790
2795
|
* Y/n prompts (register, trust to run commands, then the browser on first use). They are one
|
|
@@ -2900,8 +2905,9 @@ export function identityRow(
|
|
|
2900
2905
|
// The mark only means anything beside a named coder: with none shown there is nothing for it
|
|
2901
2906
|
// to be about, and it would read as a fault in the folder.
|
|
2902
2907
|
const mark = name && keyUnchosen ? red(" key not chosen") : "";
|
|
2903
|
-
// API key tail:
|
|
2904
|
-
|
|
2908
|
+
// API key tail, LABELLED (GH #397): a bare "| VW3Y" between the model and the folder read as a
|
|
2909
|
+
// stray code. Same "····" form every key line in gu uses.
|
|
2910
|
+
const keyIndicator = apiKeyTail ? dim(" | ") + dim(`key ····${apiKeyTail}`) : "";
|
|
2905
2911
|
return (name ? orange(name) + mark + keyIndicator + dim(" | ") : "") + cyan(where);
|
|
2906
2912
|
}
|
|
2907
2913
|
|
|
@@ -2910,7 +2916,7 @@ export function identityRow(
|
|
|
2910
2916
|
*
|
|
2911
2917
|
* ON CLIENT EXPERT, NOTHING. That mode's whole promise is that the machine keeps everything —
|
|
2912
2918
|
* API, database, models — and shares none of it, so "cloud sync on" is not merely noise, it
|
|
2913
|
-
* contradicts the mode the user chose (reported 2026-09-04).
|
|
2919
|
+
* contradicts the mode the user chose (reported 2026-09-04). The workspace record already
|
|
2914
2920
|
* knows this and stores `true` meaning "persist it", but the LABEL read that value as "to the
|
|
2915
2921
|
* cloud", which is the one place it does not go.
|
|
2916
2922
|
*
|