@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/gu-repl.mjs
CHANGED
|
@@ -126,7 +126,6 @@ import {
|
|
|
126
126
|
nothingToAnswerWith,
|
|
127
127
|
NO_MODEL_LINES,
|
|
128
128
|
resizeRepaintTarget,
|
|
129
|
-
workspaceSyncQuestion,
|
|
130
129
|
workspaceAccessQuestion,
|
|
131
130
|
workspaceSyncLabel,
|
|
132
131
|
activeCoderName,
|
|
@@ -1052,7 +1051,7 @@ const COMMANDS = [
|
|
|
1052
1051
|
// The two synonyms were dispatched but declared nowhere (found by slash-command-wiring.test.mjs):
|
|
1053
1052
|
// they worked, Tab-completed to nothing, and appeared in no help. Aliases is what the registry
|
|
1054
1053
|
// already has for exactly this — /quit, /rag-local.
|
|
1055
|
-
|
|
1054
|
+
// /host-mode is gone (owner, 11-07): modes are chosen at setup; `gu clear` sets up again.
|
|
1056
1055
|
// /undo is HIDDEN_COMMANDS now — still works when typed.
|
|
1057
1056
|
{ name: "/server", desc: "pick a deployment server (host/user) for this session" },
|
|
1058
1057
|
{ name: "/proxy", usage: "/proxy [show|set|on|off|clear|test]", desc: "route MODEL calls through an HTTP proxy (local calls always go direct)" },
|
|
@@ -1436,7 +1435,7 @@ rl._ttyWrite = (str, key) => {
|
|
|
1436
1435
|
}
|
|
1437
1436
|
if (verdict.action === "consume") {
|
|
1438
1437
|
mouseClearTimer();
|
|
1439
|
-
applyMouseReport(verdict.report);
|
|
1438
|
+
if (verdict.report) applyMouseReport(verdict.report); // null: focus in/out, a win32 record
|
|
1440
1439
|
return;
|
|
1441
1440
|
}
|
|
1442
1441
|
if (verdict.action === "replay") {
|
|
@@ -2720,6 +2719,11 @@ let cliNoticeShown = false;
|
|
|
2720
2719
|
function announceCliUpdate() {
|
|
2721
2720
|
if (cliNoticeShown || !availableUpdates.cli) return;
|
|
2722
2721
|
if (following) return; // a turn owns the transcript — try again at the next prompt
|
|
2722
|
+
// …and so does an open menu or question (GH #397): printed into a picker's in-place block, the
|
|
2723
|
+
// notice pushed it down and the picker could no longer erase itself — the rows of a menu left
|
|
2724
|
+
// closed stayed on screen. Retried at the next prompt, like a turn.
|
|
2725
|
+
// A slash command still running hides the input row (inputVisible): its output is not done.
|
|
2726
|
+
if (listPickerActive || askWaiting || !inputVisible) return;
|
|
2723
2727
|
let lines = [];
|
|
2724
2728
|
try {
|
|
2725
2729
|
lines = repeatUpdateNotice({ last: readUpdateAttempt(), cli: availableUpdates.cli, running: REPL_VERSION,
|
|
@@ -5244,7 +5248,7 @@ const ask = async (q, { prefill = "", hint = [], atomic = false, noCancel = fals
|
|
|
5244
5248
|
// Masked prompt for a secret (an API key). Mutes readline's echo/refresh (secretInput)
|
|
5245
5249
|
// so the typed value never hits the terminal or scrollback (task #127). Returns "" on a
|
|
5246
5250
|
// bare Enter (skip).
|
|
5247
|
-
const askSecret = async (q) => {
|
|
5251
|
+
const askSecret = async (q, { hint = [] } = {}) => {
|
|
5248
5252
|
// Same rule as ask(): input has ended, so stop. Returning "" here would hand an EMPTY
|
|
5249
5253
|
// secret to a caller that is about to store or send it.
|
|
5250
5254
|
if (_stdinClosed) return await ask("");
|
|
@@ -5255,6 +5259,9 @@ const askSecret = async (q) => {
|
|
|
5255
5259
|
// FOR. With the bar pinned the row itself becomes the prompt, so nothing is left to infer.
|
|
5256
5260
|
secretPrompt = String(q ?? "").replace(/\s+$/, "") + " ";
|
|
5257
5261
|
secretInput = true;
|
|
5262
|
+
// A hint UNDER the input, gone once answered (GH #397) — printed above, "type or paste the
|
|
5263
|
+
// key…" stayed in the history after the key was in, as if it were still being asked.
|
|
5264
|
+
askHint = barPinned() ? hint : [];
|
|
5258
5265
|
// Same reason as ask() above: inputLineFor returns "" when inputVisible is false, so the one
|
|
5259
5266
|
// bullet per character — the only proof a keystroke registered while the echo is muted —
|
|
5260
5267
|
// would not be drawn either. askSecret is reached from /model, which is exactly a command
|
|
@@ -5264,7 +5271,10 @@ const askSecret = async (q) => {
|
|
|
5264
5271
|
// Unpinned (piped output, or a window too short to reserve rows) there IS no frozen row, so
|
|
5265
5272
|
// the label has to be printed the old way or the prompt is silent.
|
|
5266
5273
|
if (barPinned()) paintBar();
|
|
5267
|
-
else
|
|
5274
|
+
else {
|
|
5275
|
+
for (const l of hint) console.log(GUTTER + dim(l));
|
|
5276
|
+
process.stdout.write(q);
|
|
5277
|
+
}
|
|
5268
5278
|
let raw = "";
|
|
5269
5279
|
try {
|
|
5270
5280
|
askWaiting = true;
|
|
@@ -5277,6 +5287,7 @@ const askSecret = async (q) => {
|
|
|
5277
5287
|
askWaiting = false;
|
|
5278
5288
|
secretInput = false;
|
|
5279
5289
|
secretPrompt = "";
|
|
5290
|
+
askHint = [];
|
|
5280
5291
|
inputVisible = _secretWasVisible;
|
|
5281
5292
|
// READLINE KEEPS EVERY SUBMITTED LINE (task #191). Muting the echo stops the key reaching
|
|
5282
5293
|
// the screen; it does nothing about rl.history, which is where the line goes on Enter — so
|
|
@@ -5343,10 +5354,10 @@ async function ensureWorkspace() {
|
|
|
5343
5354
|
// Registered before cloud sync existed → ask once and persist. Otherwise honor the
|
|
5344
5355
|
// stored choice. (undefined = old record that never opted in → ask.)
|
|
5345
5356
|
if (hit.allowSync === undefined) {
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5357
|
+
// No question (owner, 11-07): the mode chosen at setup decides where history goes —
|
|
5358
|
+
// the cloud on a hosted account, the local database on Client Expert. A folder that
|
|
5359
|
+
// answered No before keeps that answer; only a folder never asked is filled in.
|
|
5360
|
+
hit.allowSync = true;
|
|
5350
5361
|
await writeFile(WS_FILE, JSON.stringify({ workspaces: list }, null, 2) + "\n");
|
|
5351
5362
|
}
|
|
5352
5363
|
// The workspace facts now live in the startup banner (task #131) — just remember them.
|
|
@@ -5380,16 +5391,10 @@ async function ensureWorkspace() {
|
|
|
5380
5391
|
} catch (e) {
|
|
5381
5392
|
console.log(GUTTER + yellow(`Could not save the browser permission (${e?.message || e}) — gu will ask when it first needs it.`));
|
|
5382
5393
|
}
|
|
5383
|
-
//
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
"Allow syncing this workspace's chat history to the cloud?\n" +
|
|
5388
|
-
"(your conversation is saved to your account so you can see it on the web app)",
|
|
5389
|
-
true /* default Yes — RAG is NOT synced (it stays on your S3); only chat history. */
|
|
5390
|
-
)
|
|
5391
|
-
: syncQ.value;
|
|
5392
|
-
if (!syncQ.ask && syncQ.note) console.log(wrapGutter(syncQ.note, dim));
|
|
5394
|
+
// NO SYNC QUESTION (owner, 11-07): Cloud or Client Expert is chosen once, at setup, and it
|
|
5395
|
+
// decides where chat history lives — your account's cloud database, or the local one. To
|
|
5396
|
+
// change it, `gu clear` and set up again. RAG is never synced (it stays where it was built).
|
|
5397
|
+
const allowSync = true;
|
|
5393
5398
|
const next = list.filter((w) => w.path !== cwd);
|
|
5394
5399
|
next.push({
|
|
5395
5400
|
name: basename(cwd),
|
|
@@ -6196,7 +6201,11 @@ async function runModelDoctor() {
|
|
|
6196
6201
|
return;
|
|
6197
6202
|
}
|
|
6198
6203
|
console.log(dim(" Asking each model to answer — the real request, not just a ping."));
|
|
6199
|
-
|
|
6204
|
+
// The SAME models, URLs and keys a turn gets (GH #397): this folder's /model pick included.
|
|
6205
|
+
const turnPayload = await fetchAgentPayload([{ role: "user", content: "ping" }], sessionCodingModel).catch(() => null);
|
|
6206
|
+
const p = turnPayload?.payload ?? null;
|
|
6207
|
+
const codingKey = p?.codingBackendId ? (await fetchKeyForBackend(p.codingBackendId))?.apiKey ?? "" : "";
|
|
6208
|
+
const roles = dc.rolesFromPayload(p, settings, codingKey) ?? dc.rolesFromSettings(settings);
|
|
6200
6209
|
const results = [];
|
|
6201
6210
|
for (const r of roles) {
|
|
6202
6211
|
// One at a time, and SAID one at a time: these share a GPU, so a check can take a while and
|
|
@@ -6213,13 +6222,8 @@ async function runModelDoctor() {
|
|
|
6213
6222
|
console.log(GUTTER + (res.status === "ok" ? dim(l) : res.status === "unset" ? dim(l) : yellow(l)));
|
|
6214
6223
|
}
|
|
6215
6224
|
}
|
|
6216
|
-
const
|
|
6217
|
-
console.log(
|
|
6218
|
-
"\n" + GUTTER +
|
|
6219
|
-
(bad.length === 0
|
|
6220
|
-
? accent("✓ every configured model answered.")
|
|
6221
|
-
: red(`✗ ${bad.length} of ${results.length} did not answer.`))
|
|
6222
|
-
);
|
|
6225
|
+
const verdict0 = dc.doctorSummary(results);
|
|
6226
|
+
console.log("\n" + GUTTER + (verdict0.tone === "ok" ? accent : verdict0.tone === "bad" ? red : yellow)(verdict0.text));
|
|
6223
6227
|
// THE GU SERVICE TOO (#413): every model can answer and a coding turn still fail, because its
|
|
6224
6228
|
// instructions come from the gu service — reported on a Windows VM where only "hi" worked.
|
|
6225
6229
|
const sc = await import("./service-check.mjs");
|
|
@@ -6292,10 +6296,18 @@ async function doctorFetch(url, init) {
|
|
|
6292
6296
|
* Returns [] when there is nothing to offer, or null when the user cancelled — which the caller
|
|
6293
6297
|
* must treat as "abandon", not as "offer none", or Escape would silently save something.
|
|
6294
6298
|
*/
|
|
6295
|
-
async function pickExtraModels(
|
|
6296
|
-
|
|
6297
|
-
|
|
6299
|
+
async function pickExtraModels(rest0, preChecked = []) {
|
|
6300
|
+
// The SAME non-coding filter as the main pick (GH #397): the extras are coders too, and this
|
|
6301
|
+
// list offered image, audio and video models the picker just above had hidden.
|
|
6298
6302
|
const already = new Set(preChecked);
|
|
6303
|
+
const { codingModelCandidates } = await import("./coding-provider-setup.mjs");
|
|
6304
|
+
const keep = new Set(codingModelCandidates(rest0).models);
|
|
6305
|
+
const rest = rest0
|
|
6306
|
+
.filter((m) => keep.has(m) || already.has(m))
|
|
6307
|
+
.sort((a, b) => a.localeCompare(b, "en", { numeric: true })); // as the pick above (GH #397)
|
|
6308
|
+
if (rest.length === 0) return [];
|
|
6309
|
+
console.log(dim(` Any other models to switch to in /model? optional · up to ${MAX_ALT_CODERS}`));
|
|
6310
|
+
console.log(dim(` space ticks · Enter saves (nothing ticked skips this) · Esc cancels`));
|
|
6299
6311
|
const ticks = await pickManyFromList(
|
|
6300
6312
|
rest,
|
|
6301
6313
|
rest.map((m, i) => (already.has(m) ? i : -1)).filter((i) => i >= 0),
|
|
@@ -6323,7 +6335,12 @@ async function pickExtraModels(rest, preChecked = []) {
|
|
|
6323
6335
|
* Returns the model id, or null when the user cancelled.
|
|
6324
6336
|
*/
|
|
6325
6337
|
async function chooseCodingModelFrom(ps, all, question, current, { back = false } = {}) {
|
|
6326
|
-
const { models:
|
|
6338
|
+
const { models: coders0, filtered } = ps.codingModelCandidates(all);
|
|
6339
|
+
// SORTED, like /agent-model's list (GH #397): the provider's own order is arbitrary, and a
|
|
6340
|
+
// family (qwen3.8-*, deepseek-v4-*) scattered over the list is hard to scan.
|
|
6341
|
+
const byName = (a, b) => a.localeCompare(b, "en", { numeric: true });
|
|
6342
|
+
const coders = [...coders0].sort(byName);
|
|
6343
|
+
all = [...all].sort(byName);
|
|
6327
6344
|
let list = coders;
|
|
6328
6345
|
let showingAll = filtered === 0;
|
|
6329
6346
|
for (;;) {
|
|
@@ -6333,11 +6350,11 @@ async function chooseCodingModelFrom(ps, all, question, current, { back = false
|
|
|
6333
6350
|
// Opens on the CURRENT model where there is one — a long list that opens at the top tells the
|
|
6334
6351
|
// reader nothing about which one they are already using.
|
|
6335
6352
|
const at = typeof current === "string" ? list.indexOf(current) : Number(current) || 0;
|
|
6336
|
-
|
|
6337
|
-
|
|
6338
|
-
|
|
6339
|
-
);
|
|
6340
|
-
const idx = await pickFromList(rows, at >= 0 ? at : 0, { back });
|
|
6353
|
+
// A HEADER, not a printed line (GH #397): erased with the list, so a finished pick leaves
|
|
6354
|
+
// "→ model" behind instead of a question nobody is being asked any more.
|
|
6355
|
+
const head = [dim(` ${question} (${PICK_HINT})`)];
|
|
6356
|
+
if (!showingAll && filtered) head.push(dim(` ${filtered} non-coding models hidden — "show all" is the last row`));
|
|
6357
|
+
const idx = await pickFromList(rows, at >= 0 ? at : 0, { back, header: () => head, headerRows: head.length });
|
|
6341
6358
|
if (idx === PICK_BACK) return BACK;
|
|
6342
6359
|
if (idx < 0) return null;
|
|
6343
6360
|
if (idx === list.length) {
|
|
@@ -6349,6 +6366,7 @@ async function chooseCodingModelFrom(ps, all, question, current, { back = false
|
|
|
6349
6366
|
showingAll = true;
|
|
6350
6367
|
continue;
|
|
6351
6368
|
}
|
|
6369
|
+
console.log(dim(` → ${list[idx]}`));
|
|
6352
6370
|
return list[idx];
|
|
6353
6371
|
}
|
|
6354
6372
|
}
|
|
@@ -6366,20 +6384,40 @@ async function chooseCodingModelFrom(ps, all, question, current, { back = false
|
|
|
6366
6384
|
* stays available for the same reason it does there — a listed model may be unusable and an
|
|
6367
6385
|
* unlisted one may be exactly right.
|
|
6368
6386
|
*/
|
|
6369
|
-
async function chooseFromModelList(all, question, current = 0, { back = false } = {}) {
|
|
6370
|
-
|
|
6387
|
+
async function chooseFromModelList(all, question, current = 0, { back = false, notes = null } = {}) {
|
|
6388
|
+
// `notes` (GH #397): a dim note per row ("an audio/speech model"), as /agent-model's own list has.
|
|
6389
|
+
const rows = [...all.map((m, i) => (notes?.[i] ? `${m}${dim(` · ${notes[i]}`)}` : m)), "type a model name…"];
|
|
6371
6390
|
const at = typeof current === "string" ? all.indexOf(current) : Number(current) || 0;
|
|
6372
|
-
|
|
6373
|
-
|
|
6391
|
+
const idx = await pickFromList(rows, at >= 0 ? at : 0, {
|
|
6392
|
+
back,
|
|
6393
|
+
header: () => [dim(` ${question} (${PICK_HINT})`)],
|
|
6394
|
+
headerRows: 1,
|
|
6395
|
+
});
|
|
6374
6396
|
if (idx === PICK_BACK) return BACK;
|
|
6375
6397
|
if (idx < 0) return null;
|
|
6376
6398
|
if (idx === all.length) {
|
|
6377
6399
|
const typed = (await ask(" model name: ")).trim();
|
|
6378
6400
|
return typed || null;
|
|
6379
6401
|
}
|
|
6402
|
+
console.log(dim(` → ${all[idx]}`));
|
|
6380
6403
|
return all[idx];
|
|
6381
6404
|
}
|
|
6382
6405
|
|
|
6406
|
+
/**
|
|
6407
|
+
* The agent's pick from a fresh provider (GH #397): the SAME order and labels as /agent-model's
|
|
6408
|
+
* "pick another model on <host>…" — chat models alphabetically, then audio/image/video models
|
|
6409
|
+
* sunk to the bottom and named. The add flow showed them mixed in and unlabelled.
|
|
6410
|
+
*/
|
|
6411
|
+
async function pickAgentModelFrom(models, question, current, opts) {
|
|
6412
|
+
const am = await import("./agent-model-command.mjs");
|
|
6413
|
+
const sorted = am.agentModelsFromProvider(models);
|
|
6414
|
+
const ids = sorted.map((m) => m.model);
|
|
6415
|
+
return chooseFromModelList(ids, question, Math.max(0, ids.indexOf(current)), {
|
|
6416
|
+
...opts,
|
|
6417
|
+
notes: sorted.map((m) => m.note),
|
|
6418
|
+
});
|
|
6419
|
+
}
|
|
6420
|
+
|
|
6383
6421
|
/**
|
|
6384
6422
|
* /model's provider manager. `back` (#242): opened from /model's list, so its first menu offers
|
|
6385
6423
|
* "← back" and returns BACK. Inside it, each sub-menu's "← back" returns HERE — re-read, because a
|
|
@@ -6405,8 +6443,13 @@ async function manageCodingProviders({ back = false } = {}) {
|
|
|
6405
6443
|
);
|
|
6406
6444
|
const ADD = labels.length;
|
|
6407
6445
|
labels.push(actionRow("add another provider — Claude API, OpenAI, Kimi…"));
|
|
6408
|
-
|
|
6409
|
-
|
|
6446
|
+
// Headers, not printed lines (GH #397): going one level deeper must not leave the level above
|
|
6447
|
+
// stacked on screen.
|
|
6448
|
+
const idx = await pickFromList(labels, selAt, {
|
|
6449
|
+
back,
|
|
6450
|
+
header: () => [dim(` Providers — pick one to change it, or add another (${PICK_HINT}):`)],
|
|
6451
|
+
headerRows: 1,
|
|
6452
|
+
});
|
|
6410
6453
|
if (idx === PICK_BACK) return BACK;
|
|
6411
6454
|
if (idx < 0) {
|
|
6412
6455
|
console.log(dim(" (cancelled)\n"));
|
|
@@ -6420,11 +6463,10 @@ async function manageCodingProviders({ back = false } = {}) {
|
|
|
6420
6463
|
|
|
6421
6464
|
const p = providers[idx];
|
|
6422
6465
|
for (;;) {
|
|
6423
|
-
console.log("\n" + GUTTER + dim(`${p.host} · ${p.model || "no model set"}`));
|
|
6424
6466
|
const action = await pickFromList(
|
|
6425
6467
|
[dim("keep it"), "change the model…", "change the URL…", "remove this provider…"],
|
|
6426
6468
|
1,
|
|
6427
|
-
{ back: true }
|
|
6469
|
+
{ back: true, header: () => [GUTTER + dim(`${p.host} · ${p.model || "no model set"}`)], headerRows: 1 }
|
|
6428
6470
|
);
|
|
6429
6471
|
if (action === PICK_BACK) break; // → the provider list
|
|
6430
6472
|
if (action <= 0) {
|
|
@@ -6548,7 +6590,7 @@ async function changeProviderModel(ps, rows, p) {
|
|
|
6548
6590
|
await syncActiveCoderWhere();
|
|
6549
6591
|
console.log(GUTTER + dim("this folder's pick named the old model — cleared it."));
|
|
6550
6592
|
}
|
|
6551
|
-
console.log(accent(`
|
|
6593
|
+
console.log(accent(` ✓ ${p.host} now uses ${model}`) + dim(" · /model switches between its models\n"));
|
|
6552
6594
|
}
|
|
6553
6595
|
|
|
6554
6596
|
/** Point a provider at a different URL, keeping its id so saved picks still resolve. */
|
|
@@ -6639,6 +6681,18 @@ async function removeCodingProvider(ps, rows, p) {
|
|
|
6639
6681
|
);
|
|
6640
6682
|
console.log(GUTTER + dim(" they will be deleted too; a key with no provider can never be used again.\n"));
|
|
6641
6683
|
}
|
|
6684
|
+
// THE AGENT MAY TALK THROUGH THIS PROVIDER TOO (GH #397). Removing the row and its key left
|
|
6685
|
+
// agentModelUrl pointing at it: the banner still named the model, every question failed on a
|
|
6686
|
+
// missing key, and /agent-model probed the URL as an Ollama (…/api/tags → 404). So the agent
|
|
6687
|
+
// model goes with it — said before the question, like the keys.
|
|
6688
|
+
const settings = await fetchWorkerSettings().catch(() => null);
|
|
6689
|
+
const sameUrl = (u) => u && ps.ensureV1Root(String(u)).toLowerCase() === ps.ensureV1Root(String(p.url ?? "")).toLowerCase();
|
|
6690
|
+
const agentUses = sameUrl(settings?.agentModelUrl);
|
|
6691
|
+
if (agentUses) {
|
|
6692
|
+
const name = String(settings.agentModelName || settings.agentModel || "").trim();
|
|
6693
|
+
console.log(GUTTER + orange(` the agent model uses it too${name ? ` — ${name}` : ""}`));
|
|
6694
|
+
console.log(GUTTER + dim(" it will be unset; pick another with /agent-model.\n"));
|
|
6695
|
+
}
|
|
6642
6696
|
const yes = await askYesNo(
|
|
6643
6697
|
`Remove ${p.host} (${p.model || "no model set"})? This cannot be undone.`,
|
|
6644
6698
|
false
|
|
@@ -6650,6 +6704,7 @@ async function removeCodingProvider(ps, rows, p) {
|
|
|
6650
6704
|
const saved = await withPendingStatus("removing", () =>
|
|
6651
6705
|
md.saveWorkerSettings(apiBase, workerKey, {
|
|
6652
6706
|
agentCodingBackends: ps.removeProvider(rows, p.id),
|
|
6707
|
+
...(agentUses ? { agentModelUrl: "", agentModelName: "", agentModel: "" } : {}),
|
|
6653
6708
|
})
|
|
6654
6709
|
);
|
|
6655
6710
|
if (!saved) {
|
|
@@ -6666,6 +6721,7 @@ async function removeCodingProvider(ps, rows, p) {
|
|
|
6666
6721
|
sessionCodingKeyId = "";
|
|
6667
6722
|
await saveSessionCodingKeyId(resolve(process.cwd()));
|
|
6668
6723
|
}
|
|
6724
|
+
if (k.id === sessionAgentKeyId) await saveFolderKeyPick("agent", "");
|
|
6669
6725
|
}
|
|
6670
6726
|
// This folder may have been pointed at the removed provider.
|
|
6671
6727
|
if (sessionCodingUrl === p.url || sessionCodingModel.startsWith(`${p.id}::`)) {
|
|
@@ -6679,8 +6735,21 @@ async function removeCodingProvider(ps, rows, p) {
|
|
|
6679
6735
|
console.log(
|
|
6680
6736
|
accent(` ✓ removed ${p.host}`) +
|
|
6681
6737
|
(stranded.length ? dim(` · ${deleted} key${deleted === 1 ? "" : "s"} deleted`) : "") +
|
|
6738
|
+
(agentUses ? dim(" · agent model unset") : "") +
|
|
6682
6739
|
"\n"
|
|
6683
6740
|
);
|
|
6741
|
+
// The bar named the removed coder (the account default is held in memory): read it back.
|
|
6742
|
+
if (sameUrl(defaultCodingUrl)) {
|
|
6743
|
+
defaultCodingModel = "";
|
|
6744
|
+
defaultCodingUrl = "";
|
|
6745
|
+
}
|
|
6746
|
+
const after = (await fetchAgentPayload([{ role: "user", content: "." }]).catch(() => null))?.payload;
|
|
6747
|
+
if (after) {
|
|
6748
|
+
seedDefaultCoder(after);
|
|
6749
|
+
agentMissing = after.agentMissing === true;
|
|
6750
|
+
noModelAtAll = nothingToAnswerWith(after);
|
|
6751
|
+
}
|
|
6752
|
+
if (barPinned()) paintBar();
|
|
6684
6753
|
}
|
|
6685
6754
|
|
|
6686
6755
|
/**
|
|
@@ -6709,6 +6778,7 @@ async function removeCodingProvider(ps, rows, p) {
|
|
|
6709
6778
|
* newKey ask for a key even if the server has one (the stored one was refused)
|
|
6710
6779
|
*/
|
|
6711
6780
|
async function addServer({ role = "coding", dialect = "openai", url: presetUrl = "", model: presetModel = "", plan: presetPlan = null, newKey = false, back = false } = {}) {
|
|
6781
|
+
let claudeSdkLater = false; // offered after the save, see step 1
|
|
6712
6782
|
const as = await import("./add-server.mjs");
|
|
6713
6783
|
const ps = await import("./coding-provider-setup.mjs");
|
|
6714
6784
|
const sk = await import("./server-keys.mjs");
|
|
@@ -6736,7 +6806,8 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6736
6806
|
const providerPicker = !presetUrl && dialect === "openai";
|
|
6737
6807
|
const ec = role === "embed" ? await import("./embed-model-command.mjs") : null;
|
|
6738
6808
|
let target, host, found, need, key, keyName, typedKey, models, model;
|
|
6739
|
-
|
|
6809
|
+
let lastPick = 0; // GH #397: back from the key re-opens the list ON the provider just chosen
|
|
6810
|
+
providerLoop: for (;;) {
|
|
6740
6811
|
// --- 1. the server --------------------------------------------------------------------
|
|
6741
6812
|
target = null;
|
|
6742
6813
|
// The dialect the URL prompt reads the address in: the caller's, or Ollama's when that row was picked.
|
|
@@ -6754,22 +6825,33 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6754
6825
|
const presets = ps.providerPresets(role);
|
|
6755
6826
|
// #244: an Ollama on this machine or the network is a row here, like the hosted ones — the
|
|
6756
6827
|
// menus no longer have a separate "use an Ollama server…" entrance. It asks for the address.
|
|
6757
|
-
|
|
6828
|
+
// OLLAMA FIRST (GH #397): first-run setup lists "Ollama server — this machine or your own"
|
|
6829
|
+
// first and these pickers listed it seventh, so the same choice moved between two screens.
|
|
6830
|
+
// gu is local-first; the hosted providers follow in their usual order.
|
|
6831
|
+
const OLLAMA_HERE = 0;
|
|
6758
6832
|
const rowsShown = [
|
|
6759
|
-
...presets.map((p) => `${p.name}${dim(` · ${p.url}`)}`),
|
|
6760
6833
|
`Ollama — this machine or your network…${dim(" · e.g. 127.0.0.1:11434")}`,
|
|
6834
|
+
// The HOST, not the full URL (GH #397): "…/compatible-mode/v1" ran past 100 columns and was
|
|
6835
|
+
// cut mid-word; the path is plumbing, and the URL is echoed in full once one is picked.
|
|
6836
|
+
...presets.map((p) => `${p.name}${dim(` · ${hostOf(p.url)}`)}`),
|
|
6761
6837
|
"another URL…",
|
|
6762
6838
|
];
|
|
6763
6839
|
// A preset names its own dialect (Ollama cloud is "ollama"); the rest are OpenAI-compatible.
|
|
6764
|
-
|
|
6765
|
-
|
|
6840
|
+
// The question is the picker's HEADER, erased with it (GH #397): printed above, it stayed in
|
|
6841
|
+
// the history every time "← back" re-opened this list — "Which provider?" stacked up.
|
|
6842
|
+
const pi = await pickFromList(rowsShown, lastPick, { back, header: () => [dim(` Which provider? (${PICK_HINT})`)], headerRows: 1 });
|
|
6766
6843
|
if (pi === PICK_BACK) return BACK;
|
|
6767
6844
|
if (pi < 0) {
|
|
6768
6845
|
console.log(dim(" (nothing saved)\n"));
|
|
6769
6846
|
return;
|
|
6770
6847
|
}
|
|
6771
|
-
|
|
6848
|
+
lastPick = pi;
|
|
6849
|
+
const preset = pi > OLLAMA_HERE && pi <= presets.length ? presets[pi - 1] : null;
|
|
6850
|
+
if (preset) target = as.serverTarget(preset.dialect ?? "openai", preset.url);
|
|
6772
6851
|
else if (pi === OLLAMA_HERE) askDialect = "ollama";
|
|
6852
|
+
// SAY WHAT WAS PICKED (GH #397): the list is erased on Enter, and what followed ("… needs an
|
|
6853
|
+
// API key", "key name:") never named the provider — the choice was nowhere on screen.
|
|
6854
|
+
console.log(GUTTER + dim(`→ ${preset ? preset.name : pi === OLLAMA_HERE ? "Ollama" : "another URL"}`));
|
|
6773
6855
|
}
|
|
6774
6856
|
if (!target && !presetUrl) {
|
|
6775
6857
|
const dflt = as.defaultServerUrl(role, askDialect, settings, existing);
|
|
@@ -6806,10 +6888,10 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6806
6888
|
console.log("");
|
|
6807
6889
|
return;
|
|
6808
6890
|
}
|
|
6809
|
-
// Claude as the CODER: prompt caching needs Anthropic's SDK (#262)
|
|
6810
|
-
|
|
6811
|
-
|
|
6812
|
-
|
|
6891
|
+
// Claude as the CODER: prompt caching needs Anthropic's SDK (#262). Offered AFTER the key and the
|
|
6892
|
+
// model are saved (GH #397): asked first, it interrupted the flow before the user had even been
|
|
6893
|
+
// asked for a key, and dumped pip's raw output over the screen.
|
|
6894
|
+
claudeSdkLater = Boolean(target.claude && role === "coding");
|
|
6813
6895
|
|
|
6814
6896
|
// --- 2. the server's key --------------------------------------------------------------
|
|
6815
6897
|
// AN EXISTING SERVER IS REUSED, never stored twice (two rows for one URL would be two picker
|
|
@@ -6835,8 +6917,19 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6835
6917
|
if (key) console.log(GUTTER + dim(`using the key "${inUse.name}" already saved for ${host}.`));
|
|
6836
6918
|
}
|
|
6837
6919
|
}
|
|
6838
|
-
|
|
6839
|
-
|
|
6920
|
+
// ESC AT THE KEY GOES BACK ONE STEP (GH #397): with a provider picker behind it, Esc at
|
|
6921
|
+
// "key name:" / "API key:" returns to "Which provider?" — it used to unwind the whole command,
|
|
6922
|
+
// so choosing the wrong provider meant starting /agent-model or /model again. Nothing is saved
|
|
6923
|
+
// before step 5, so going back leaves nothing behind. Without a picker it still cancels.
|
|
6924
|
+
const BACK_TO_PROVIDERS = Symbol("back");
|
|
6925
|
+
const askKey = async ({ again = false } = {}) => {
|
|
6926
|
+
let t;
|
|
6927
|
+
try {
|
|
6928
|
+
t = await askServerKey({ host, optional: need === "optional", escBack: providerPicker, ...(again && typedKey ? { name: keyName } : {}) });
|
|
6929
|
+
} catch (e) {
|
|
6930
|
+
if (wasPromptCancelled(e) && providerPicker) return BACK_TO_PROVIDERS;
|
|
6931
|
+
throw e;
|
|
6932
|
+
}
|
|
6840
6933
|
key = t.secret;
|
|
6841
6934
|
keyName = t.name;
|
|
6842
6935
|
typedKey = Boolean(t.secret);
|
|
@@ -6844,7 +6937,9 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6844
6937
|
};
|
|
6845
6938
|
if (!key && (need !== "none" || newKey)) {
|
|
6846
6939
|
if (need === "required") console.log(GUTTER + dim(`${host} needs an API key.`));
|
|
6847
|
-
|
|
6940
|
+
const got = await askKey();
|
|
6941
|
+
if (got === BACK_TO_PROVIDERS) continue providerLoop;
|
|
6942
|
+
if (!got) return;
|
|
6848
6943
|
}
|
|
6849
6944
|
|
|
6850
6945
|
// --- 3. its models, listed with that key ----------------------------------------------
|
|
@@ -6861,10 +6956,18 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6861
6956
|
}
|
|
6862
6957
|
});
|
|
6863
6958
|
let probe = await list();
|
|
6864
|
-
//
|
|
6865
|
-
|
|
6866
|
-
|
|
6867
|
-
|
|
6959
|
+
// A REFUSED KEY IS ASKED FOR AGAIN, HERE (GH #397). The stored one always was; a TYPED one
|
|
6960
|
+
// ended the command with "check the key and try again" — which meant /model, the provider and
|
|
6961
|
+
// the key name all over again for one mistyped character. Two more tries, then stop.
|
|
6962
|
+
for (let retry = 0; retry < 2 && (probe.status === 401 || probe.status === 403) && key; retry++) {
|
|
6963
|
+
// Two short lines, not one long one: at 80 columns the single sentence wrapped mid-word.
|
|
6964
|
+
console.log(yellow(typedKey
|
|
6965
|
+
? ` ${host} rejected that key (HTTP ${probe.status}).`
|
|
6966
|
+
: ` ${host} refused the stored key (HTTP ${probe.status}).`));
|
|
6967
|
+
console.log(GUTTER + dim(providerPicker ? "paste the right one — Esc goes back to the providers." : "paste the right one — Esc cancels."));
|
|
6968
|
+
const got = await askKey({ again: true });
|
|
6969
|
+
if (got === BACK_TO_PROVIDERS) continue providerLoop;
|
|
6970
|
+
if (!got || !key) return;
|
|
6868
6971
|
probe = await list();
|
|
6869
6972
|
}
|
|
6870
6973
|
if (probe.status || probe.err) {
|
|
@@ -6896,7 +6999,9 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6896
6999
|
const canBack = { back: providerPicker || back };
|
|
6897
7000
|
model = R.filterCoders
|
|
6898
7001
|
? await chooseCodingModelFrom(ps, models, R.question, 0, canBack)
|
|
6899
|
-
:
|
|
7002
|
+
: role === "agent"
|
|
7003
|
+
? await pickAgentModelFrom(models, R.question, current, canBack)
|
|
7004
|
+
: await chooseFromModelList(models, R.question, Math.max(0, models.indexOf(current)), canBack);
|
|
6900
7005
|
if (model === BACK) {
|
|
6901
7006
|
if (providerPicker) continue; // → Which provider?
|
|
6902
7007
|
return BACK;
|
|
@@ -6941,6 +7046,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6941
7046
|
}
|
|
6942
7047
|
|
|
6943
7048
|
// --- 5. save: the server, its key, then the role --------------------------------------
|
|
7049
|
+
let alsoInModel = false;
|
|
6944
7050
|
let rowId = found?.id ?? "";
|
|
6945
7051
|
let rows = existing;
|
|
6946
7052
|
// A NEW ROW ONLY WHEN SOMETHING NEEDS ONE: the coder is picked through its row, and a key is
|
|
@@ -6967,7 +7073,8 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6967
7073
|
console.log(GUTTER + dim("if this machine's local API is old, restart it with `gu api restart`.\n"));
|
|
6968
7074
|
return;
|
|
6969
7075
|
}
|
|
6970
|
-
|
|
7076
|
+
// Said AFTER the ✓ (GH #397) — printed here it came before the result it is a footnote to.
|
|
7077
|
+
if (!found && role !== "coding" && as.serverRole(role).rowModel) alsoInModel = true;
|
|
6971
7078
|
}
|
|
6972
7079
|
if (typedKey) {
|
|
6973
7080
|
const id = await withPendingStatus("saving the key", () => addCodingKey(keyName, key, rowId));
|
|
@@ -6980,14 +7087,15 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6980
7087
|
// the stored one was refused: it is the server's key from now on, for every role and folder.
|
|
6981
7088
|
if (found) await setServerDefaultKey(rowId, id);
|
|
6982
7089
|
}
|
|
6983
|
-
const keyLine = key ? dim(`
|
|
7090
|
+
const keyLine = key ? dim(` API key ${keyName || "saved"} ····${key.slice(-4)}`) : "";
|
|
6984
7091
|
|
|
6985
7092
|
if (role === "agent") {
|
|
6986
7093
|
// A MACHINE SETTING, not a folder pick: the agent is what gu talks to in every folder. Saved
|
|
6987
7094
|
// as URL + NAME, the pair app.ts matches back to the server row to find the key.
|
|
6988
|
-
|
|
6989
|
-
|
|
6990
|
-
|
|
7095
|
+
// ONE BLOCK under the ✓ (GH #397): the key, then the footnotes — not three loose paragraphs.
|
|
7096
|
+
const extra = [keyLine, alsoInModel ? dim(` ${host} is also offered as a coding model in /model.`) : ""];
|
|
7097
|
+
if (await saveAgentModel(target.roleUrl("agent"), model, host, extra.filter(Boolean))) {
|
|
7098
|
+
/* said */
|
|
6991
7099
|
} else if (rows !== existing || typedKey) {
|
|
6992
7100
|
// The SERVER landed — say so, or the user re-enters a key that is already stored.
|
|
6993
7101
|
console.log(yellow(` ${host} was saved, but the agent model was not pointed at it.`));
|
|
@@ -7008,7 +7116,16 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
7008
7116
|
}
|
|
7009
7117
|
return;
|
|
7010
7118
|
}
|
|
7011
|
-
await useCoderInThisFolder({ ps, rows, beforeChoices, url: target.url, host, locality: target.locality, model, backendId: rowId, key, keyName });
|
|
7119
|
+
await useCoderInThisFolder({ ps, rows, beforeChoices, url: target.url, host, locality: target.locality, model, backendId: rowId, key, keyName, extras });
|
|
7120
|
+
if (claudeSdkLater) {
|
|
7121
|
+
// pip's own lines ("Requirement already satisfied…", "Collecting…") are noise here: keep gu's.
|
|
7122
|
+
const pipNoise = /^(Requirement already satisfied|Collecting|Downloading|Using cached|Installing collected|Successfully installed|Obtaining|Preparing|Building|Created wheel|Stored in directory|WARNING|\[notice\]|\s*[━╸─]+)/i;
|
|
7123
|
+
await md.ensureClaudeSdk({
|
|
7124
|
+
log: (l) => { const t = String(l ?? "").trim(); if (t && !pipNoise.test(t)) console.log(GUTTER + dim(t)); },
|
|
7125
|
+
confirm: (q) => askYesNo(` ${q}`, true),
|
|
7126
|
+
});
|
|
7127
|
+
console.log("");
|
|
7128
|
+
}
|
|
7012
7129
|
}
|
|
7013
7130
|
|
|
7014
7131
|
/**
|
|
@@ -7021,7 +7138,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
7021
7138
|
* · DID SAVING EVICT ANOTHER PROVIDER? An API older than row ids keys a backend by its KIND,
|
|
7022
7139
|
* so a second openai row replaced the first with a 200. Said BEFORE the success line.
|
|
7023
7140
|
*/
|
|
7024
|
-
async function useCoderInThisFolder({ ps, rows, beforeChoices, url, host, locality, model, backendId, key, keyName }) {
|
|
7141
|
+
async function useCoderInThisFolder({ ps, rows, beforeChoices, url, host, locality, model, backendId, key, keyName, extras = [] }) {
|
|
7025
7142
|
const cwd = resolve(process.cwd());
|
|
7026
7143
|
const after = await withPendingStatus("checking it is offered", () =>
|
|
7027
7144
|
fetchAgentPayload([{ role: "user", content: "ping" }]).catch(() => null)
|
|
@@ -7070,15 +7187,23 @@ async function useCoderInThisFolder({ ps, rows, beforeChoices, url, host, locali
|
|
|
7070
7187
|
// that was just entered, which reads as the save having failed.
|
|
7071
7188
|
codingKeyMissingKnown = false;
|
|
7072
7189
|
console.log(accent(` ✓ coding model → ${model}`) + dim(` · ${host}`));
|
|
7073
|
-
if (key) console.log(dim(` API key ${keyName || "saved"} ····${key.slice(-4)}
|
|
7190
|
+
if (key) console.log(dim(` API key ${keyName || "saved"} ····${key.slice(-4)}`));
|
|
7191
|
+
// The extras were ticked a screen ago and never mentioned again (GH #397) — say where they went.
|
|
7192
|
+
if (extras.length) console.log(dim(` also in /model: ${extras.join(", ")}`));
|
|
7193
|
+
console.log("");
|
|
7074
7194
|
// A HOSTED SERVER NEEDS ITS WINDOW SET: with nothing set gu assumes 24,000 prompt tokens and
|
|
7075
7195
|
// compacts there — on a large hosted model that throws history away every turn.
|
|
7076
7196
|
if (ps.shouldMentionContextWindow(locality)) {
|
|
7077
|
-
console.log(GUTTER + dim("tip:
|
|
7078
|
-
console.log(GUTTER + dim("
|
|
7197
|
+
console.log(GUTTER + dim("tip: set this model's context size with /context-window —"));
|
|
7198
|
+
console.log(GUTTER + dim(" hosted providers don't report it, so gu assumes 24k tokens.\n"));
|
|
7079
7199
|
}
|
|
7080
7200
|
const t = await fetchOutputTokenTotals(cwd);
|
|
7081
7201
|
{ const g = pickGlobalTotal(t); if (g !== null) globalOutputTokens = g; }
|
|
7202
|
+
// The bar still said "coding model not set · /model" under the success line of the FIRST model
|
|
7203
|
+
// added on a fresh install, until the next turn repainted it (GH #397).
|
|
7204
|
+
noModelAtAll = after?.payload ? nothingToAnswerWith(after.payload) : false;
|
|
7205
|
+
if (after?.payload) agentMissing = after.payload.agentMissing === true;
|
|
7206
|
+
if (barPinned()) paintBar();
|
|
7082
7207
|
}
|
|
7083
7208
|
|
|
7084
7209
|
|
|
@@ -7181,11 +7306,13 @@ async function chooseModel() {
|
|
|
7181
7306
|
// choices while still occupying its URL, so sending this branch straight to the ADD flow
|
|
7182
7307
|
// would meet "already configured here" and dead-end. The manager fixes such a row, and falls
|
|
7183
7308
|
// through to add when there is genuinely nothing there.
|
|
7184
|
-
|
|
7309
|
+
// As the picker's header (GH #397), so "← back" re-opening this menu does not stack it.
|
|
7185
7310
|
const go = await pickFromList(
|
|
7186
|
-
[dim("leave it"), "add
|
|
7187
|
-
1
|
|
7311
|
+
[dim("leave it"), "add a provider — Claude API, OpenAI, Kimi, Ollama…"],
|
|
7312
|
+
1,
|
|
7313
|
+
{ header: () => [dim(" No coding model configured.")], headerRows: 1 }
|
|
7188
7314
|
);
|
|
7315
|
+
if (go !== 1) console.log(dim(" No coding model configured."));
|
|
7189
7316
|
if (go === 1 && (await manageCodingProviders({ back: true })) === BACK) return chooseModel();
|
|
7190
7317
|
if (go !== 1) console.log("");
|
|
7191
7318
|
return;
|
|
@@ -7194,16 +7321,10 @@ async function chooseModel() {
|
|
|
7194
7321
|
// provider manager is where another one is added (an Ollama box is "another URL…").
|
|
7195
7322
|
// #412 removed "change the Ollama server this machine uses…" from all three menus: it re-asked
|
|
7196
7323
|
// the agent, embedder and coder servers in one go, which confused people picking a model.
|
|
7197
|
-
|
|
7198
|
-
|
|
7199
|
-
|
|
7200
|
-
|
|
7201
|
-
0
|
|
7202
|
-
);
|
|
7203
|
-
if (go === 1 && (await manageCodingProviders({ back: true })) === BACK) return chooseModel();
|
|
7204
|
-
if (go !== 1) console.log("");
|
|
7205
|
-
return;
|
|
7206
|
-
}
|
|
7324
|
+
// GH #397: ONE model used to get its own screen ("Only one coding model is available: X ·
|
|
7325
|
+
// keep it / add or change a provider"). Set up from /agent-model first, that was the agent's
|
|
7326
|
+
// model — and the other 12 coders on the same provider were three menus away. The full menu
|
|
7327
|
+
// below already has "pick another model on <host>…", keys and add, so one model uses it too.
|
|
7207
7328
|
const active = sessionCodingModel || def;
|
|
7208
7329
|
// Arrow-key picker (↑/↓ + Enter) — pre-highlight the active model, tag the default, and
|
|
7209
7330
|
// name the backend when more than one is in play (the same model name can appear twice).
|
|
@@ -7212,6 +7333,9 @@ async function chooseModel() {
|
|
|
7212
7333
|
// OpenAI-compatible providers are both "OpenAI-compatible", so the kind names neither.
|
|
7213
7334
|
const { labels, tags, showTag } = codingChoiceLabels(choices, def);
|
|
7214
7335
|
const activeIdx = choices.findIndex((c) => c.id === active);
|
|
7336
|
+
// GH #397: "(default)" marked the account default while this folder used another model, and
|
|
7337
|
+
// nothing said which one runs here. The one in use is marked.
|
|
7338
|
+
if (activeIdx >= 0 && choices[activeIdx].id !== def) labels[activeIdx] += accent(" (in use here)");
|
|
7215
7339
|
// THE CURRENT CODER'S KEYS, ON THIS LIST (#248) — the manager ctrl+k opens, for a provider that
|
|
7216
7340
|
// takes a key (OpenAI-compatible, ollama.com). Before the add row, so no model row moves.
|
|
7217
7341
|
const activeChoice = choices[activeIdx >= 0 ? activeIdx : choices.findIndex((c) => c.id === def)];
|
|
@@ -7239,8 +7363,11 @@ async function chooseModel() {
|
|
|
7239
7363
|
// enough in name to have already been mixed up. The fix is to say which one THIS is — not to
|
|
7240
7364
|
// print the other two here: the agent model and the embedder are independent settings with
|
|
7241
7365
|
// their own commands, and listing them together implies a relationship that does not exist.
|
|
7242
|
-
|
|
7243
|
-
|
|
7366
|
+
const idx = await pickFromList(labels, activeIdx >= 0 ? activeIdx : 0, {
|
|
7367
|
+
footer: ESC_FOOTER,
|
|
7368
|
+
header: () => [dim(` Choose the CODING model — the one that writes code (${PICK_ARROWS}):`)],
|
|
7369
|
+
headerRows: 1,
|
|
7370
|
+
});
|
|
7244
7371
|
if (idx < 0) {
|
|
7245
7372
|
console.log(dim(" (cancelled)\n"));
|
|
7246
7373
|
return;
|
|
@@ -9164,7 +9291,14 @@ async function chooseEmbedModel() {
|
|
|
9164
9291
|
if (keyRequired) labels.push(actionRow(`API keys for ${host || currentUrl} — switch, add or remove…`));
|
|
9165
9292
|
const ADD = labels.length;
|
|
9166
9293
|
labels.push(actionRow("add a provider — OpenAI, Ollama…"));
|
|
9167
|
-
|
|
9294
|
+
// NOTHING SET IS SAID AS NOTHING SET (GH #397). "now nomic-embed-text:latest on 127.0.0.1:11434"
|
|
9295
|
+
// was the DEFAULT, printed as if it were in use — on a Mac with no Ollama, where /doctor said
|
|
9296
|
+
// "embedder (no model) — no server configured" a moment later. The default still drives the
|
|
9297
|
+
// cursor and the "pick another model on …" row; the header only claims what is set.
|
|
9298
|
+
const embedConfigured = isSet || Boolean(String(settings.ragEmbedUrl ?? "").trim());
|
|
9299
|
+
const head = embedConfigured
|
|
9300
|
+
? ` Embedding model (now ${current} on ${host || currentUrl})`
|
|
9301
|
+
: " Embedding model — none set up yet, so RAG is off. Pick one";
|
|
9168
9302
|
const hint = `(${PICK_ARROWS}):`;
|
|
9169
9303
|
const cols = Math.max(20, process.stdout.columns || 80);
|
|
9170
9304
|
console.log(dim(head.length + 2 + hint.length < cols ? `${head} ${hint}` : `${head}\n ${hint}`));
|
|
@@ -9218,7 +9352,7 @@ async function chooseEmbedModel() {
|
|
|
9218
9352
|
}
|
|
9219
9353
|
|
|
9220
9354
|
/** Save the agent model on this machine: the URL the API sends turns to, and the model name. */
|
|
9221
|
-
async function saveAgentModel(agentUrl, model, host) {
|
|
9355
|
+
async function saveAgentModel(agentUrl, model, host, extra = []) {
|
|
9222
9356
|
const md = await import("./model-doctor.mjs");
|
|
9223
9357
|
const ok = await withPendingStatus("pointing the agent model at it", () =>
|
|
9224
9358
|
md.saveWorkerSettings(apiBase, workerKey, { agentModelUrl: agentUrl, agentModelName: model, agentModel: model })
|
|
@@ -9229,6 +9363,7 @@ async function saveAgentModel(agentUrl, model, host) {
|
|
|
9229
9363
|
return false;
|
|
9230
9364
|
}
|
|
9231
9365
|
console.log(accent(` ✓ agent model → ${model}`) + dim(` · ${host}`));
|
|
9366
|
+
for (const l of extra) console.log(l);
|
|
9232
9367
|
console.log(dim(" takes effect on your next question.\n"));
|
|
9233
9368
|
return true;
|
|
9234
9369
|
}
|
|
@@ -9237,7 +9372,9 @@ async function saveAgentModel(agentUrl, model, host) {
|
|
|
9237
9372
|
* Ask for a key's NAME — and refuse the key itself as a name: names are printed wherever keys are
|
|
9238
9373
|
* listed, so a pasted secret there is a secret on screen (reported: "API key sk-E8Iv… ····3E66").
|
|
9239
9374
|
*/
|
|
9240
|
-
|
|
9375
|
+
// "(optional)" because nothing said so (GH #397): a bare "key name:" with no default reads as a
|
|
9376
|
+
// required field, while Enter has always meant "no name".
|
|
9377
|
+
async function askKeyName(prompt = " key name (optional): ") {
|
|
9241
9378
|
const am = await import("./agent-model-command.mjs");
|
|
9242
9379
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
9243
9380
|
const name = (await ask(prompt)).trim();
|
|
@@ -9258,15 +9395,18 @@ async function askKeyName(prompt = " key name: ") {
|
|
|
9258
9395
|
* "no key" (a local vLLM answers anonymously). `host` names the server in the prompt and decides
|
|
9259
9396
|
* the shape check's locality: a local server accepts any string, so "abc" is not questioned there.
|
|
9260
9397
|
*/
|
|
9261
|
-
async function askServerKey({ host = "", optional = false } = {}) {
|
|
9398
|
+
async function askServerKey({ host = "", optional = false, escBack = false, name: knownName = null } = {}) {
|
|
9262
9399
|
const ps = await import("./coding-provider-setup.mjs");
|
|
9263
9400
|
// Esc is said here (#249): the prompt that follows is bare text, with nothing on it saying a
|
|
9264
9401
|
// way out exists — Esc cancels the whole add, nothing saved. (The line explaining what a key
|
|
9265
9402
|
// name is for was removed on request.)
|
|
9266
|
-
|
|
9267
|
-
|
|
9268
|
-
console.log(GUTTER + dim(
|
|
9269
|
-
const
|
|
9403
|
+
// GH #397: where a provider list is behind this prompt, Esc returns to it — and says so.
|
|
9404
|
+
// A RETRY KEEPS THE NAME (GH #397): only the secret was wrong, so only the secret is asked.
|
|
9405
|
+
if (knownName === null) console.log("\n" + GUTTER + dim(escBack ? "Esc goes back to the providers." : "Esc to cancel."));
|
|
9406
|
+
const name = knownName === null ? (await askKeyName()).trim() : String(knownName);
|
|
9407
|
+
const secret = (
|
|
9408
|
+
await askSecret(` API key${host ? ` for ${host}` : ""}${name ? ` (${name})` : ""}: `, { hint: [SECRET_KEY_HINT] })
|
|
9409
|
+
).trim();
|
|
9270
9410
|
if (!secret) {
|
|
9271
9411
|
console.log(GUTTER + dim(optional ? "(no key — the server will be called anonymously)" : "(cancelled — nothing was saved)\n"));
|
|
9272
9412
|
return { name, secret: "" };
|
|
@@ -9354,19 +9494,38 @@ async function chooseAgentModel() {
|
|
|
9354
9494
|
const source = am.agentListSource(settings);
|
|
9355
9495
|
const needsKey = am.agentSourceNeedsKey(source);
|
|
9356
9496
|
for (;;) {
|
|
9357
|
-
|
|
9497
|
+
// The first row is the default and the way back to ONE model: the coding model talks too (GH #397).
|
|
9498
|
+
const labels = [
|
|
9499
|
+
`same as the coding model${dim(" — follows /model")}` + (current ? "" : accent(" (current)")),
|
|
9500
|
+
...saved.map((c) => c.label + (c.current ? accent(" (current)") : "")),
|
|
9501
|
+
];
|
|
9358
9502
|
const MODELS = curHost ? labels.length : -1;
|
|
9359
9503
|
if (curHost) labels.push(actionRow(`pick another model on ${curHost}…`));
|
|
9504
|
+
// NO SEPARATE AGENT MODEL YET: offer the coder's own server, so a talking model from the provider
|
|
9505
|
+
// already set up is one pick away — its saved key is reused (GH #397).
|
|
9506
|
+
const coderUrl = (await import("./coding-provider-setup.mjs")).ensureV1Root(String(sessionCodingUrl || defaultCodingUrl || "")).toLowerCase();
|
|
9507
|
+
const coderRow = curHost ? null : (saved.find((c) => c.url.toLowerCase() === coderUrl) ?? null);
|
|
9508
|
+
const CODER_MODELS = coderRow ? labels.length : -1;
|
|
9509
|
+
if (coderRow) labels.push(actionRow(`pick another model on ${coderRow.host}…`));
|
|
9360
9510
|
const KEYS = curHost && needsKey ? labels.length : -1;
|
|
9361
9511
|
if (KEYS >= 0) labels.push(actionRow(`API keys for ${curHost} — switch, add or remove…`));
|
|
9362
9512
|
const ADD = labels.length;
|
|
9363
9513
|
labels.push(actionRow("add a provider — Claude API, OpenAI, Kimi, Ollama…"));
|
|
9364
9514
|
// The hint drops to its own row when the header would wrap (80 columns over SSH).
|
|
9365
|
-
|
|
9515
|
+
// GH #397: "(now unset)" hid that the coding model answers in the meantime; and printed
|
|
9516
|
+
// before the list, this title stacked each time "← back" re-opened the menu.
|
|
9517
|
+
const now = current
|
|
9518
|
+
? `now ${current}${curHost ? ` on ${curHost}` : ""}`
|
|
9519
|
+
: "same as the coding model";
|
|
9520
|
+
const head = ` Agent model — the one that TALKS (${now})`;
|
|
9366
9521
|
const hint = `(${PICK_ARROWS}):`;
|
|
9367
9522
|
const cols = Math.max(20, process.stdout.columns || 80);
|
|
9368
|
-
|
|
9369
|
-
const idx = await pickFromList(labels,
|
|
9523
|
+
const headLines = head.length + 2 + hint.length < cols ? [`${head} ${hint}`] : [head, ` ${hint}`];
|
|
9524
|
+
const idx = await pickFromList(labels, saved.findIndex((c) => c.current) + 1, {
|
|
9525
|
+
footer: ESC_FOOTER,
|
|
9526
|
+
header: () => headLines.map((l) => dim(l)),
|
|
9527
|
+
headerRows: headLines.length,
|
|
9528
|
+
});
|
|
9370
9529
|
if (idx < 0) {
|
|
9371
9530
|
console.log(dim(" (cancelled)\n"));
|
|
9372
9531
|
return;
|
|
@@ -9376,6 +9535,11 @@ async function chooseAgentModel() {
|
|
|
9376
9535
|
if ((await agentModelOnServer({ back: true, start: "models" })) === BACK) continue;
|
|
9377
9536
|
return;
|
|
9378
9537
|
}
|
|
9538
|
+
if (idx === CODER_MODELS) {
|
|
9539
|
+
const dialect = coderRow.kind === "ollama" ? "ollama" : "openai";
|
|
9540
|
+
if ((await addServer({ role: "agent", dialect, url: coderRow.url, back: true })) === BACK) continue;
|
|
9541
|
+
return;
|
|
9542
|
+
}
|
|
9379
9543
|
if (idx === KEYS) {
|
|
9380
9544
|
const rowId = String(source.row?.id ?? "").trim() || String(source.row?.kind ?? "").trim();
|
|
9381
9545
|
if (!rowId) return enterAgentProviderKey(source.row, curHost);
|
|
@@ -9386,7 +9550,12 @@ async function chooseAgentModel() {
|
|
|
9386
9550
|
if ((await addServer({ role: "agent", dialect: "openai", back: true })) === BACK) continue;
|
|
9387
9551
|
return;
|
|
9388
9552
|
}
|
|
9389
|
-
|
|
9553
|
+
if (idx === 0) {
|
|
9554
|
+
if (!current) console.log(dim(" already the same as the coding model — nothing changed.\n"));
|
|
9555
|
+
else await useCoderForAgent();
|
|
9556
|
+
return;
|
|
9557
|
+
}
|
|
9558
|
+
await useSavedAgentChoice(saved[idx - 1]);
|
|
9390
9559
|
return;
|
|
9391
9560
|
}
|
|
9392
9561
|
}
|
|
@@ -9396,6 +9565,31 @@ async function chooseAgentModel() {
|
|
|
9396
9565
|
* API finds the key by the URL (resolveKeyForRole), so nothing is re-entered — unless the server
|
|
9397
9566
|
* needs a key and has none, which is asked here rather than failing on the next question.
|
|
9398
9567
|
*/
|
|
9568
|
+
/**
|
|
9569
|
+
* "Same as the coding model" (GH #397): clear the separate agent model, so the coding model talks
|
|
9570
|
+
* too — and keeps following each folder's /model pick. The way BACK to one model: before this the
|
|
9571
|
+
* only route was removing the whole provider (and its key).
|
|
9572
|
+
*/
|
|
9573
|
+
async function useCoderForAgent() {
|
|
9574
|
+
const md = await import("./model-doctor.mjs");
|
|
9575
|
+
const saved = await withPendingStatus("saving", () =>
|
|
9576
|
+
md.saveWorkerSettings(apiBase, workerKey, { agentModelUrl: "", agentModelName: "", agentModel: "" })
|
|
9577
|
+
);
|
|
9578
|
+
if (!saved) {
|
|
9579
|
+
console.log(red(" couldn't save that — nothing changed.\n"));
|
|
9580
|
+
return;
|
|
9581
|
+
}
|
|
9582
|
+
if (sessionAgentKeyId) await saveFolderKeyPick("agent", "");
|
|
9583
|
+
console.log(accent(" ✓ agent model → same as the coding model") + dim(" · /model switches both"));
|
|
9584
|
+
console.log(GUTTER + dim("takes effect on your next question.\n"));
|
|
9585
|
+
const after = (await fetchAgentPayload([{ role: "user", content: "." }]).catch(() => null))?.payload;
|
|
9586
|
+
if (after) {
|
|
9587
|
+
agentMissing = after.agentMissing === true;
|
|
9588
|
+
noModelAtAll = nothingToAnswerWith(after);
|
|
9589
|
+
}
|
|
9590
|
+
if (barPinned()) paintBar();
|
|
9591
|
+
}
|
|
9592
|
+
|
|
9399
9593
|
async function useSavedAgentChoice(c) {
|
|
9400
9594
|
if (c.current) {
|
|
9401
9595
|
console.log(dim(` already using ${c.model} on ${c.host} — nothing changed.\n`));
|
|
@@ -9499,7 +9693,7 @@ async function agentModelOnServer({ back = false, start = "menu" } = {}) {
|
|
|
9499
9693
|
const options = am.agentRecoveryOptions({ keyProblem, host: listHost });
|
|
9500
9694
|
const go = await pickFromList(
|
|
9501
9695
|
options.map((o, i) => (i === 0 ? dim(o.label) : o.label)),
|
|
9502
|
-
|
|
9696
|
+
am.agentRecoveryDefault(options, { keyProblem, configured: Boolean(url || current) }),
|
|
9503
9697
|
{ back }
|
|
9504
9698
|
);
|
|
9505
9699
|
if (go === PICK_BACK) return BACK;
|
|
@@ -10829,8 +11023,7 @@ async function replaceCodingKey({ back = false } = {}) {
|
|
|
10829
11023
|
(ks.stored ? dim("(replacing ") + accent(maskKey(ks.stored)) + dim(")") : dim("(none stored)")) +
|
|
10830
11024
|
dim(" · Enter alone cancels, \"clear\" removes it\n")
|
|
10831
11025
|
);
|
|
10832
|
-
|
|
10833
|
-
const typed = (await askSecret(` ${ks.model} API key: `)).trim();
|
|
11026
|
+
const typed = (await askSecret(` ${ks.model} API key: `, { hint: [SECRET_KEY_HINT] })).trim();
|
|
10834
11027
|
if (!typed) {
|
|
10835
11028
|
console.log(GUTTER + dim("(cancelled — the stored key is unchanged)\n"));
|
|
10836
11029
|
paintBar();
|
|
@@ -10884,7 +11077,9 @@ async function manageServerKeys({ role = "coding", backendId, host, model = "",
|
|
|
10884
11077
|
});
|
|
10885
11078
|
const ADD = rows.length;
|
|
10886
11079
|
rows.push(dim("add a new key…"));
|
|
10887
|
-
|
|
11080
|
+
// The agent model is a MACHINE setting (GH #397): "proj's agent model" named a folder it is not tied to.
|
|
11081
|
+
const whose = role === "agent" ? `the ${roleLabel}` : `${folderLabel()}'s ${roleLabel}`;
|
|
11082
|
+
console.log("\n" + GUTTER + dim(`Keys for ${host} — ${whose}${model ? ` (${model})` : ""}\n`));
|
|
10888
11083
|
console.log(GUTTER + dim("Enter chooses · ctrl+d deletes the highlighted key\n"));
|
|
10889
11084
|
const picked = await pickFromList(rows, Math.max(0, keys.findIndex((k) => k.id === inUse?.id)), { allowDelete: true, back });
|
|
10890
11085
|
const idx = picked?.index ?? -1;
|
|
@@ -11812,6 +12007,28 @@ function pickFromList(items, initialSel = 0, opts = {}) {
|
|
|
11812
12007
|
listTimeoutIndex = -1;
|
|
11813
12008
|
listHeader = typeof opts.header === "function" ? opts.header : null;
|
|
11814
12009
|
listHeaderRows = listHeader ? Math.max(0, opts.headerRows | 0) : 0;
|
|
12010
|
+
// A STATIC HEADER WIDER THAN THE TERMINAL IS WRAPPED AT WORDS (GH #397), not clipped: on 80
|
|
12011
|
+
// columns "Which model on token-plan.maas.qwencloudapi.com? (↑/↓ then Enter · Esc to c" lost
|
|
12012
|
+
// its way out. Done once, here, so the block keeps the constant height its redraw walks
|
|
12013
|
+
// depend on. A live panel (refreshMs) is already sized by its caller and is left alone.
|
|
12014
|
+
if (listHeader && !(opts.refreshMs > 0)) {
|
|
12015
|
+
const w = Math.max(20, process.stdout.columns || 80) - 1;
|
|
12016
|
+
let given = [];
|
|
12017
|
+
try { given = (listHeader(listHeaderRows) || []).slice(0, listHeaderRows); } catch { /* blank */ }
|
|
12018
|
+
if (given.some((l) => visibleLen(l) > w)) {
|
|
12019
|
+
const wrapped = given.flatMap((l) => {
|
|
12020
|
+
if (visibleLen(l) <= w) return [l];
|
|
12021
|
+
const plain = stripAnsiEscapes(l);
|
|
12022
|
+
const lead = /^\s*/.exec(plain)[0];
|
|
12023
|
+
// The key hint " (↑/↓ then Enter · Esc …)" moves to its own line whole, before any word wrap.
|
|
12024
|
+
const at = plain.indexOf(" (", lead.length);
|
|
12025
|
+
const parts = at > 0 ? [plain.slice(lead.length, at), plain.slice(at + 2)] : [plain.slice(lead.length)];
|
|
12026
|
+
return parts.flatMap((p) => wrapIndented(p, { width: w, indent: lead, hang: "" })).map((r) => dim(r));
|
|
12027
|
+
});
|
|
12028
|
+
listHeader = () => wrapped;
|
|
12029
|
+
listHeaderRows = wrapped.length;
|
|
12030
|
+
}
|
|
12031
|
+
}
|
|
11815
12032
|
if (listHeader && opts.refreshMs > 0) {
|
|
11816
12033
|
// A live panel (#376): redrawn in place until the picker closes.
|
|
11817
12034
|
const live = setInterval(() => {
|
|
@@ -12290,6 +12507,7 @@ async function localScaffoldState() {
|
|
|
12290
12507
|
/** Print the startup scaffold line — deferred while a turn owns the transcript. */
|
|
12291
12508
|
function announceScaffold() {
|
|
12292
12509
|
if (!scaffoldNotice || following) return;
|
|
12510
|
+
if (listPickerActive || askWaiting || !inputVisible) return; // as announceCliUpdate (GH #397)
|
|
12293
12511
|
const { kind, text } = scaffoldNotice;
|
|
12294
12512
|
scaffoldNotice = null;
|
|
12295
12513
|
if (kind === "link") {
|
|
@@ -14201,7 +14419,12 @@ async function main() {
|
|
|
14201
14419
|
// local mongodb is unreachable. Cloud mode failing to reach the hosted API means the
|
|
14202
14420
|
// computer has lost its internet connection. Two different messages, one per mode.
|
|
14203
14421
|
dbUnreachable = true;
|
|
14204
|
-
if (isLocalApiBase(apiBase)) {
|
|
14422
|
+
if (isLocalApiBase(apiBase) && (isNetworkError(err) || isTimeout(err))) {
|
|
14423
|
+
// THE API ITSELF IS NOT ANSWERING (GH #397) — MongoDB was not even asked. Blaming the
|
|
14424
|
+
// database here sent the reader to check mongod while it was fine; the message right after
|
|
14425
|
+
// this one names the API and starts it.
|
|
14426
|
+
console.error(dim("(the local API isn't answering — your saved model/key choice is unavailable)"));
|
|
14427
|
+
} else if (isLocalApiBase(apiBase)) {
|
|
14205
14428
|
console.error(dim("(cannot access local mongodb — your saved model/key choice is unavailable)"));
|
|
14206
14429
|
} else {
|
|
14207
14430
|
console.error(dim("(your computer has lost its internet connection — your saved model/key choice is unavailable)"));
|
|
@@ -14337,7 +14560,11 @@ async function main() {
|
|
|
14337
14560
|
await import("./agent-model-label.mjs");
|
|
14338
14561
|
// Skip the `ps` sweep entirely when settings already name the model — the overwhelmingly
|
|
14339
14562
|
// common case, and startup is not the place to spend a subprocess proving it.
|
|
14340
|
-
|
|
14563
|
+
// NO AGENT MODEL SET: the payload's agentModelId is only the account-default coder, which may
|
|
14564
|
+
// not be this folder's pick — naming it here said "kimi" over a folder running qwen (GH #397).
|
|
14565
|
+
const agentLabel = p.agentMissing === true
|
|
14566
|
+
? { name: "", known: false, reason: "not-set" }
|
|
14567
|
+
: namesAModel(p.agentModelId)
|
|
14341
14568
|
? { name: String(p.agentModelId).trim(), known: true, reason: "settings" }
|
|
14342
14569
|
: agentModelLabel({
|
|
14343
14570
|
configuredId: p.agentModelId,
|
|
@@ -14598,7 +14825,7 @@ async function main() {
|
|
|
14598
14825
|
dim(" Skipped — this machine won't SERVE models; the agent still runs here and\n") +
|
|
14599
14826
|
dim(" calls your model machine over the network. Point the Agent model URL at\n") +
|
|
14600
14827
|
dim(" that machine (not 127.0.0.1) — `gu status` shows what it resolves to.\n") +
|
|
14601
|
-
dim(" `gu doctor` sets models up here ·
|
|
14828
|
+
dim(" `gu doctor` sets models up here · `gu clear` starts setup over.\n")
|
|
14602
14829
|
);
|
|
14603
14830
|
void st;
|
|
14604
14831
|
}
|
|
@@ -14772,6 +14999,19 @@ async function main() {
|
|
|
14772
14999
|
);
|
|
14773
15000
|
} else {
|
|
14774
15001
|
const started = await tryStartLocalApi();
|
|
15002
|
+
// CARRY ON INSTEAD OF "run gu again" (GH #397). tryStartLocalApi waited for the API to
|
|
15003
|
+
// answer, so the only thing left is the startup this process already abandoned — and the
|
|
15004
|
+
// simplest correct way to redo it is to BE a fresh gu: execve keeps the pid, the terminal
|
|
15005
|
+
// and the arguments. Where execve is missing (Node < 22.15, Windows) the old sentence stays.
|
|
15006
|
+
// ONCE: an API that answers the health check and then dies would otherwise loop forever.
|
|
15007
|
+
if (started && !process.env.GU_API_REOPENED && typeof process.execve === "function" && process.platform !== "win32" && process.stdin.isTTY) {
|
|
15008
|
+
console.error(dim(" Started it — opening gu…\n"));
|
|
15009
|
+
try {
|
|
15010
|
+
process.execve(process.execPath, [process.execPath, ...process.execArgv, ...process.argv.slice(1)], { ...process.env, GU_API_REOPENED: "1" });
|
|
15011
|
+
} catch {
|
|
15012
|
+
/* not allowed here — fall through to the sentence below */
|
|
15013
|
+
}
|
|
15014
|
+
}
|
|
14775
15015
|
console.error(
|
|
14776
15016
|
started
|
|
14777
15017
|
? dim(" Started it — run `gu` again.\n")
|
|
@@ -14794,6 +15034,7 @@ async function main() {
|
|
|
14794
15034
|
}
|
|
14795
15035
|
process.exit(1);
|
|
14796
15036
|
}
|
|
15037
|
+
delete process.env.GU_API_REOPENED; // GH #397: started — the next start may re-open again
|
|
14797
15038
|
console.log(wrapGutter("Ask about this repo. Type / to see commands (Tab completes). Ctrl-C aborts a running turn.", dim) + "\n");
|
|
14798
15039
|
// #221: gu runs, but its own npm folder is not on PATH (npm skipped the postinstall that adds it).
|
|
14799
15040
|
try {
|
|
@@ -15326,18 +15567,6 @@ async function main() {
|
|
|
15326
15567
|
);
|
|
15327
15568
|
continue;
|
|
15328
15569
|
}
|
|
15329
|
-
if (line === "/host-mode") {
|
|
15330
|
-
const ctl2 = await import("./daemon-control.mjs");
|
|
15331
|
-
await ctl2.setClientMode(false);
|
|
15332
|
-
await saveSessionSkip(cwd, "skipModelSetupPrompt", false);
|
|
15333
|
-
await saveSessionSkip(cwd, "skipDaemonPrompt", false);
|
|
15334
|
-
console.log(
|
|
15335
|
-
accent("host mode") +
|
|
15336
|
-
dim(" — this machine will run turns again. Start the worker with `gu start`, ") +
|
|
15337
|
-
dim("and `gu doctor` to set up models here.\n")
|
|
15338
|
-
);
|
|
15339
|
-
continue;
|
|
15340
|
-
}
|
|
15341
15570
|
if (line === "/rag" || line === "/rag-cloud" || line === "/rag-local") {
|
|
15342
15571
|
// ONE QUESTION, THREE ANSWERS (task #180). This was two commands that toggled one flag,
|
|
15343
15572
|
// so the command you typed did not determine the state you got — and the reply named a
|