@tiens.nguyen/gu-cli 1.0.802 → 1.0.804

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 CHANGED
@@ -1,52 +1,150 @@
1
1
  # @tiens.nguyen/gu-cli
2
- Run:
3
- GONEXT_API_BASE=... GONEXT_WORKER_KEY=... npx -y --package @tiens.nguyen/gu-cli gu-cli
4
2
 
5
- ## Agent chat mode
3
+ **gu** is a terminal coding agent for the folder you are in, plus the local worker that runs
4
+ agent / OCR / PDF / embedding jobs on your machine (Ollama, MLX or any OpenAI-compatible server).
6
5
 
7
- Select **Agent** (instead of Chat) under the composer in the web app. Your
8
- free-form prompt is handed to a [smolagents](https://github.com/huggingface/smolagents)
9
- agent running on your local MLX/Ollama model. The agent can call tools (v1:
10
- `http_request`) and streams its thinking steps + final answer directly into the
11
- chat thread.
6
+ ```sh
7
+ npm install -g @tiens.nguyen/gu-cli
8
+ gu # open the REPL in the current folder
9
+ ```
12
10
 
13
- Requires smolagents in the worker's Python environment:
11
+ The package installs three commands: `gu` (the terminal REPL), `gu-cli` and `gu-local-worker`
12
+ (the same worker CLI). Requires Node.js 18 or newer. The install runs `fix-path` for you; if
13
+ npm's global bin folder is not on your PATH afterwards, run `gu-cli fix-path`.
14
+ On Windows, PowerShell's built-in `gu` alias (Get-Unique) shadows this CLI — run
15
+ `gu-cli fix-powershell` to drop it.
16
+
17
+ ## Quick start
18
+
19
+ 1. `gu login` — browser sign-in; writes `~/.gonext/worker.env`.
20
+ (Client Expert machines can instead paste a token from the Account page with `gu-cli token <token>`.)
21
+ 2. `gu doctor` — set up and check the models on this machine
22
+ (`gu doctor --client` installs only the agent dependencies when the models live on another machine).
23
+ 3. `gu` in a project folder, then type a request. Conversation history is saved per folder in
24
+ `~/.gonext/sessions` and resumed next time; `/new` starts a fresh one and archives the old one.
25
+
26
+ A first install asks how this machine runs (hosted client / MLX host / private). Pass
27
+ `--license <pro token>` to choose a pro license up front; without it a first install is private,
28
+ meaning the whole stack runs on this machine.
29
+
30
+ ## The `gu` REPL
14
31
 
15
- ```sh
16
- pip install smolagents certifi
17
32
  ```
33
+ gu [-v|--verbose] [--log] [--license <pro token>]
34
+ ```
35
+
36
+ - `-v, --verbose` shows RAG index-file activity in the bottom bar.
37
+ - `--log` logs this run in detail to `~/.gonext/worker.log`. Without it only starts, stops and errors are logged.
38
+ - Ctrl-C stops following a turn.
39
+
40
+ ### Three models, three commands
41
+
42
+ | Command | Model it switches |
43
+ | --- | --- |
44
+ | `/model` | the CODING model (the one that writes code), for this session |
45
+ | `/agent-model` | the AGENT model (the one that talks) — saved on this machine |
46
+ | `/embed-model` | the RAG EMBEDDING model (nomic-embed-text / qwen3-embedding) and its server; re-indexes what is already indexed; supports a remote URL + API key |
18
47
 
19
- - `certifi` supplies a trusted CA bundle so the agent's `http_request` tool can
20
- verify HTTPS certificates on macOS (where Python's default bundle may be
21
- missing). The worker falls back to the system bundle if certifi is absent.
22
- - Agent mode is blocked for cloud models (the API returns 400). Select a local
23
- MLX or Ollama model first.
24
- - Tool steps appear in the collapsible reasoning (`<think>`) area; the final
25
- answer is the message body — no new UI needed.
48
+ `/agent-model` saves the model's server URL (`agentModelUrl`) together with its name, so picking a
49
+ model from another provider (for example OpenAI) can no longer leave chat pointed at the old
50
+ server. After picking, `gu doctor` shows the URL and model that are actually in use.
26
51
 
27
- The agent script is `gonext_agent_chat.py` (reads `{messages, agentBaseURL,
28
- agentApiKey, agentModelId, tools, maxSteps}` on stdin; emits NDJSON
29
- `{"type":"step"/"final","text":"..."}` lines on stdout).
52
+ ### Slash commands
30
53
 
31
- ## API Check / HTTP probe (Tools & Agents modes)
54
+ | Command | What it does |
55
+ | --- | --- |
56
+ | `/model`, `/agent-model`, `/embed-model` | switch models (see above) |
57
+ | `/context-window [tokens\|auto]` | tell gu how big the coding model's context is (hosted models cannot be probed) |
58
+ | `/doctor` | check every configured model answers, especially the embedder |
59
+ | `/tokens` | break down the last prompt sent to the coding model — how much of it was RAG |
60
+ | `/prices [update]` | price and cache tables, and what the last turn and this session cost |
61
+ | `/rag` | where this folder's RAG knowledge base lives: local, cloud, or both |
62
+ | `/s3 [test]` | set up your own S3 bucket (RAG index, generated PDFs/downloads) and check the AWS keys |
63
+ | `/proxy [show\|set\|on\|off\|clear\|test]` | route model calls through an HTTP proxy (local calls always go direct) |
64
+ | `/test-auto` | toggle auto-test (verify and fix until it passes) for this folder |
65
+ | `/undo [runId]` | put back the files the last stopped turn changed, from its backups (asks first) |
66
+ | `/server` | pick a deployment server (host/user) for this session |
67
+ | `/host-mode` | undo client mode — run agent turns on this machine again |
68
+ | `/save-settings [name\|path]`, `/load-settings [name\|path]` | move providers, models and key names (no secrets) to another machine |
69
+ | `/save-preset [name]`, `/open-presets [name]`, `/use-presets [name]` | presets: your whole setup, keys included, to copy to another computer, edit, and apply (backs up first) |
70
+ | `/hooks [list\|approve\|unapprove]` | commands gu runs on onStart, onAsk, onToolDone, onTurnEnd |
71
+ | `/mcp [list\|tools\|add\|remove\|enable\|disable\|approve\|allow\|disallow\|reconnect]` | manage MCP servers and which of their tools run without asking |
72
+ | `/switch-skill [claude\|gu]` | use Claude Code's (`.claude`) or gu's own (`.gonext`) skills, MCP servers and hooks for this folder |
73
+ | `/clear-logs` | show what logs, RAG indexes and old API versions take on disk, then ask what to clear |
74
+ | `/new` | start a new conversation in this folder (the old one is archived) |
75
+ | `/help`, `/exit` (`/quit`) | list commands / quit |
32
76
 
33
- The worker can run Postman-style HTTP probes queued from the web app
34
- ("Tools" = `tool_only`, "Agents" = `agentic`). The worker always performs the
35
- actual HTTP request itself (Node `fetch`, so TLS works), measures the
36
- status/latency/headers/body, and classifies it (2xx/3xx/4xx/5xx/timeout/
37
- network_error).
77
+ Each skill in `~/.gonext/skills/<name>/SKILL.md` (and the folder's `.gonext/skills`) is also a
78
+ slash command of its own. Run `/help` in the REPL for the list your version offers.
38
79
 
39
- - **Tools (`tool_only`)** — no extra setup. The selected local model writes a
40
- one-line health summary of the measured result.
41
- - **Agents (`agentic`)** — a smolagents agent (running on the selected local
42
- model) produces the summary. Requires `pip install smolagents`.
80
+ ## Shell commands
81
+
82
+ ```
83
+ gu status worker + a table of the running model servers
84
+ gu start | stop | restart run the polling daemon in the background (stop --models: model servers too)
85
+ gu logs [--lines N] [-f] tail the daemon log; gu logs <port|pid|chat|embed> [-f] tails a model server's log
86
+ gu doctor [--client] set up models here
87
+ gu rag test the embedder end-to-end and list knowledge bases
88
+ gu rag plan [path] is RAG worth it here? files / chars / chunks -> estimated index time
89
+ gu rag --test can this machine embed and retrieve?
90
+ gu api <start|stop|restart|status|upgrade> the local API (Local / Client Expert only)
91
+ gu workspace revert [runId] undo an agent run's file edits from its backups
92
+ gu test-proxy can this machine route model calls through a proxy?
93
+ gu mongo migrate-from-docker macOS: move a Docker MongoDB to native (Homebrew)
94
+ gu clear [--scratch|--keep-history] [--yes] back to a fresh machine (asks first)
95
+ gu fix-path | fix-powershell repair PATH / drop PowerShell's `gu` alias
96
+ ```
43
97
 
44
- The agent talks to your local MLX OpenAI-compatible server (no cloud calls).
45
- The agent only summarizes; the worker's measurement stays the source of truth,
46
- so if smolagents or the model is unavailable the probe still returns the
47
- measured result with a note.
98
+ `gu-cli` accepts the same commands, plus `login [--api-base <url>]`, `set <workerKey> [--api-base <url>]`,
99
+ `token [<token>]`, `ws-ping-test` and `simulate-chat [text...]`.
100
+ `gu api restart` runs the installed version; an upgrade changes the files, not a process that is already serving.
48
101
 
49
- ### Env vars
102
+ ## Files and settings
103
+
104
+ | Path | Contents |
105
+ | --- | --- |
106
+ | `~/.gonext/worker.env` | sign-in: worker key and API base |
107
+ | `~/.gonext/sessions/` | per-folder conversation history (`archive/` holds old conversations) |
108
+ | `~/.gonext/worker.log` | worker log (detailed with `--log`) |
109
+ | `~/.gonext/local-api.log` | the local API's log |
110
+ | `~/.gonext/mcp.json` | MCP servers and their allow rules |
111
+ | `~/.gonext/skills/`, `.gonext/skills` | skills (global / this folder) |
112
+
113
+ ## Worker modes
114
+
115
+ The background worker polls the API for jobs queued from the web app and runs them on this machine.
116
+
117
+ ### Agent chat
118
+
119
+ Choosing **Agent** under the composer in the web app hands your prompt to a
120
+ [smolagents](https://github.com/huggingface/smolagents) agent running on your local model. It
121
+ needs `pip install smolagents certifi` in the worker's Python environment (`certifi` gives
122
+ HTTPS a trusted CA bundle on macOS). Agent mode is blocked for cloud models; pick a local
123
+ MLX or Ollama model first. The script is `gonext_agent_chat.py`.
124
+
125
+ ### API check / HTTP probe
126
+
127
+ - **Tools (`tool_only`)**: the worker runs the HTTP request itself, measures status, latency, headers and
128
+ body, classifies it (2xx/3xx/4xx/5xx/timeout/network_error), and the local model writes a one-line summary.
129
+ - **Agents (`agentic`)**: a smolagents agent writes the summary; the measurement stays the source of truth.
130
+
131
+ ### Environment variables
132
+
133
+ | Variable | Meaning |
134
+ | --- | --- |
135
+ | `GONEXT_API_BASE`, `GONEXT_WORKER_KEY` | API URL and worker key (normally from `~/.gonext/worker.env`) |
136
+ | `GONEXT_PROBE_PYTHON` | Python for smolagents scripts (default: `GONEXT_MLX_LM_PYTHON` or `python3`) |
137
+ | `GONEXT_MLX_LM_PYTHON` | Python for the MLX LM native probe (default `python3`) |
138
+ | `GONEXT_OCR_MODEL_PATH` | local GLM-OCR-bf16 directory (default `~/mlx-models/GLM-OCR-bf16`) |
139
+ | `GONEXT_OCR_DEBUG=1` | print raw OCR stdout/stderr snippets |
140
+ | `GONEXT_SIMULATE_TEXT` | default reply body for `simulate-chat` |
141
+
142
+ ## Development
143
+
144
+ ```sh
145
+ npm test # python unittest + node --test
146
+ npm run preflight # checks before a release
147
+ npm run publish:org # npm publish --access public
148
+ ```
50
149
 
51
- GONEXT_PROBE_PYTHON Python executable for smolagents scripts
52
- (default: GONEXT_MLX_LM_PYTHON or python3)
150
+ Source and issues: https://github.com/tiennsloit/gu
package/gu-repl.mjs CHANGED
@@ -111,6 +111,7 @@ import {
111
111
  bulletKind,
112
112
  workspaceScopeWarning,
113
113
  isLocalApiBase,
114
+ apiVersionSuffix,
114
115
  modeRow,
115
116
  nothingToAnswerWith,
116
117
  NO_MODEL_LINES,
@@ -7800,13 +7801,49 @@ async function runLoadSettingsCommand(line) {
7800
7801
  */
7801
7802
  async function applyTransferDoc(value, opts = {}) {
7802
7803
  const pa = await import("./preset-apply.mjs");
7803
- return pa.applyTransfer(value, {
7804
+ const result = await pa.applyTransfer(value, {
7804
7805
  ...opts,
7805
7806
  api: replPresetApi(),
7806
7807
  io: replPresetIo,
7807
7808
  backupDir: join(homedir(), ".gonext", "preset-backups"),
7808
7809
  host: hostname(),
7809
7810
  });
7811
+ if (result?.applied) await refreshAfterPresetApply(value, opts.codingDefault, result);
7812
+ return result;
7813
+ }
7814
+
7815
+ /**
7816
+ * AFTER A PRESET / SETTINGS FILE LANDS, bring the running session in line with it (task #21).
7817
+ * The settings were written, but the REPL kept what it read at startup: the old default coder
7818
+ * (defaultCodingModel/Url), the folder's stored /model pick (which outranks the default, so the
7819
+ * bar and /model kept showing the old coder), and the "no embed model" / "no agent model"
7820
+ * warnings. /model already does this after a change; applying a preset must too.
7821
+ */
7822
+ async function refreshAfterPresetApply(value, codingDefault, result) {
7823
+ try {
7824
+ // The preset's coder IS the account default now — a folder pick would shadow it.
7825
+ if (result.coder && codingDefault?.model) {
7826
+ sessionCodingModel = "";
7827
+ sessionCodingLabel = "";
7828
+ sessionCodingUrl = "";
7829
+ await saveSessionCoding(resolve(process.cwd()));
7830
+ defaultCodingModel = codingDefault.model;
7831
+ defaultCodingUrl = codingDefault.url;
7832
+ }
7833
+ // What the API now answers with is authoritative — read it back rather than assume.
7834
+ const probe = await fetchAgentPayload([{ role: "user", content: "ping" }]).catch(() => null);
7835
+ const p = probe?.payload ?? {};
7836
+ if (p.codingModelId) defaultCodingModel = p.codingModelId;
7837
+ if (p.codingBaseURL) defaultCodingUrl = p.codingBaseURL;
7838
+ activeCoderWhere = whereOfActiveCoder(probe?.codingChoices ?? p.codingChoices, sessionCodingModel);
7839
+ if (value?.agentModel?.name || p.agentMissing === false) agentMissing = false;
7840
+ else if (p.agentMissing === true) agentMissing = true;
7841
+ if (probe?.payload) noModelAtAll = nothingToAnswerWith(p);
7842
+ await syncEmbedWarning({ repair: true });
7843
+ if (barPinned()) paintBar();
7844
+ } catch {
7845
+ /* a failed refresh must never undo a preset that applied — the next launch re-reads everything */
7846
+ }
7810
7847
  }
7811
7848
 
7812
7849
  /** The .json files directly in a folder, with enough of each to recognise a settings file. */
@@ -8583,6 +8620,18 @@ async function chooseAgentModel() {
8583
8620
  console.log(dim(` already using ${current} — nothing changed.\n`));
8584
8621
  return;
8585
8622
  }
8623
+ if (rows[idx].missing) {
8624
+ // The one row agentModelRows injects WITHOUT the host listing it (#108): picking it re-affirms
8625
+ // a name the server no longer has, which fails later as a confused agent, not here. The user
8626
+ // can still insist — it may be a list outage — but the consequence is said BEFORE the write.
8627
+ console.log(GUTTER + yellow(`${chosen} is not on ${listHost || "the server"} any more — it may have been removed there.`));
8628
+ console.log(GUTTER + yellow("As the agent model it would fail on your next question, not now."));
8629
+ const go = await pickFromList([`use it anyway`, `pick something else`], 1);
8630
+ if (go !== 0) {
8631
+ console.log(dim(" (kept)\n"));
8632
+ return;
8633
+ }
8634
+ }
8586
8635
  if (rows[idx].embedder) {
8587
8636
  // Not a refusal — it is their box and their call — but the consequence has to be said BEFORE
8588
8637
  // the write, because an embedder as the chat model does not fail here. It fails as an agent
@@ -8597,9 +8646,17 @@ async function chooseAgentModel() {
8597
8646
  }
8598
8647
 
8599
8648
  const md = await import("./model-doctor.mjs");
8600
- // BOTH FIELDS. With a direct agentModelUrl set — the Client Expert shape — the API reads
8601
- // agentModelName; agentModel is the legacy mirror. Writing one leaves the two disagreeing.
8649
+ const ps2 = await import("./coding-provider-setup.mjs");
8650
+ // THREE FIELDS — NAME **AND** SERVER (#108). This picker once wrote only agentModelName +
8651
+ // agentModel, leaving agentModelUrl wherever it was: choosing an OpenAI model while the stored
8652
+ // URL still said the shared Ollama box meant every turn went to that (dead) host and died as an
8653
+ // nginx 502, with the valid OpenAI key never touched — the key is chosen by MATCHING the URL.
8654
+ // The URL saved is the one this very list came from (agentListSource reads settings.agentModelUrl),
8655
+ // normalised to the /v1 root so it matches the provider row app.ts resolves the key against —
8656
+ // the same shape the add-server flow's saveAgentModel stores. Name and server can no longer diverge.
8657
+ const agentUrl = ps2.ensureV1Root(String(settings?.agentModelUrl ?? source.url ?? ""));
8602
8658
  const saved = await md.saveWorkerSettings(apiBase, workerKey, {
8659
+ agentModelUrl: agentUrl,
8603
8660
  agentModelName: chosen,
8604
8661
  agentModel: chosen,
8605
8662
  });
@@ -8611,7 +8668,7 @@ async function chooseAgentModel() {
8611
8668
  console.log(GUTTER + dim("`gu api restart` to pick up the current version.\n"));
8612
8669
  return;
8613
8670
  }
8614
- console.log(accent(` ✓ agent model → ${chosen}`));
8671
+ console.log(accent(` ✓ agent model → ${chosen}`) + (listHost ? dim(` · ${listHost}`) : ""));
8615
8672
  console.log(dim(" takes effect on your next question.\n"));
8616
8673
  }
8617
8674
 
@@ -12795,8 +12852,21 @@ async function main() {
12795
12852
  agentUrl: p.agentBaseURL,
12796
12853
  servers: await (await import("./model-doctor.mjs")).discoverModelServers(),
12797
12854
  });
12855
+ // Task #18: in client expert / private mode the API is a SEPARATE build running on this
12856
+ // machine — `gu-cli api upgrade` moves it without touching the CLI, so `v<cli>` alone
12857
+ // does not say what is answering. Ask its /api/health for the running version and show
12858
+ // both. Best-effort and only in that mode: a client machine's API is the hosted service,
12859
+ // whose version the user cannot act on, and an unreachable local API must not hold up or
12860
+ // break the banner.
12861
+ let apiBannerVersion = "";
12862
+ if (isClientExpert()) {
12863
+ try {
12864
+ const ceApi = await import("./client-expert-api.mjs");
12865
+ apiBannerVersion = String((await ceApi.probeLocalApi()).body?.version ?? "").trim();
12866
+ } catch { /* absent beats guessed — no suffix */ }
12867
+ }
12798
12868
  const statusRows = [
12799
- bold(accent("GU AGENT")) + dim(` v${REPL_VERSION}`),
12869
+ bold(accent("GU AGENT")) + dim(` v${REPL_VERSION}${apiVersionSuffix(apiBannerVersion)}`),
12800
12870
  // Task #134: a client machine OVERRIDES the account's agent model locally (the
12801
12871
  // account value points at the model host's own server). Show what will actually be
12802
12872
  // used, not a value this machine can't reach.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gu-cli",
3
- "version": "1.0.802",
3
+ "version": "1.0.804",
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",
@@ -2320,6 +2320,17 @@ export function isLocalApiBase(apiBase) {
2320
2320
  * entire distinction — go unmentioned. Reported live on a fully set-up Ubuntu box that said
2321
2321
  * plain "client".
2322
2322
  */
2323
+ /**
2324
+ * The banner's API-version suffix for client expert / private mode (task #18). There the API
2325
+ * runs on THIS machine and its build can differ from the CLI's — an `api upgrade` without a gu
2326
+ * update is the common way — so the banner shows both, next to `v<cli>`. An empty version means
2327
+ * the running build did not report one (or is unreachable), and absent beats guessed: no suffix.
2328
+ */
2329
+ export function apiVersionSuffix(version) {
2330
+ const v = String(version ?? "").trim();
2331
+ return v ? ` · api v${v}` : "";
2332
+ }
2333
+
2323
2334
  export function modeRow({ clientExpert, clientMode, modelUrls = [], coderWhere }) {
2324
2335
  if (clientExpert) {
2325
2336
  // "models remote" WAS A CONSTANT (task #333): a box running its own Ollama on 127.0.0.1 —