@tiens.nguyen/gu-cli 1.0.692 → 1.0.693

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.
@@ -84,7 +84,7 @@ export async function promptCodingKeyIfMissing(payload, { askSecret, saveCodingK
84
84
  return { asked: false, saved: Boolean(ok), keyless: true, model, url: payload.codingKeyUrl };
85
85
  }
86
86
 
87
- log(dim("\ngonext: your agent coding model (") + cyan(model) + dim(") needs an API key."));
87
+ log(dim("\ngu: your agent coding model (") + cyan(model) + dim(") needs an API key."));
88
88
  log(dim(" Until it's set, coding falls back to your local model. Enter the key now,"));
89
89
  log(dim(" or press Enter to skip (I'll ask again next time).\n"));
90
90
 
@@ -251,7 +251,7 @@ export async function findRunningDaemonPids() {
251
251
  * True when `pid` is actually a gu polling daemon (not an unrelated process that reused
252
252
  * the PID). Reads that one pid's command line — cheap and exact (review M1).
253
253
  */
254
- function pidIsGonextDaemon(pid) {
254
+ function pidIsGuDaemon(pid) {
255
255
  if (!pid || !Number.isFinite(pid)) return Promise.resolve(false);
256
256
  return new Promise((resolve) => {
257
257
  const one = processCommandCommand(pid);
@@ -324,7 +324,7 @@ export async function readBusyState() {
324
324
  export async function daemonStatus() {
325
325
  const [pids, pidFilePid] = await Promise.all([findRunningDaemonPids(), readPidFile()]);
326
326
  const pidFileIsDaemon =
327
- pidFilePid != null && (pids.includes(pidFilePid) || (await pidIsGonextDaemon(pidFilePid)));
327
+ pidFilePid != null && (pids.includes(pidFilePid) || (await pidIsGuDaemon(pidFilePid)));
328
328
  const allPids = pidFileIsDaemon && !pids.includes(pidFilePid) ? [...pids, pidFilePid] : pids;
329
329
  return {
330
330
  running: allPids.length > 0,
@@ -5508,7 +5508,7 @@ def _rag_read_allowed(path: str) -> str:
5508
5508
  base = _WS_ACTIVE or (_WS_ROOTS[0]["path"] if _WS_ROOTS else None)
5509
5509
  if base is None:
5510
5510
  raise ValueError(
5511
- "No workspace is active — register a folder with `gonext-local-worker "
5511
+ "No workspace is active — register a folder with `gu-local-worker "
5512
5512
  "workspace add <path>` before reading files."
5513
5513
  )
5514
5514
  expanded = os.path.join(base, _ws_strip_echoed_root(base, expanded))
@@ -5842,7 +5842,7 @@ def _ws_write_allowed(path: str) -> str:
5842
5842
  if w is None:
5843
5843
  raise ValueError(
5844
5844
  "Writes are only allowed inside a registered workspace "
5845
- "(register one with: gonext-local-worker workspace add <path>)."
5845
+ "(register one with: gu-local-worker workspace add <path>)."
5846
5846
  )
5847
5847
  rel = os.path.relpath(rp, w["path"])
5848
5848
  if rel == ".git" or rel.startswith(".git" + os.sep) or (os.sep + ".git" + os.sep) in (os.sep + rel + os.sep):
@@ -5884,7 +5884,7 @@ def _ws_snapshot(path: str) -> str:
5884
5884
 
5885
5885
 
5886
5886
  def _ws_record_change(action: str, path: str, backup) -> None:
5887
- """Track a change AND persist the run manifest so `gonext-local-worker workspace
5887
+ """Track a change AND persist the run manifest so `gu-local-worker workspace
5888
5888
  revert` can restore edits / delete created files after the process is gone."""
5889
5889
  import os
5890
5890
  _WS_CHANGES.append({"action": action, "path": path, "backup": backup or None})
@@ -6772,7 +6772,7 @@ def run_agent_chat(cfg):
6772
6772
  # Ollama?" or "did the user pick openai?" separately — reasoning_effort, streaming, the
6773
6773
  # #113 reasoning repairs, and this step budget — now reads `coding_backend`.
6774
6774
  #
6775
- # The worker (gonext-local-worker.mjs) resolves this first, because it must decide
6775
+ # The worker (gu-local-worker.mjs) resolves this first, because it must decide
6776
6776
  # whether to fetch the coding API key BEFORE python starts, and passes the answer down
6777
6777
  # as codingBackend. Trust it only when we ended up on the URL it judged: the fallbacks
6778
6778
  # above can swap coding_base_url for the chat model's, which is a different server and
@@ -7094,7 +7094,7 @@ def run_agent_chat(cfg):
7094
7094
  )
7095
7095
 
7096
7096
  # ---- Workspaces (local folders the agent may read/edit/test code in) ----
7097
- # Registered on the Mac via `gonext-local-worker workspace add <path>`; the worker
7097
+ # Registered on the Mac via `gu-local-worker workspace add <path>`; the worker
7098
7098
  # passes the registry through cfg. Only these roots are writable — the security
7099
7099
  # boundary for the coding tools.
7100
7100
  global _WS_ROOTS, _WS_ACTIVE
@@ -11556,7 +11556,7 @@ def run_agent_chat(cfg):
11556
11556
  "To give the user a FILE or a ZIP of a folder, use the create_download "
11557
11557
  "tool instead — it needs no shell and works here. "
11558
11558
  "(To enable real commands, re-register with: "
11559
- "gonext-local-worker workspace add <path> --allow-run)")
11559
+ "gu-local-worker workspace add <path> --allow-run)")
11560
11560
  _last_obs["text"] = msg
11561
11561
  return msg
11562
11562
  argv = shlex.split(command or "")
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- GoNext MLX embeddings server — an OpenAI-compatible /v1/embeddings endpoint for MLX
3
+ Gu MLX embeddings server — an OpenAI-compatible /v1/embeddings endpoint for MLX
4
4
  embedding models (e.g. Qwen3-Embedding-8B), which `mlx_lm.server` does NOT provide.
5
5
 
6
- The GoNext worker's RAG tools call `POST {url}/v1/embeddings` first, so pointing the
6
+ The Gu worker's RAG tools call `POST {url}/v1/embeddings` first, so pointing the
7
7
  "RAG embedding server URL" (web Settings → Agent → RAG) at this server makes MLX
8
8
  embeddings work with no other changes.
9
9
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env python3
2
- """Voice transcription for the GoNext local worker (Audio support, task #21).
2
+ """Voice transcription for the Gu local worker (Audio support, task #21).
3
3
 
4
4
  Reads a JSON job on stdin: {"audioPath": str, "model": str, "language": str?}.
5
5
  Transcribes with mlx-whisper and prints {"text": "..."} on stdout.
package/gu-cli.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * GoNext local worker:
3
+ * Gu local worker:
4
4
  * - `gu-cli set <workerKey> [--api-base URL]`
5
5
  * writes ~/.gonext/worker.env
6
6
  * - `gu-cli ws-ping-test` — POST worker ws-ping (needs GONEXT_* env)
@@ -2927,7 +2927,7 @@ async function runOcrJob(job) {
2927
2927
  }
2928
2928
  console.log(`[gu-worker] OCR source jobId=${jobId} ${sourceLabel}`);
2929
2929
 
2930
- const tempDir = await mkdtemp(join(tmpdir(), "gonext-ocr-worker-"));
2930
+ const tempDir = await mkdtemp(join(tmpdir(), "gu-ocr-worker-"));
2931
2931
  let extractedText = "";
2932
2932
  try {
2933
2933
  const imagePath = join(
package/gu-repl.mjs CHANGED
@@ -783,7 +783,6 @@ const COMMANDS = [
783
783
  // The two synonyms were dispatched but declared nowhere (found by slash-command-wiring.test.mjs):
784
784
  // they worked, Tab-completed to nothing, and appeared in no help. Aliases is what the registry
785
785
  // already has for exactly this — /quit, /rag-local.
786
- { name: "/ask-again", aliases: ["/daemon-ask", "/model-setup-ask"], desc: "forget the remembered \"no\" for the model-setup prompt in this folder" },
787
786
  { name: "/host-mode", desc: "undo client mode — run agent turns on THIS machine again" },
788
787
  { name: "/server", desc: "pick a deployment server (host/user) for this session" },
789
788
  { name: "/proxy", usage: "/proxy [show|set|on|off|clear|test]", desc: "route MODEL calls through an HTTP proxy (local calls always go direct)" },
@@ -1795,7 +1794,7 @@ async function refreshAvailableUpdates() {
1795
1794
  const { updatesAvailable } = await import("./version-check.mjs");
1796
1795
  const cliLatest = await (async () => {
1797
1796
  try {
1798
- const r = await fetch("https://registry.npmjs.org/@tiens.nguyen%2Fgonext-cli/latest", {
1797
+ const r = await fetch("https://registry.npmjs.org/@tiens.nguyen%2Fgu-cli/latest", {
1799
1798
  signal: AbortSignal.timeout(6000),
1800
1799
  });
1801
1800
  return r.ok ? (await r.json())?.version ?? "" : "";
@@ -9350,7 +9349,7 @@ async function main() {
9350
9349
  servers: await (await import("./model-doctor.mjs")).discoverModelServers(),
9351
9350
  });
9352
9351
  const statusRows = [
9353
- bold(cyan("GONEXT AGENT")) + dim(` v${REPL_VERSION}`),
9352
+ bold(cyan("GU AGENT")) + dim(` v${REPL_VERSION}`),
9354
9353
  // Task #134: a client machine OVERRIDES the account's agent model locally (the
9355
9354
  // account value points at the model host's own server). Show what will actually be
9356
9355
  // used, not a value this machine can't reach.
@@ -9422,7 +9421,7 @@ async function main() {
9422
9421
  const skipDeps = await loadSessionSkip(resolve(process.cwd()), "skipDepsPrompt");
9423
9422
  if (!skipDeps) {
9424
9423
  console.log(
9425
- dim("\ngonext: the agent framework (smolagents + openai) isn't installed here —") +
9424
+ dim("\ngu: the agent framework (smolagents + openai) isn't installed here —") +
9426
9425
  dim("\n every turn will fail without it. It's a small pip install; no models.")
9427
9426
  );
9428
9427
  if (await askOr(false, () => askYesNo("Install the agent framework now?", true))) {
@@ -9469,10 +9468,10 @@ async function main() {
9469
9468
  // to a local model setup here — its model is remote by design.
9470
9469
  if (!clientModeBanner && !(await md.probeModelServer(chatUrl))) {
9471
9470
  console.log(
9472
- dim(`\ngonext: the chat model server (${md.modelRoot(chatUrl)}) isn't responding.`)
9471
+ dim(`\ngu: the chat model server (${md.modelRoot(chatUrl)}) isn't responding.`)
9473
9472
  );
9474
9473
  // Task #135: remember a "no" for this folder. `gu-cli doctor` still works
9475
- // explicitly; /model-setup-ask clears the memory.
9474
+ // explicitly; `gu doctor` runs the same setup whenever it is wanted.
9476
9475
  const skipSetup = await loadSessionSkip(resolve(process.cwd()), "skipModelSetupPrompt");
9477
9476
  // Esc = "not now" rather than "yes": this branch DOWNLOADS a model, and a cancelled
9478
9477
  // question must never be read as consent to start one.
@@ -9483,8 +9482,8 @@ async function main() {
9483
9482
  );
9484
9483
  if (skipSetup) {
9485
9484
  console.log(
9486
- dim(" Set-up skipped (remembered for this folder) — `gu doctor` to run it, ") +
9487
- dim("`/model-setup-ask` to be asked again.\n")
9485
+ dim(" Set-up skipped (remembered for this folder) — `gu doctor` runs it ") +
9486
+ dim("any time.\n")
9488
9487
  );
9489
9488
  }
9490
9489
  if (go) {
@@ -9980,24 +9979,6 @@ async function main() {
9980
9979
  );
9981
9980
  continue;
9982
9981
  }
9983
- if (line === "/ask-again" || line === "/daemon-ask" || line === "/model-setup-ask") {
9984
- // Task #135: the way BACK from a remembered "no". Without this the choice is a
9985
- // one-way door — a setup that later changes (model installed, daemon wanted) would
9986
- // have no route to the prompt again short of editing the session file by hand.
9987
- await saveSessionSkip(cwd, "skipDaemonPrompt", false);
9988
- await saveSessionSkip(cwd, "skipModelSetupPrompt", false);
9989
- await saveSessionSkip(cwd, "skipDepsPrompt", false);
9990
- // The background worker is no longer among these: it is started and announced rather than
9991
- // asked about (task #159), so there is no remembered "no" left to undo. `gu stop` is
9992
- // the way to not have one running. Clearing its old key anyway, harmlessly, so a session
9993
- // file written by an earlier version does not keep a setting nothing reads.
9994
- console.log(
9995
- cyan("startup prompts re-enabled") +
9996
- dim(" — you'll be asked about model setup again next time you run `gu` here.\n") +
9997
- dim(" (the background worker is started automatically now — `gu stop` to stop it.)\n")
9998
- );
9999
- continue;
10000
- }
10001
9982
  if (line === "/rag" || line === "/rag-cloud" || line === "/rag-local") {
10002
9983
  // ONE QUESTION, THREE ANSWERS (task #180). This was two commands that toggled one flag,
10003
9984
  // so the command you typed did not determine the state you got — and the reply named a
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * postinstall: make `gu` / `gu-cli` runnable from the NEXT terminal without the user
4
+ * ever editing a profile by hand.
5
+ *
6
+ * WHY: the support loop that prompted this file is always the same — a user on macOS or
7
+ * Ubuntu runs `npm install -g @tiens.nguyen/gu-cli`, npm puts the shims in a global bin
8
+ * dir their shell never heard of, and the very first command answers "gu: command not
9
+ * found". The CLI works; the PATH does not. Users file it as "the install is broken".
10
+ *
11
+ * WHAT THIS DOES, in order of preference:
12
+ * 1. Nothing, when the global bin dir is already on PATH (the common case with nvm,
13
+ * Homebrew-node, or a repointed npm prefix).
14
+ * 2. Appends ONE idempotent, marker-wrapped `export PATH=...` block to the user's
15
+ * shell rc file — .zshrc on macOS (default shell since Catalina), .bashrc on
16
+ * Ubuntu/Debian — creating the file only when that shell is the platform default.
17
+ * 3. Prints the exact `export` line for the CURRENT terminal, because a child process
18
+ * physically cannot `source` anything into its parent shell.
19
+ *
20
+ * HARD RULES:
21
+ * - NEVER fail the install. Every path through this file ends in exit 0; npm treats a
22
+ * postinstall failure as a failed install and the user gets nothing.
23
+ * - NEVER run on Windows (PATH there lives in the registry, and `gu`'s PowerShell
24
+ * alias problem is powershell-alias.mjs's job, explicitly interactive).
25
+ * - NEVER touch an rc file twice: the marker pair makes re-runs no-ops.
26
+ * - Under `sudo npm i -g`, write to the REAL user's home (SUDO_USER) and chown the
27
+ * file back, or the user's next login shell reads a root-owned rc file.
28
+ */
29
+ import { appendFileSync, existsSync, readFileSync, chownSync, writeFileSync } from "node:fs";
30
+ import { homedir, platform, tmpdir } from "node:os";
31
+ import { join, resolve } from "node:path";
32
+ import { fileURLToPath } from "node:url";
33
+
34
+ const BEGIN = "# >>> gu-cli PATH >>>";
35
+ const END = "# <<< gu-cli PATH <<<";
36
+
37
+ /**
38
+ * Self-execute ONLY as the postinstall entry point.
39
+ *
40
+ * WHY THE GUARD: scripts/preflight.mjs step 4 does `await import()` on every shipped
41
+ * module to prove it resolves. The first version of this file ran main() and called
42
+ * process.exit(0) at import time — the import KILLED the preflight runner mid-step,
43
+ * exit code 0, summary never printed. A module that exits its importer is a landmine;
44
+ * only the direct `node install-path.mjs` invocation (npm's postinstall) may run it.
45
+ */
46
+ const isEntryPoint = (() => {
47
+ try {
48
+ return process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
49
+ } catch {
50
+ return false;
51
+ }
52
+ })();
53
+
54
+ if (isEntryPoint) {
55
+ try {
56
+ main();
57
+ } catch (e) {
58
+ // A postinstall that throws uninstalls the package. Report, swallow, succeed.
59
+ console.log(`gu-cli: PATH setup skipped (${e.message}) — see \`gu-cli doctor\` if \`gu\` is not found.`);
60
+ }
61
+ process.exit(0);
62
+ }
63
+
64
+ function main() {
65
+ if (platform() === "win32") return; // registry PATH + PowerShell alias: not here
66
+ if (process.env.CI) return; // no user shell to help in CI
67
+ const prefix = process.env.npm_config_prefix;
68
+ if (!prefix) return; // not an npm install context
69
+ const binDir = join(prefix, "bin");
70
+ if (resolve(binDir).startsWith(resolve(tmpdir()))) return; // preflight's throwaway prefix
71
+
72
+ const onPath = (process.env.PATH || "").split(":").some((p) => p === binDir);
73
+ if (onPath) return; // nothing to fix — most installs
74
+
75
+ const home = realUserHome();
76
+ const targets = rcFilesFor(home);
77
+ const block = `${BEGIN}\n# npm's global bin dir was not on PATH; added by @tiens.nguyen/gu-cli postinstall.\nexport PATH="${binDir}:$PATH"\n${END}\n`;
78
+
79
+ let wrote = null;
80
+ for (const rc of targets) {
81
+ if (ensureBlock(rc, block)) { wrote = wrote || rc; }
82
+ }
83
+
84
+ if (wrote) {
85
+ console.log(`gu-cli: added ${binDir} to PATH in ${wrote}.`);
86
+ console.log(`gu-cli: open a NEW terminal to use \`gu\`, or run this once in the current one:`);
87
+ console.log(` export PATH="${binDir}:$PATH"`);
88
+ } else if (targets.length === 0) {
89
+ console.log(`gu-cli: ${binDir} is not on your PATH and no shell profile was found to update.`);
90
+ console.log(` export PATH="${binDir}:$PATH"`);
91
+ }
92
+ }
93
+
94
+ /** The installer's home — the invoking user's, not root's, under sudo. */
95
+ function realUserHome() {
96
+ const sudoUser = process.env.SUDO_USER;
97
+ if (sudoUser && process.getuid && process.getuid() === 0) {
98
+ return platform() === "darwin" ? join("/Users", sudoUser) : join("/home", sudoUser);
99
+ }
100
+ return homedir();
101
+ }
102
+
103
+ /**
104
+ * Which rc files to maintain, in order. macOS default is zsh; Ubuntu default is bash.
105
+ * A non-default shell's rc is updated only if the file already exists — we fix the
106
+ * setups people HAVE, we don't seed shells they don't use.
107
+ */
108
+ function rcFilesFor(home) {
109
+ const zshrc = join(home, ".zshrc");
110
+ const bashrc = join(home, ".bashrc");
111
+ const out = [];
112
+ if (platform() === "darwin") {
113
+ out.push(zshrc);
114
+ if (existsSync(bashrc)) out.push(bashrc);
115
+ } else {
116
+ out.push(bashrc);
117
+ if (existsSync(zshrc)) out.push(zshrc);
118
+ }
119
+ return out;
120
+ }
121
+
122
+ /** Append the block unless the marker is already there. True when the file changed. */
123
+ function ensureBlock(rcFile, block) {
124
+ const prior = existsSync(rcFile) ? readFileSync(rcFile, "utf8") : "";
125
+ if (prior.includes(BEGIN)) return false;
126
+ if (!existsSync(rcFile)) writeFileSync(rcFile, block);
127
+ else appendFileSync(rcFile, (prior.endsWith("\n") || prior === "" ? "" : "\n") + block);
128
+ chownBackToSudoUser(rcFile);
129
+ return true;
130
+ }
131
+
132
+ /** `sudo npm i -g` would otherwise leave a root-owned .zshrc the user cannot edit. */
133
+ function chownBackToSudoUser(file) {
134
+ const { SUDO_UID, SUDO_GID } = process.env;
135
+ if (SUDO_UID && SUDO_GID && process.getuid && process.getuid() === 0) {
136
+ try { chownSync(file, Number(SUDO_UID), Number(SUDO_GID)); } catch { /* best effort */ }
137
+ }
138
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gu-cli",
3
- "version": "1.0.692",
3
+ "version": "1.0.693",
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",
@@ -18,6 +18,7 @@
18
18
  "gu": "gu-repl.mjs"
19
19
  },
20
20
  "scripts": {
21
+ "postinstall": "node install-path.mjs",
21
22
  "test": "python3 -m unittest discover tests -v && node --test",
22
23
  "test:js": "node --test",
23
24
  "test:keys": "node --test tests/key-choice-regression.test.mjs tests/coding-key-line.test.mjs tests/coding-key-picker.test.mjs tests/coding-key-prompt.test.mjs tests/coding-key-notice.test.mjs tests/coding-key-fallback-line.test.mjs tests/manage-coding-providers.test.mjs tests/add-coding-provider.test.mjs tests/coding-provider-setup.test.mjs",
@@ -78,6 +79,7 @@
78
79
  "model-doctor.mjs",
79
80
  "coding-key-prompt.mjs",
80
81
  "powershell-alias.mjs",
82
+ "install-path.mjs",
81
83
  "gonext_probe_agent.py",
82
84
  "gonext_agent_chat.py",
83
85
  "gonext_transcribe.py",