@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 +136 -38
- package/gu-repl.mjs +75 -5
- package/package.json +1 -1
- package/terminal-layout.mjs +11 -0
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
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
28
|
-
agentApiKey, agentModelId, tools, maxSteps}` on stdin; emits NDJSON
|
|
29
|
-
`{"type":"step"/"final","text":"..."}` lines on stdout).
|
|
52
|
+
### Slash commands
|
|
30
53
|
|
|
31
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8601
|
-
//
|
|
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.
|
|
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",
|
package/terminal-layout.mjs
CHANGED
|
@@ -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 —
|