@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/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<h1 align="center">gu — the coding agent for your own models</h1>
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
|
-
<strong>
|
|
5
|
-
|
|
6
|
-
<strong>Free
|
|
4
|
+
<strong>Describe the change. gu edits your repo, runs the tests and shows you the diff, with Ollama or any OpenAI-compatible API.</strong><br>
|
|
5
|
+
Your prompts and code go to the model you choose, not to gu: its API and database run on your computer.<br>
|
|
6
|
+
<strong>Free for 30 days: no account, no sign-in, no card.</strong>
|
|
7
7
|
</p>
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
<a href="https://gucli.com">Website</a> ·
|
|
19
19
|
<a href="https://gucli.com/start">Docs</a> ·
|
|
20
20
|
<a href="https://gucli.com/pricing">Pricing</a> ·
|
|
21
|
-
<a href="https://gucli.com/support">Support</a>
|
|
21
|
+
<a href="https://gucli.com/support">Support</a><br>
|
|
22
|
+
<sub>The website is also in Tiếng Việt, 中文 and 日本語.</sub>
|
|
22
23
|
</p>
|
|
23
24
|
|
|
24
25
|
<p align="center">
|
|
@@ -40,8 +41,13 @@ gu
|
|
|
40
41
|
computer. Press Enter to use it, pick a model provider, then type what you want done.
|
|
41
42
|
|
|
42
43
|
A free install is **private**: gu's API and its database (MongoDB) run on this computer, next to
|
|
43
|
-
your code. The first run shows what it will
|
|
44
|
-
Needs **Node.js 20.19+**.
|
|
44
|
+
your code, and your prompts and code go to the model you pick, not to gu. The first run shows what it will
|
|
45
|
+
install and asks before installing anything. Needs **Node.js 20.19+**.
|
|
46
|
+
|
|
47
|
+
**Local or hosted: you choose once, at the first run.** Free installs run in *Local mode* (above).
|
|
48
|
+
With a Pro token, gu uses gu's hosted API and models instead. To switch later, run `gu clear` and
|
|
49
|
+
set gu up again. In Local mode `gu clear` also deletes this computer's conversations (it asks
|
|
50
|
+
first); `gu clear --keep-history` keeps them.
|
|
45
51
|
|
|
46
52
|
> **`gu: command not found`?** Run `npx @tiens.nguyen/gu-cli fix-path`. See [Troubleshooting](#troubleshooting).
|
|
47
53
|
|
|
@@ -65,11 +71,15 @@ Needs **Node.js 20.19+**.
|
|
|
65
71
|
## Is gu for you?
|
|
66
72
|
|
|
67
73
|
**A good fit if** you run (or want to run) your own models, can't send code to someone else's
|
|
68
|
-
cloud, work on remote Linux boxes over SSH, or want a coding agent that
|
|
69
|
-
|
|
74
|
+
cloud, work on remote Linux boxes over SSH, or want a terminal coding agent that is free with
|
|
75
|
+
your own models and $4 a month with ours.
|
|
70
76
|
|
|
71
77
|
**Want a frontier model?** gu runs those too, through any OpenAI-compatible API, and keeps
|
|
72
|
-
everything else: the edits in your repo, the tests, the browser check and `/undo`.
|
|
78
|
+
everything else: the edits in your repo, the tests, the browser check and `/undo`. (Your code then
|
|
79
|
+
goes to that provider, as with any tool that uses it.)
|
|
80
|
+
|
|
81
|
+
**Not a fit if** you want one click and no setup: gu asks you to pick a model, and it works best
|
|
82
|
+
with a strong one.
|
|
73
83
|
|
|
74
84
|
## Quick start
|
|
75
85
|
|
|
@@ -89,8 +99,8 @@ gu uses up to three models, each with its own command:
|
|
|
89
99
|
| `/agent-model` | **agent**: talks to you and plans | this machine |
|
|
90
100
|
| `/embed-model` | **embedding**: RAG (`nomic-embed-text`, `qwen3-embedding`), local or remote | this machine |
|
|
91
101
|
|
|
92
|
-
Run `/doctor` any time to check that each one answers.
|
|
93
|
-
|
|
102
|
+
Run `/doctor` any time to check that each one answers. Pick a strong model for coding: small
|
|
103
|
+
local models often fail at multi-step changes.
|
|
94
104
|
|
|
95
105
|
## Using gu
|
|
96
106
|
|
|
@@ -167,7 +177,7 @@ gu doctor set up and check models on this machine
|
|
|
167
177
|
gu rag [plan [path] | --test] test the embedder, estimate index time
|
|
168
178
|
gu workspace revert [runId] undo an agent run's file edits from its backups
|
|
169
179
|
gu support send a support request with masked logs
|
|
170
|
-
gu clear [--
|
|
180
|
+
gu clear [--keep-history] back to a fresh machine (asks first)
|
|
171
181
|
gu fix-path | fix-powershell repair PATH / remove PowerShell's `gu` alias
|
|
172
182
|
```
|
|
173
183
|
|
|
@@ -188,6 +198,7 @@ gu fix-path | fix-powershell repair PATH / remove PowerShell's `gu` alias
|
|
|
188
198
|
| --- | --- | --- |
|
|
189
199
|
| **Free trial** | 30 days, from the first run | nothing: the token is created for you |
|
|
190
200
|
| **Free plan** | 3 more months on that computer | a free account at [gucli.com](https://gucli.com) |
|
|
201
|
+
| **After that** | gu asks for a token | Pro, or a voucher for more free days |
|
|
191
202
|
| **Pro** | **$4 a month** or **$40 a year** (+ VAT where it applies) | hosted models (no model server to run), any number of computers, provider budgets |
|
|
192
203
|
|
|
193
204
|
On Pro, the hosted models and the hosted API run on gu's servers, so the prompts and code you
|
|
@@ -206,6 +217,11 @@ so the step that adds npm's bin folder to your PATH may not have run. Run
|
|
|
206
217
|
`npx @tiens.nguyen/gu-cli fix-path`. It also finds what an old install left behind (broken links,
|
|
207
218
|
a second gu from another npm, a stale PATH line) and asks before fixing each one. `--check` only reports.
|
|
208
219
|
|
|
220
|
+
**`EACCES` / permission denied during install (macOS, Linux).** npm's global folder belongs to
|
|
221
|
+
root. No sudo needed: run `npm config set prefix ~/.npm-global`, then install again. gu adds that
|
|
222
|
+
folder to your PATH; open a new terminal. With nvm, skip that (nvm refuses an npm prefix):
|
|
223
|
+
`nvm use` a Node 20.19 or newer and install again.
|
|
224
|
+
|
|
209
225
|
**Windows: `gu` runs `Get-Unique`.** PowerShell has a built-in `gu` alias. Run `gu-cli fix-powershell`.
|
|
210
226
|
|
|
211
227
|
**Node version.** gu needs **Node.js 20.19+** (a Pro install using only hosted models runs on 18+).
|
package/agent-model-command.mjs
CHANGED
|
@@ -119,6 +119,11 @@ export function agentModelRows(models, current) {
|
|
|
119
119
|
* model is always named, because that is the one fact still true when the network is not.
|
|
120
120
|
*/
|
|
121
121
|
export function describeTagsFailure({ url, current, error, proxied = false } = {}) {
|
|
122
|
+
// NOTHING CONFIGURED IS NOT A FAILURE (GH #397). There was no list to fetch, so "couldn't fetch
|
|
123
|
+
// the model list from (no agent model URL configured)", its echo and a /proxy hint were three
|
|
124
|
+
// ways of saying one thing — and the proxy cannot help with a URL that does not exist. Same
|
|
125
|
+
// sentence /model uses for the same state.
|
|
126
|
+
if (!url && !current) return ["No agent model configured."];
|
|
122
127
|
const lines = [
|
|
123
128
|
`couldn't fetch the model list from ${url || "(no agent model URL configured)"}`,
|
|
124
129
|
];
|
|
@@ -355,6 +360,16 @@ export function agentRecoveryOptions({ keyProblem = false, host = "" } = {}) {
|
|
|
355
360
|
];
|
|
356
361
|
}
|
|
357
362
|
|
|
363
|
+
/**
|
|
364
|
+
* Which recovery row the cursor starts on (GH #397). A key problem → the key row. NOTHING SET UP →
|
|
365
|
+
* "add a provider": the only way forward there, and what /model already highlights for its own
|
|
366
|
+
* "No coding model configured." Otherwise "leave it", so Enter on an unreachable box changes nothing.
|
|
367
|
+
*/
|
|
368
|
+
export function agentRecoveryDefault(options, { keyProblem = false, configured = true } = {}) {
|
|
369
|
+
const want = keyProblem ? "key" : configured ? "leave" : "provider";
|
|
370
|
+
return Math.max(0, options.findIndex((o) => o.action === want));
|
|
371
|
+
}
|
|
372
|
+
|
|
358
373
|
// ---------------------------------------------------------------------------------------------
|
|
359
374
|
// #44 — WHAT TO CHANGE comes first.
|
|
360
375
|
// ---------------------------------------------------------------------------------------------
|
package/agent-model-label.mjs
CHANGED
|
@@ -153,7 +153,7 @@ export function sameModelName(a, b) {
|
|
|
153
153
|
/** Plain-text fallback for each reason we cannot name the model. Never a sentinel. */
|
|
154
154
|
export const UNKNOWN_MODEL_TEXT = Object.freeze({
|
|
155
155
|
remote: "(named by the remote server)",
|
|
156
|
-
"not-set": "
|
|
156
|
+
"not-set": "same as the coding model",
|
|
157
157
|
"no-server": "(no model server running)",
|
|
158
158
|
ambiguous: "(the loaded model)",
|
|
159
159
|
});
|
package/client-expert-run.mjs
CHANGED
|
@@ -20,8 +20,9 @@ import { homedir } from "node:os";
|
|
|
20
20
|
import { join } from "node:path";
|
|
21
21
|
import { promisify } from "node:util";
|
|
22
22
|
import { createHash } from "node:crypto";
|
|
23
|
-
import { localModeNodeProblem } from "./node-version.mjs";
|
|
23
|
+
import { localModeNodeProblem, nvmNodeVersions } from "./node-version.mjs";
|
|
24
24
|
import { downloadMongodExe, startPortableMongod, portableMongoInstalled, mongoWinPaths, mongodCommandLine, MONGO_WIN_VERSION, mongodRuns, isDllNotFoundExit, installVcRuntime, VC_REDIST_URL } from "./mongo-windows.mjs";
|
|
25
|
+
import { macosMajor, macNeedsOwnMongo, mongoMacPaths, macMongoInstalled, downloadMongodMac, mongodRunsMac, MONGO_MAC_OLD_VERSION, MONGO_MAC_BREW_MIN_MAJOR } from "./mongo-mac.mjs";
|
|
25
26
|
import {
|
|
26
27
|
apiDepsInstalled,
|
|
27
28
|
installApiDeps,
|
|
@@ -248,11 +249,17 @@ export async function probeMongo() {
|
|
|
248
249
|
// saying ("you already have mongosh") and because `verify` runs through it.
|
|
249
250
|
const serverPath = (await tryRun("sh", ["-c", "command -v mongod || true"])) || "";
|
|
250
251
|
const shellPath = (await tryRun("sh", ["-c", "command -v mongosh || true"])) || "";
|
|
252
|
+
// gu's own mongod on macOS 13 and older (GH #403) is not on PATH; `portable` says to start that one.
|
|
253
|
+
const portable = process.platform === "darwin" && (await macMongoInstalled());
|
|
254
|
+
// On those Macs Homebrew's mongod cannot run, so it does not count as installed: "it is not
|
|
255
|
+
// running" would send the reader to a server that can never start.
|
|
256
|
+
const brewUsable = !(process.platform === "darwin" && macNeedsOwnMongo(macosMajor()));
|
|
251
257
|
return {
|
|
252
258
|
running: false,
|
|
253
259
|
how: null,
|
|
254
|
-
installed: Boolean(serverPath.trim()),
|
|
260
|
+
installed: (brewUsable && Boolean(serverPath.trim())) || portable,
|
|
255
261
|
shellInstalled: Boolean(shellPath.trim()),
|
|
262
|
+
portable,
|
|
256
263
|
};
|
|
257
264
|
}
|
|
258
265
|
|
|
@@ -298,7 +305,12 @@ export async function probeDockerForMongo(name = MONGO_DOCKER_NAME) {
|
|
|
298
305
|
* macOS: make a native MongoDB answer on 27017 (task #304). → { ok, explained }.
|
|
299
306
|
* `explained` = it printed its own guidance, so the caller adds none.
|
|
300
307
|
*/
|
|
301
|
-
export async function ensureNativeMongoMac(ask, mongo, { log = (l) => console.log(l), brewHasTrust = null } = {}) {
|
|
308
|
+
export async function ensureNativeMongoMac(ask, mongo, { log = (l) => console.log(l), brewHasTrust = null, osMajor = macosMajor, ownMongo = ensureOwnMongoMac } = {}) {
|
|
309
|
+
// macOS 13 and older: Homebrew's MongoDB installs and then cannot run there (GH #403), so gu
|
|
310
|
+
// runs its own. A Mac that already has gu's own mongod keeps using it after a macOS upgrade.
|
|
311
|
+
if (!mongo.silentPort && (mongo.portable || macNeedsOwnMongo(osMajor()))) {
|
|
312
|
+
return ownMongo(ask, mongo, { log });
|
|
313
|
+
}
|
|
302
314
|
const mn = await import("./mongo-native.mjs");
|
|
303
315
|
brewHasTrust ??= mn.brewHasTrust;
|
|
304
316
|
const brew = mn.findBrew();
|
|
@@ -351,6 +363,71 @@ export async function ensureNativeMongoMac(ask, mongo, { log = (l) => console.lo
|
|
|
351
363
|
return { ok: true, explained: true };
|
|
352
364
|
}
|
|
353
365
|
|
|
366
|
+
/**
|
|
367
|
+
* macOS 13 and older (GH #403): gu's own mongod 8.0.5 in ~/.gonext/mongodb, run on 127.0.0.1 —
|
|
368
|
+
* no Homebrew, no service, no sudo. Shown, asked, then done, like the other platforms.
|
|
369
|
+
* Not started at login: gu's start-up check (preflightLocalMongo) starts it again when needed.
|
|
370
|
+
*/
|
|
371
|
+
export async function ensureOwnMongoMac(ask, mongo, {
|
|
372
|
+
log = (l) => console.log(l),
|
|
373
|
+
osMajor = macosMajor,
|
|
374
|
+
paths = mongoMacPaths(),
|
|
375
|
+
download = downloadMongodMac,
|
|
376
|
+
runs = (p) => mongodRunsMac(p.exe),
|
|
377
|
+
start = startPortableMongod,
|
|
378
|
+
waitFor = null,
|
|
379
|
+
} = {}) {
|
|
380
|
+
const installing = !mongo.portable;
|
|
381
|
+
const { host, port } = mongoHostPort(LOCAL_MONGO_URI);
|
|
382
|
+
if (installing) {
|
|
383
|
+
const major = osMajor();
|
|
384
|
+
log(dim(`\n Homebrew's MongoDB needs macOS ${MONGO_MAC_BREW_MIN_MAJOR} or newer${major ? ` (this Mac has macOS ${major})` : ""}, so gu runs`));
|
|
385
|
+
log(dim(` its own MongoDB ${MONGO_MAC_OLD_VERSION} instead — no Homebrew, no service, no password:\n`));
|
|
386
|
+
log(` download MongoDB ${MONGO_MAC_OLD_VERSION} (about 85 MB) into ${paths.dir}`);
|
|
387
|
+
} else {
|
|
388
|
+
log(dim("\n gu's MongoDB is installed but not running. This starts it:\n"));
|
|
389
|
+
}
|
|
390
|
+
log(" " + mongodCommandLine({ port, paths }));
|
|
391
|
+
log("");
|
|
392
|
+
const yes = await readYesNo(ask, ` ${installing ? "Install" : "Start"} MongoDB now? [Y/n]: `, true, log);
|
|
393
|
+
if (!yes) {
|
|
394
|
+
log(dim(`\n Not ${installing ? "installed" : "started"} — run the commands above yourself.`));
|
|
395
|
+
return { ok: false, explained: true };
|
|
396
|
+
}
|
|
397
|
+
log("\n " + bold(installing ? "Installing MongoDB" : "Starting MongoDB") + dim(installing ? " about 85 MB" : ""));
|
|
398
|
+
if (installing) {
|
|
399
|
+
try {
|
|
400
|
+
await download({ paths, log });
|
|
401
|
+
} catch (e) {
|
|
402
|
+
log("\n " + red("✗ ") + `could not download MongoDB: ${e?.message ?? e}`);
|
|
403
|
+
return { ok: false, explained: true };
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const probe = await runs(paths);
|
|
407
|
+
if (!probe.ok) {
|
|
408
|
+
log("\n " + red("✗ ") + `MongoDB cannot run on this Mac: ${probe.why}`);
|
|
409
|
+
return { ok: false, explained: true };
|
|
410
|
+
}
|
|
411
|
+
let started;
|
|
412
|
+
try {
|
|
413
|
+
started = await start({ port, paths });
|
|
414
|
+
} catch (e) {
|
|
415
|
+
started = { ok: false, error: e };
|
|
416
|
+
}
|
|
417
|
+
if (!started?.ok) {
|
|
418
|
+
log("\n " + red("✗ ") + `MongoDB could not be started${started?.error ? `: ${started.error.message ?? started.error}` : ""}`);
|
|
419
|
+
return { ok: false, explained: true };
|
|
420
|
+
}
|
|
421
|
+
const wait = waitFor ?? (await import("./mongo-native.mjs")).waitForMongo;
|
|
422
|
+
if (!(await wait(host, port, { timeoutMs: 60000 }))) {
|
|
423
|
+
log("\n " + red("✗ ") + `MongoDB was ${installing ? "installed" : "started"} but is not answering at ${host}:${port} yet.`);
|
|
424
|
+
log(dim(` Its log: ${paths.log} — then run gu again.`));
|
|
425
|
+
return { ok: false, explained: true };
|
|
426
|
+
}
|
|
427
|
+
log(" " + cyan("✓ MongoDB is running") + dim(` at ${host}:${port} (started by gu — data in ${paths.data})`));
|
|
428
|
+
return { ok: true, explained: true };
|
|
429
|
+
}
|
|
430
|
+
|
|
354
431
|
/** /etc/os-release → { id, codename, versionId }, or null where there is none (not Linux). */
|
|
355
432
|
export function readOsRelease({ read = (f) => readFileSync(f, "utf8") } = {}) {
|
|
356
433
|
let text = "";
|
|
@@ -622,7 +699,8 @@ export async function preflightLocalMongo({
|
|
|
622
699
|
log("\n " + red("gu: MongoDB is not answering") + dim(` at ${host}:${port}`) +
|
|
623
700
|
red(mongo.installed || mongo.silentPort || !local ? "." : " — it is not installed on this machine."));
|
|
624
701
|
log(dim(" Local mode keeps your conversations, models and keys in that database, so"));
|
|
625
|
-
|
|
702
|
+
// Nothing here checks the API, so say nothing about it (it was "fine" with no API process, GH #406).
|
|
703
|
+
log(dim(" nothing can start without it. gu starts the API once the database answers."));
|
|
626
704
|
if (!local) {
|
|
627
705
|
log(dim(`\n That database is on another machine (${host}). Start it there, then run gu again.\n`));
|
|
628
706
|
return { ok: false, fixed: false, installed: false };
|
|
@@ -1132,7 +1210,7 @@ export async function runClientExpertSetup({
|
|
|
1132
1210
|
// BEFORE ANYTHING IS INSTALLED (task #333): the API needs a newer Node than gu does. Not part
|
|
1133
1211
|
// of the gate on purpose — a gate "no" makes the first run settle on Cloud mode for good,
|
|
1134
1212
|
// while the fix here is a Node upgrade, after which `gu` must resume Local setup.
|
|
1135
|
-
const nodeProblem = localModeNodeProblem(process.version, machine.platform);
|
|
1213
|
+
const nodeProblem = localModeNodeProblem(process.version, machine.platform, { nvmVersions: nvmNodeVersions() });
|
|
1136
1214
|
if (nodeProblem) {
|
|
1137
1215
|
console.log("\n " + red("✗ ") + nodeProblem + "\n");
|
|
1138
1216
|
return { complete: false, stoppedAt: "node" };
|
|
@@ -1189,6 +1267,7 @@ export async function runClientExpertSetup({
|
|
|
1189
1267
|
// "Docker is not available here": a machine without Docker is the ordinary case on a server
|
|
1190
1268
|
// and on Windows, not an error to open with.
|
|
1191
1269
|
let explained = false;
|
|
1270
|
+
const hadServer = Boolean(mongo.installed);
|
|
1192
1271
|
if (machine.platform !== "darwin" && !mongo.silentPort && (await probeDockerForMongo()).usable) {
|
|
1193
1272
|
if (await startMongoInDocker(ask)) {
|
|
1194
1273
|
mongo.running = true;
|
|
@@ -1233,6 +1312,8 @@ export async function runClientExpertSetup({
|
|
|
1233
1312
|
mongo.running = true;
|
|
1234
1313
|
mongo.how = after.how;
|
|
1235
1314
|
}
|
|
1315
|
+
// Setup INSTALLED it a moment ago, so it has no credentials of its own to ask about.
|
|
1316
|
+
mongo.installedNow = !hadServer;
|
|
1236
1317
|
}
|
|
1237
1318
|
|
|
1238
1319
|
console.log("\n " + cyan("✓ MongoDB is running") + dim(` (${mongo.how})`));
|
|
@@ -1251,6 +1332,8 @@ export async function runClientExpertSetup({
|
|
|
1251
1332
|
if (mongoUri !== LOCAL_MONGO_URI) {
|
|
1252
1333
|
console.log(dim(" using the MongoDB credentials from the previous run"));
|
|
1253
1334
|
}
|
|
1335
|
+
} else if (credsDecision === "ask" && mongo.installedNow) {
|
|
1336
|
+
state = { ...state, mongoCredsConfirmed: true };
|
|
1254
1337
|
} else if (credsDecision === "ask") {
|
|
1255
1338
|
const credsChoice = await askMongoCredentials(ask, askSecret);
|
|
1256
1339
|
if (credsChoice.exit) {
|
|
@@ -1575,8 +1658,10 @@ export async function runClientExpertSetup({
|
|
|
1575
1658
|
}
|
|
1576
1659
|
if (skipModels) {
|
|
1577
1660
|
console.log("");
|
|
1578
|
-
|
|
1579
|
-
|
|
1661
|
+
// "skipped" is only true when nothing was chosen: a provider that was picked is NOT skipped,
|
|
1662
|
+
// its key and model are just asked in gu itself (GH #397).
|
|
1663
|
+
console.log(" " + cyan(addProvider ? `✓ ${addProvider.name || "Provider"} chosen` : "✓ Model setup skipped") + dim(addProvider
|
|
1664
|
+
? " — gu asks for its key and model as soon as it starts"
|
|
1580
1665
|
: loadPreset
|
|
1581
1666
|
? " — the preset picker opens as soon as gu starts"
|
|
1582
1667
|
: " — set them any time with /model (or /use-presets)"));
|
|
@@ -729,7 +729,7 @@ const NOT_A_CHAT_MODEL = [
|
|
|
729
729
|
/moderation/i,
|
|
730
730
|
/\btts\b|-tts|text-to-speech/i,
|
|
731
731
|
/whisper|transcribe|audio|realtime|voice/i,
|
|
732
|
-
/image|vision-preview|dall-?e|sora/i,
|
|
732
|
+
/image|vision-preview|dall-?e|sora|video/i,
|
|
733
733
|
/search(-preview|-api)?$|-search-/i,
|
|
734
734
|
/rerank|guard|classifier/i,
|
|
735
735
|
// OpenAI's original completion endpoints — they take a prompt, not messages.
|
package/daemon-control.mjs
CHANGED
|
@@ -18,7 +18,8 @@ import {
|
|
|
18
18
|
spawnSurviving,
|
|
19
19
|
} from "./platform-tools.mjs";
|
|
20
20
|
import { isLocalApiBase } from "./terminal-layout.mjs";
|
|
21
|
-
|
|
21
|
+
// Only this user's processes (GH #396): another account's gu daemon is not "ours is running".
|
|
22
|
+
const PROC_LIST = processListCommand(process.platform, { ownUser: true });
|
|
22
23
|
|
|
23
24
|
const DIR = join(homedir(), ".gonext");
|
|
24
25
|
export const PID_FILE = join(DIR, "worker.pid");
|
package/doctor-check.mjs
CHANGED
|
@@ -63,6 +63,50 @@ export function rolesFromSettings(s) {
|
|
|
63
63
|
];
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* THE MODELS A TURN ACTUALLY RUNS ON (GH #397), from the agent-payload the turn itself fetches.
|
|
68
|
+
*
|
|
69
|
+
* rolesFromSettings reads the legacy single-provider fields. /model now stores the coder as a
|
|
70
|
+
* provider row (agentCodingBackends), so on a machine set up with today's /model the coder read
|
|
71
|
+
* as "not configured" while turns ran on it fine — and the chat model was probed WITHOUT its key,
|
|
72
|
+
* so a working Qwen/OpenAI setup showed "HTTP 401 No API-key provided". Asking the same endpoint
|
|
73
|
+
* a turn asks means /doctor checks what a turn uses, key included.
|
|
74
|
+
*
|
|
75
|
+
* `codingKey` is fetched separately (the payload never carries the coder's key). The embedder is
|
|
76
|
+
* not in the payload, so it still comes from settings. Returns null when there is no payload, so
|
|
77
|
+
* the caller can fall back to rolesFromSettings.
|
|
78
|
+
*/
|
|
79
|
+
export function rolesFromPayload(payload, settings, codingKey = "") {
|
|
80
|
+
if (!payload || typeof payload !== "object") return null;
|
|
81
|
+
const str = (v) => String(v ?? "").trim();
|
|
82
|
+
// "local" is the API's "no key needed" marker, not a credential.
|
|
83
|
+
const realKey = (k) => (str(k) && str(k) !== "local" ? str(k) : "");
|
|
84
|
+
const [, , embed] = rolesFromSettings(settings);
|
|
85
|
+
const agentSet = payload.agentMissing !== true;
|
|
86
|
+
// NO SEPARATE AGENT MODEL: the coding model answers the chat calls too (GH #397), so check it
|
|
87
|
+
// on THAT path with its key — a "not set" here hid a 401 on "hi" while coding turns worked.
|
|
88
|
+
const chat = agentSet
|
|
89
|
+
? { url: str(payload.agentBaseURL), model: str(payload.agentModelId), apiKey: realKey(payload.agentApiKey) }
|
|
90
|
+
: { url: str(payload.codingBaseURL), model: str(payload.codingModelId), apiKey: realKey(codingKey) };
|
|
91
|
+
return [
|
|
92
|
+
{
|
|
93
|
+
role: "chat",
|
|
94
|
+
label: "chat model",
|
|
95
|
+
why: agentSet ? "answers and routes every turn" : "answers and routes every turn (the coding model, none set apart)",
|
|
96
|
+
...chat,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
role: "code",
|
|
100
|
+
label: "coding model",
|
|
101
|
+
why: "writes the code",
|
|
102
|
+
url: str(payload.codingBaseURL),
|
|
103
|
+
model: str(payload.codingModelId),
|
|
104
|
+
apiKey: realKey(codingKey),
|
|
105
|
+
},
|
|
106
|
+
embed,
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
|
|
66
110
|
/**
|
|
67
111
|
* WHY a model that will not answer is not answering, when the host is an Ollama.
|
|
68
112
|
*
|
|
@@ -166,7 +210,8 @@ export async function checkRole(
|
|
|
166
210
|
try {
|
|
167
211
|
res = await fetchImpl(endpoint, {
|
|
168
212
|
method: "POST",
|
|
169
|
-
|
|
213
|
+
// The key goes in the header only — never into the result, which is printed.
|
|
214
|
+
headers: { "Content-Type": "application/json", ...(r.apiKey ? { Authorization: `Bearer ${r.apiKey}` } : {}) },
|
|
170
215
|
body: JSON.stringify(body),
|
|
171
216
|
signal: AbortSignal.timeout(timeoutMs),
|
|
172
217
|
});
|
|
@@ -245,3 +290,23 @@ export function doctorLines(results) {
|
|
|
245
290
|
}
|
|
246
291
|
return out;
|
|
247
292
|
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* THE VERDICT LINE (GH #397). "✓ every configured model answered." was printed when NOTHING was
|
|
296
|
+
* configured — vacuously true, and green on a machine that cannot answer a single question. The
|
|
297
|
+
* check mark is only earned by a model that answered; with none set up the verdict says so and
|
|
298
|
+
* names where to start. Returns { tone: "ok" | "warn" | "bad", text }.
|
|
299
|
+
*/
|
|
300
|
+
export function doctorSummary(results) {
|
|
301
|
+
const list = Array.isArray(results) ? results : [];
|
|
302
|
+
const bad = list.filter((r) => r.status !== "ok" && r.status !== "unset");
|
|
303
|
+
const ok = list.filter((r) => r.status === "ok");
|
|
304
|
+
const unset = list.filter((r) => r.status === "unset");
|
|
305
|
+
if (bad.length) return { tone: "bad", text: `✗ ${bad.length} of ${list.length} did not answer.` };
|
|
306
|
+
if (!ok.length) return { tone: "warn", text: "· no model is set up yet, so nothing was checked — start with /model." };
|
|
307
|
+
if (unset.length) {
|
|
308
|
+
const names = unset.map((r) => r.label).join(", ");
|
|
309
|
+
return { tone: "warn", text: `✓ ${ok.length} answered · not set up: ${names}.` };
|
|
310
|
+
}
|
|
311
|
+
return { tone: "ok", text: "✓ every configured model answered." };
|
|
312
|
+
}
|
package/embed-model-command.mjs
CHANGED
|
@@ -228,14 +228,14 @@ export function embedNotice({ state, model } = {}) {
|
|
|
228
228
|
* WHICH SETUP ROWS THE BOTTOM BAR SHOWS (first-run noise, #188). A fresh install used to stack
|
|
229
229
|
* three warnings at once. Now they come in order of need, and pure (no OS-specific logic):
|
|
230
230
|
* - nothing to answer with → ONLY "coding model not set" (the one thing to do first);
|
|
231
|
-
* - a coding model exists but no agent model →
|
|
231
|
+
* - a coding model exists but no agent model → NOTHING: one model that also talks is the normal
|
|
232
|
+
* setup, not a fault (GH #397). `agentMissing` is still accepted and ignored.
|
|
232
233
|
* - no embed model → NOTHING until embedding was actually needed (`embedNeeded`: the agent tried
|
|
233
234
|
* a RAG tool, or the user opened /embed-model), then the embed row.
|
|
234
235
|
*/
|
|
235
236
|
export function setupNoticeRows({ noModel = false, agentMissing = false, embedMissing = false, embedNeeded = false, embedWhy } = {}) {
|
|
236
237
|
if (noModel) return ["coding model not set · /model"];
|
|
237
238
|
return [
|
|
238
|
-
...(agentMissing ? ["agent model not set — using the coding model · /agent-model"] : []),
|
|
239
239
|
...(embedMissing && embedNeeded ? [embedNotice(embedWhy) || "no embed model — RAG is off · /embed-model"] : []),
|
|
240
240
|
];
|
|
241
241
|
}
|
package/gonext_agent_chat.py
CHANGED
|
@@ -1707,7 +1707,9 @@ def _coding_backend_flags(backend):
|
|
|
1707
1707
|
# — it applies whenever a folder is registered, i.e. every coding session — and 20 was
|
|
1708
1708
|
# far too low for real work: a NestJS feature routinely spends that on reading and
|
|
1709
1709
|
# installing before it has written a line, so the run stopped to ask mid-task
|
|
1710
|
-
# (user, 2026-08-10: "lets increase it to 100 instead of 20").
|
|
1710
|
+
# (user, 2026-08-10: "lets increase it to 100 instead of 20"). Then 200 for the same reason
|
|
1711
|
+
# (GH #395, user, 2026-11-06: "increase step per turn to 200 instead of 100, only if reach 200
|
|
1712
|
+
# steps it will ask for permission to continue"): the __MAXSTEP__ question is only asked at 200.
|
|
1711
1713
|
#
|
|
1712
1714
|
# Raising it is cheap because the budget is a STOP, not a reservation: a turn that needs
|
|
1713
1715
|
# 12 steps still takes 12. What it changes is how often a working run is interrupted to
|
|
@@ -1716,7 +1718,7 @@ def _coding_backend_flags(backend):
|
|
|
1716
1718
|
#
|
|
1717
1719
|
# The no-workspace defaults stay small on purpose: without a registered folder there are
|
|
1718
1720
|
# no file tools, so a long run there is a model looping rather than making progress.
|
|
1719
|
-
"budget": (20,
|
|
1721
|
+
"budget": (20, 200) if backend == "openai" else (10, 200) if backend == "ollama" else (5, 40),
|
|
1720
1722
|
}
|
|
1721
1723
|
|
|
1722
1724
|
|
package/gu-cli.mjs
CHANGED
|
@@ -400,7 +400,7 @@ if (["start", "stop", "status", "logs", "restart"].includes(args[0])) {
|
|
|
400
400
|
if (await ctl.isClientMode()) {
|
|
401
401
|
console.log(
|
|
402
402
|
"[gu-worker] CLIENT mode — this machine runs the agent but serves no models;\n" +
|
|
403
|
-
" it calls your model machine over the network.
|
|
403
|
+
" it calls your model machine over the network. `gu clear` starts setup over."
|
|
404
404
|
);
|
|
405
405
|
}
|
|
406
406
|
if (s.running) {
|
|
@@ -4214,7 +4214,13 @@ async function runAgentChatJob(job) {
|
|
|
4214
4214
|
const input = JSON.stringify({
|
|
4215
4215
|
messages: payload?.messages ?? [],
|
|
4216
4216
|
agentBaseURL,
|
|
4217
|
-
|
|
4217
|
+
// NO AGENT MODEL SET (GH #397): the API points the chat calls at the coding model and sends
|
|
4218
|
+
// the placeholder key "local". Chat calls ("hi", the plain-reply fallback, routing) must
|
|
4219
|
+
// carry the CODER's real key, or a hosted provider answers 401 while coding turns work.
|
|
4220
|
+
agentApiKey:
|
|
4221
|
+
payload?.agentMissing === true && codingApiKey
|
|
4222
|
+
? codingApiKey
|
|
4223
|
+
: (payload?.agentApiKey ?? ""),
|
|
4218
4224
|
agentModelId,
|
|
4219
4225
|
// Task #134: on a CLIENT the RAG embedder lives on the model machine, so "run doctor
|
|
4220
4226
|
// here" is advice that cannot work. The agent uses this to say the right thing.
|