@tiens.nguyen/gu-cli 1.0.796 → 1.0.797
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/gonext_agent_chat.py +204 -23
- package/gu-cli.mjs +37 -2
- package/gu-repl.mjs +189 -176
- package/log-level.mjs +193 -0
- package/package.json +2 -1
package/gu-repl.mjs
CHANGED
|
@@ -588,13 +588,15 @@ if (cliVerdict.kind === "run") {
|
|
|
588
588
|
} else if (argv.includes("--help") || argv.includes("-h")) {
|
|
589
589
|
console.log(
|
|
590
590
|
"gu — terminal agent REPL for the current folder\n\n" +
|
|
591
|
-
"Usage: gu [-v|--verbose] [--license <pro token>]\n" +
|
|
591
|
+
"Usage: gu [-v|--verbose] [--log] [--license <pro token>]\n" +
|
|
592
592
|
" gu status # worker + a table of the running model servers\n" +
|
|
593
593
|
" gu start|stop|restart|logs # manage the background worker\n" +
|
|
594
594
|
" gu logs <port|pid> [-f] # tail a running model server's log\n" +
|
|
595
595
|
" gu doctor|login|set # models / account (same as `gu-cli …`)\n" +
|
|
596
596
|
" gu rag # is RAG working? tests the embedder end-to-end\n\n" +
|
|
597
597
|
" -v, --verbose show RAG index-file activity in the bottom bar (each shard written)\n" +
|
|
598
|
+
" --log log this run in detail (~/.gonext/worker.log). Without it only starts,\n" +
|
|
599
|
+
" stops and errors are logged. Ends when this gu closes.\n" +
|
|
598
600
|
" --license <token> pro license: a first install ASKS how this machine runs (hosted\n" +
|
|
599
601
|
" client / MLX host / private). Without it, a first install is\n" +
|
|
600
602
|
" private by default — the whole stack on this machine.\n" +
|
|
@@ -614,6 +616,17 @@ if (cliVerdict.kind === "run") {
|
|
|
614
616
|
// could not deliver, because agent LOG events stay on the worker's console and never enter
|
|
615
617
|
// the job stream. It now drives a real, streamable signal.
|
|
616
618
|
const REPL_VERBOSE = argv.includes("-v") || argv.includes("--verbose");
|
|
619
|
+
// --log (task #352): THIS RUN logs in detail. By default the worker writes only starts, stops and
|
|
620
|
+
// errors; a run started with --log takes a lease (its pid, in ~/.gonext/log-detail.json) that the
|
|
621
|
+
// worker reads, and gives it back on exit. Per run by the owner's rule — nothing is saved, and a
|
|
622
|
+
// run that is killed leaves a lease for a dead process, which is no lease.
|
|
623
|
+
const REPL_LOG = argv.includes("--log");
|
|
624
|
+
if (REPL_LOG) {
|
|
625
|
+
const ll = await import("./log-level.mjs");
|
|
626
|
+
await ll.takeLease().catch(() => {});
|
|
627
|
+
process.on("exit", () => ll.releaseLeaseSync());
|
|
628
|
+
console.log(dim(" --log: detailed logging is on for this run (~/.gonext/worker.log); back to minimal when gu closes."));
|
|
629
|
+
}
|
|
617
630
|
if (REPL_VERBOSE) {
|
|
618
631
|
process.env.GONEXT_REPL_VERBOSE = "1";
|
|
619
632
|
}
|
|
@@ -2446,10 +2459,10 @@ async function runUpdate() {
|
|
|
2446
2459
|
if (cli) rows.push({ id: "cli", label: `Update gu to ${cli}`, note: "takes effect after you restart gu" });
|
|
2447
2460
|
if (!rows.length) return;
|
|
2448
2461
|
console.log("");
|
|
2449
|
-
console.log(dim(`
|
|
2462
|
+
console.log(dim(` Update (${PICK_HINT}):`));
|
|
2450
2463
|
const idx = await pickFromList(rows.map((r) => r.label + dim(` · ${r.note}`)), 0);
|
|
2451
2464
|
if (idx < 0) {
|
|
2452
|
-
console.log(dim("
|
|
2465
|
+
console.log(dim(" (cancelled)\n"));
|
|
2453
2466
|
paintBar();
|
|
2454
2467
|
return;
|
|
2455
2468
|
}
|
|
@@ -2476,7 +2489,7 @@ async function runUpdate() {
|
|
|
2476
2489
|
r.stderr ? ` — ${String(r.stderr).trim().split("\n").slice(-1)[0]}` : ""}`);
|
|
2477
2490
|
}
|
|
2478
2491
|
});
|
|
2479
|
-
console.log(accent(`
|
|
2492
|
+
console.log(accent(` ✓ API updated to ${api}\n`));
|
|
2480
2493
|
availableUpdates = { ...availableUpdates, api: "", any: Boolean(availableUpdates.cli) };
|
|
2481
2494
|
} else {
|
|
2482
2495
|
await withPendingStatus(`updating gu to ${cli}`, async () => {
|
|
@@ -2506,12 +2519,12 @@ async function runUpdate() {
|
|
|
2506
2519
|
});
|
|
2507
2520
|
// NOT "updated" — the files changed, THIS process did not. Claiming otherwise is the one
|
|
2508
2521
|
// way this feature could lie to someone.
|
|
2509
|
-
console.log(accent(`
|
|
2522
|
+
console.log(accent(` ✓ gu ${cli} installed`) +
|
|
2510
2523
|
dim(` — ${updateRestartNote()}\n`));
|
|
2511
2524
|
availableUpdates = { ...availableUpdates, cli: "", any: Boolean(availableUpdates.api) };
|
|
2512
2525
|
}
|
|
2513
2526
|
} catch (e) {
|
|
2514
|
-
console.log(red(`
|
|
2527
|
+
console.log(red(` update failed: ${e?.message ?? e}\n`));
|
|
2515
2528
|
}
|
|
2516
2529
|
paintBar();
|
|
2517
2530
|
}
|
|
@@ -4304,8 +4317,8 @@ if (process.stdin.isTTY) {
|
|
|
4304
4317
|
// PROCESS (reported: "PromptCancelled: cancelled … Node.js v22"). Cancel is caught here; any
|
|
4305
4318
|
// other failure is reported and the session goes on — a keypress must not kill the REPL.
|
|
4306
4319
|
replaceCodingKey().catch((e) => {
|
|
4307
|
-
if (wasPromptCancelled(e)) console.log(dim("
|
|
4308
|
-
else console.log(red(`
|
|
4320
|
+
if (wasPromptCancelled(e)) console.log(dim(" (cancelled)\n"));
|
|
4321
|
+
else console.log(red(` ctrl+k failed: ${e?.message ?? e}\n`));
|
|
4309
4322
|
paintBar();
|
|
4310
4323
|
});
|
|
4311
4324
|
return;
|
|
@@ -5653,10 +5666,10 @@ async function runModelDoctor() {
|
|
|
5653
5666
|
const dc = await import("./doctor-check.mjs");
|
|
5654
5667
|
const settings = await withPendingStatus("reading your model settings", fetchWorkerSettings);
|
|
5655
5668
|
if (!settings) {
|
|
5656
|
-
console.log(red("
|
|
5669
|
+
console.log(red(" couldn't reach the API to read your model settings.\n"));
|
|
5657
5670
|
return;
|
|
5658
5671
|
}
|
|
5659
|
-
console.log(dim("
|
|
5672
|
+
console.log(dim(" Asking each model to answer — the real request, not just a ping."));
|
|
5660
5673
|
const roles = dc.rolesFromSettings(settings);
|
|
5661
5674
|
const results = [];
|
|
5662
5675
|
for (const r of roles) {
|
|
@@ -5699,7 +5712,7 @@ async function reconfigureOllamaServer() {
|
|
|
5699
5712
|
|
|
5700
5713
|
const settings = await withPendingStatus("reading your model settings", fetchWorkerSettings);
|
|
5701
5714
|
if (!settings) {
|
|
5702
|
-
console.log(red("
|
|
5715
|
+
console.log(red(" couldn't reach the API to read your current settings.\n"));
|
|
5703
5716
|
return;
|
|
5704
5717
|
}
|
|
5705
5718
|
|
|
@@ -5728,7 +5741,7 @@ async function reconfigureOllamaServer() {
|
|
|
5728
5741
|
console.log(dim(` ${hint()}`));
|
|
5729
5742
|
// askSecret: masked, kept out of history; Esc throws PromptCancelled → /model is cancelled
|
|
5730
5743
|
// with nothing changed, which is what "skip all" means here.
|
|
5731
|
-
const k = (await askSecret(`
|
|
5744
|
+
const k = (await askSecret(` API key for ${host} (Enter for none): `)).trim();
|
|
5732
5745
|
typedKeys.set(host, k);
|
|
5733
5746
|
console.log(GUTTER + dim(ui.keyConfirmation(host, k)));
|
|
5734
5747
|
return k;
|
|
@@ -5744,7 +5757,7 @@ async function reconfigureOllamaServer() {
|
|
|
5744
5757
|
// instruction, leaving the reader unsure whether their own address needs it. It does not
|
|
5745
5758
|
// — normalizeOllamaUrl adds it — and displayUrl round-trips, so what is shown can be fed
|
|
5746
5759
|
// straight back as the default.
|
|
5747
|
-
console.log(dim(`
|
|
5760
|
+
console.log(dim(` ${label} — Enter keeps ${os.displayUrl(dflt)}`));
|
|
5748
5761
|
console.log(dim(` ${hint()}`));
|
|
5749
5762
|
// THE ROW MUST SAY WHICH SERVER IT IS ASKING FOR. It said a bare "server:" — chosen to
|
|
5750
5763
|
// fit the frozen row, which traded away the one thing the label was for. Two questions
|
|
@@ -5760,7 +5773,7 @@ async function reconfigureOllamaServer() {
|
|
|
5760
5773
|
// other. See urlCandidates; the load decides, not the heuristic.
|
|
5761
5774
|
const candidates = os.urlCandidates(typed || dflt);
|
|
5762
5775
|
if (!candidates.length) {
|
|
5763
|
-
console.log(dim("
|
|
5776
|
+
console.log(dim(" that is not a server address — try 192.168.1.40 or a full URL"));
|
|
5764
5777
|
continue;
|
|
5765
5778
|
}
|
|
5766
5779
|
let n = null;
|
|
@@ -5775,7 +5788,7 @@ async function reconfigureOllamaServer() {
|
|
|
5775
5788
|
try {
|
|
5776
5789
|
models = await load(cand, key);
|
|
5777
5790
|
n = cand;
|
|
5778
|
-
if (candidates.length > 1) console.log(dim(`
|
|
5791
|
+
if (candidates.length > 1) console.log(dim(` using ${os.displayUrl(cand.url)}`));
|
|
5779
5792
|
return;
|
|
5780
5793
|
} catch (err) {
|
|
5781
5794
|
lastErr = err;
|
|
@@ -5785,31 +5798,31 @@ async function reconfigureOllamaServer() {
|
|
|
5785
5798
|
await tryAll();
|
|
5786
5799
|
// Refused for want of credentials: ask now, whatever the address looked like, and list again.
|
|
5787
5800
|
if (!n && ui.needsKeyAfterStatus(lastErr?.httpStatus) && !key) {
|
|
5788
|
-
console.log(yellow(`
|
|
5801
|
+
console.log(yellow(` ${candidates[0].host} needs an API key to list its models.`));
|
|
5789
5802
|
typedKeys.delete(candidates[0].host);
|
|
5790
5803
|
key = await askKeyFor(candidates[0].host);
|
|
5791
5804
|
if (key) await tryAll();
|
|
5792
5805
|
}
|
|
5793
5806
|
if (!n && ui.needsKeyAfterStatus(lastErr?.httpStatus)) {
|
|
5794
|
-
console.log(red(`
|
|
5807
|
+
console.log(red(` ${candidates[0].host} refused the request (HTTP ${lastErr.httpStatus})${key ? " — that key was not accepted" : ""}.`));
|
|
5795
5808
|
continue;
|
|
5796
5809
|
}
|
|
5797
5810
|
if (!n) {
|
|
5798
5811
|
if (lastErr?.httpStatus === 404) {
|
|
5799
5812
|
// IT ANSWERED — it is simply not an Ollama (#43). "Couldn't reach … is Ollama listening"
|
|
5800
5813
|
// sent people to debug a server that was working.
|
|
5801
|
-
for (const l of am.describeNotOllama(candidates[0].host, role)) console.log(yellow(`
|
|
5814
|
+
for (const l of am.describeNotOllama(candidates[0].host, role)) console.log(yellow(` ${l}`));
|
|
5802
5815
|
continue;
|
|
5803
5816
|
}
|
|
5804
5817
|
// One address typed, one failure reported — naming every candidate would be two errors
|
|
5805
5818
|
// for one mistake, and neither would be the address they gave.
|
|
5806
|
-
console.log(red(`
|
|
5807
|
-
console.log(dim("
|
|
5819
|
+
console.log(red(` couldn't reach ${candidates[0].host}: ${os.networkReason(lastErr)}`));
|
|
5820
|
+
console.log(dim(" check the address, that Ollama is listening on the network, and /proxy."));
|
|
5808
5821
|
continue;
|
|
5809
5822
|
}
|
|
5810
5823
|
const usable = os.usableModelList(models);
|
|
5811
5824
|
if (!usable.ok) {
|
|
5812
|
-
for (const l of os.describeUnusableList(usable.why, n.host)) console.log(dim(`
|
|
5825
|
+
for (const l of os.describeUnusableList(usable.why, n.host)) console.log(dim(` ${l}`));
|
|
5813
5826
|
continue;
|
|
5814
5827
|
}
|
|
5815
5828
|
if (require) {
|
|
@@ -5818,13 +5831,13 @@ async function reconfigureOllamaServer() {
|
|
|
5818
5831
|
// rather than the server.
|
|
5819
5832
|
const need = os.checkRequired(models);
|
|
5820
5833
|
if (!need.ok) {
|
|
5821
|
-
for (const l of os.describeNotReady(n.host, need.missing)) console.log(yellow(`
|
|
5822
|
-
console.log(dim("
|
|
5834
|
+
for (const l of os.describeNotReady(n.host, need.missing)) console.log(yellow(` ${l}`));
|
|
5835
|
+
console.log(dim(" (or type s to skip — you can set it later with /model)"));
|
|
5823
5836
|
continue;
|
|
5824
5837
|
}
|
|
5825
|
-
console.log(dim(`
|
|
5838
|
+
console.log(dim(` ✓ ${n.host} has the ${os.REQUIRED_AGENT_PREFIX} family and the embedder`));
|
|
5826
5839
|
}
|
|
5827
|
-
console.log(dim(`
|
|
5840
|
+
console.log(dim(` found ${models.length} model${models.length === 1 ? "" : "s"} on ${n.host}`));
|
|
5828
5841
|
return { normalized: n, models };
|
|
5829
5842
|
}
|
|
5830
5843
|
return null;
|
|
@@ -5841,11 +5854,11 @@ async function reconfigureOllamaServer() {
|
|
|
5841
5854
|
let agent = null;
|
|
5842
5855
|
if (agentOnProvider) {
|
|
5843
5856
|
const host = hostOf(String(settings.agentModelUrl ?? ""));
|
|
5844
|
-
console.log(dim(`
|
|
5845
|
-
console.log(dim("
|
|
5857
|
+
console.log(dim(` The agent model is on ${host} (OpenAI-compatible) — kept as it is; change it with /agent-model.`));
|
|
5858
|
+
console.log(dim(" Enter keeps the current address."));
|
|
5846
5859
|
} else {
|
|
5847
|
-
console.log(dim("
|
|
5848
|
-
console.log(dim("
|
|
5860
|
+
console.log(dim(" TWO servers may be used: the agent + embedder on one, the coder on another."));
|
|
5861
|
+
console.log(dim(" Enter keeps the current address."));
|
|
5849
5862
|
agent = await askServer("Agent + embedding server", curAgent, {
|
|
5850
5863
|
require: true,
|
|
5851
5864
|
short: "agent + embed server",
|
|
@@ -5865,7 +5878,7 @@ async function reconfigureOllamaServer() {
|
|
|
5865
5878
|
}
|
|
5866
5879
|
|
|
5867
5880
|
if (!agent && !coder) {
|
|
5868
|
-
console.log(dim("
|
|
5881
|
+
console.log(dim(" nothing set — models stay as they are.\n"));
|
|
5869
5882
|
return;
|
|
5870
5883
|
}
|
|
5871
5884
|
|
|
@@ -5888,13 +5901,13 @@ async function reconfigureOllamaServer() {
|
|
|
5888
5901
|
const size = am.formatSize(m.sizeBytes);
|
|
5889
5902
|
return `${m.model}${size ? dim(` ${size}`) : ""}${m.model === agentDefault ? dim(" (current)") : ""}`;
|
|
5890
5903
|
});
|
|
5891
|
-
console.log(dim(`
|
|
5904
|
+
console.log(dim(` AGENT model — routes every turn, writes the plain replies (${PICK_HINT}):`));
|
|
5892
5905
|
const pick = await pickFromList(
|
|
5893
5906
|
labels,
|
|
5894
5907
|
Math.max(0, cands.findIndex((m) => m.model === agentDefault))
|
|
5895
5908
|
);
|
|
5896
5909
|
if (pick < 0) {
|
|
5897
|
-
console.log(dim("
|
|
5910
|
+
console.log(dim(" (cancelled — nothing changed)\n"));
|
|
5898
5911
|
return;
|
|
5899
5912
|
}
|
|
5900
5913
|
chat = cands[pick].model;
|
|
@@ -5913,10 +5926,10 @@ async function reconfigureOllamaServer() {
|
|
|
5913
5926
|
const kind = am.isLikelyEmbedder(m) ? dim(" · embedder") : "";
|
|
5914
5927
|
return `${m.model}${size ? dim(` ${size}`) : ""}${kind}${m.model === roles.code ? dim(" (current)") : ""}`;
|
|
5915
5928
|
});
|
|
5916
|
-
console.log(dim(`
|
|
5929
|
+
console.log(dim(` CODING model — writes the code; worth waiting for (${PICK_HINT}):`));
|
|
5917
5930
|
const idx = await pickFromList(labels, Math.max(0, coder.models.findIndex((m) => m.model === roles.code)));
|
|
5918
5931
|
if (idx < 0) {
|
|
5919
|
-
console.log(dim("
|
|
5932
|
+
console.log(dim(" (cancelled — nothing changed)\n"));
|
|
5920
5933
|
return;
|
|
5921
5934
|
}
|
|
5922
5935
|
code = coder.models[idx].model;
|
|
@@ -5931,7 +5944,7 @@ async function reconfigureOllamaServer() {
|
|
|
5931
5944
|
.flatMap((b) => (Array.isArray(b.models) ? b.models : []))
|
|
5932
5945
|
);
|
|
5933
5946
|
console.log(
|
|
5934
|
-
dim(`
|
|
5947
|
+
dim(` Also offer these in /model? (up to ${MAX_ALT_CODERS} · ${PICK_MANY_HINT}):`)
|
|
5935
5948
|
);
|
|
5936
5949
|
const ticks = await pickManyFromList(
|
|
5937
5950
|
rest.map((m) => `${m.model}${am.formatSize(m.sizeBytes) ? dim(` ${am.formatSize(m.sizeBytes)}`) : ""}`),
|
|
@@ -5940,7 +5953,7 @@ async function reconfigureOllamaServer() {
|
|
|
5940
5953
|
);
|
|
5941
5954
|
// null is Escape and cancels the command; an EMPTY list is a legitimate "offer none".
|
|
5942
5955
|
if (ticks === null) {
|
|
5943
|
-
console.log(dim("
|
|
5956
|
+
console.log(dim(" (cancelled — nothing changed)\n"));
|
|
5944
5957
|
return;
|
|
5945
5958
|
}
|
|
5946
5959
|
extras = ticks.map((i) => rest[i].model);
|
|
@@ -5986,7 +5999,7 @@ async function reconfigureOllamaServer() {
|
|
|
5986
5999
|
md.saveWorkerSettings(apiBase, workerKey, patch)
|
|
5987
6000
|
);
|
|
5988
6001
|
if (!saved) {
|
|
5989
|
-
console.log(red("
|
|
6002
|
+
console.log(red(" couldn't save that to this machine — nothing changed."));
|
|
5990
6003
|
console.log(GUTTER + dim("if this machine's local API is old, restart it with `gu api restart`.\n"));
|
|
5991
6004
|
return;
|
|
5992
6005
|
}
|
|
@@ -6006,13 +6019,13 @@ async function reconfigureOllamaServer() {
|
|
|
6006
6019
|
const url = used.find((u) => hostnameOf(u) === host);
|
|
6007
6020
|
const found = ps.providerRowForUrl(ps.existingBackendRows(fresh ?? {}), url);
|
|
6008
6021
|
if (!found) {
|
|
6009
|
-
console.log(yellow(`
|
|
6022
|
+
console.log(yellow(` no server row holds ${host} yet — add its key with ctrl+k in /model.`));
|
|
6010
6023
|
continue;
|
|
6011
6024
|
}
|
|
6012
6025
|
const ok = await withPendingStatus("saving the key", () => addCodingKey(host, k, found.id));
|
|
6013
6026
|
console.log(ok
|
|
6014
6027
|
? GUTTER + dim(`✓ key stored for ${host} — the server's default key`)
|
|
6015
|
-
: red(`
|
|
6028
|
+
: red(` couldn't store the key for ${host} — add it with ctrl+k.`));
|
|
6016
6029
|
}
|
|
6017
6030
|
}
|
|
6018
6031
|
}
|
|
@@ -6030,11 +6043,11 @@ async function reconfigureOllamaServer() {
|
|
|
6030
6043
|
for (const u of cloudUrls) {
|
|
6031
6044
|
const found = ps.providerRowForUrl(ps.existingBackendRows(fresh ?? {}), u);
|
|
6032
6045
|
if (found && !am.boundKeyFor(keys, found.id)) {
|
|
6033
|
-
console.log(yellow(`
|
|
6046
|
+
console.log(yellow(` ${hostOf(u)} is the Ollama cloud — it needs its own API key (the model list worked without one).`));
|
|
6034
6047
|
// The shared key prompt (task #245); the server's first key becomes its default.
|
|
6035
6048
|
const { name, secret } = await askServerKey({ host: hostOf(u) });
|
|
6036
6049
|
if (secret && !(await withPendingStatus("saving the key", () => addCodingKey(name, secret, found.id)))) {
|
|
6037
|
-
console.log(red("
|
|
6050
|
+
console.log(red(" couldn't save the key — add it with ctrl+k.\n"));
|
|
6038
6051
|
}
|
|
6039
6052
|
}
|
|
6040
6053
|
}
|
|
@@ -6070,7 +6083,7 @@ async function reconfigureOllamaServer() {
|
|
|
6070
6083
|
if (g !== null) globalOutputTokens = g;
|
|
6071
6084
|
}
|
|
6072
6085
|
paintBar(); // …and show all of it now, not at the next keystroke
|
|
6073
|
-
console.log(accent("
|
|
6086
|
+
console.log(accent(" ✓ models saved"));
|
|
6074
6087
|
if (agent) {
|
|
6075
6088
|
console.log(dim(` chat ${chat} @ ${agent.normalized.host}`));
|
|
6076
6089
|
console.log(dim(` RAG ${os.defaultEmbedModel?.(agent.models) ?? os.REQUIRED_EMBED_MODEL} @ ${agent.normalized.host}`));
|
|
@@ -6088,9 +6101,9 @@ async function reconfigureOllamaServer() {
|
|
|
6088
6101
|
// falsely scream "changed" while the seeded value is exactly what settings now hold.
|
|
6089
6102
|
const seededEmbed = agent ? (os.defaultEmbedModel?.(agent.models) ?? os.REQUIRED_EMBED_MODEL) : "";
|
|
6090
6103
|
if (agent && String(settings.ragEmbedModel ?? "") !== seededEmbed) {
|
|
6091
|
-
console.log(dim("
|
|
6104
|
+
console.log(dim(" the embedder changed — existing RAG indexes need rebuilding (/rag)."));
|
|
6092
6105
|
}
|
|
6093
|
-
console.log(dim("
|
|
6106
|
+
console.log(dim(" takes effect on your next question.\n"));
|
|
6094
6107
|
}
|
|
6095
6108
|
|
|
6096
6109
|
// ---------- fetch the ready-to-run agent payload from user settings ----------
|
|
@@ -6141,7 +6154,7 @@ async function reconfigureOllamaServer() {
|
|
|
6141
6154
|
*/
|
|
6142
6155
|
async function pickExtraModels(rest, preChecked = []) {
|
|
6143
6156
|
if (rest.length === 0) return [];
|
|
6144
|
-
console.log(dim(`
|
|
6157
|
+
console.log(dim(` Also offer these in /model? (up to ${MAX_ALT_CODERS} · ${PICK_MANY_HINT}):`));
|
|
6145
6158
|
const already = new Set(preChecked);
|
|
6146
6159
|
const ticks = await pickManyFromList(
|
|
6147
6160
|
rest,
|
|
@@ -6187,7 +6200,7 @@ async function chooseCodingModelFrom(ps, all, question, current) {
|
|
|
6187
6200
|
const idx = await pickFromList(rows, at >= 0 ? at : 0);
|
|
6188
6201
|
if (idx < 0) return null;
|
|
6189
6202
|
if (idx === list.length) {
|
|
6190
|
-
const typed = (await ask("
|
|
6203
|
+
const typed = (await ask(" model name: ")).trim();
|
|
6191
6204
|
return typed || null;
|
|
6192
6205
|
}
|
|
6193
6206
|
if (idx === list.length + 1) {
|
|
@@ -6215,11 +6228,11 @@ async function chooseCodingModelFrom(ps, all, question, current) {
|
|
|
6215
6228
|
async function chooseFromModelList(all, question, current = 0) {
|
|
6216
6229
|
const rows = [...all, "type a model name…"];
|
|
6217
6230
|
const at = typeof current === "string" ? all.indexOf(current) : Number(current) || 0;
|
|
6218
|
-
console.log(dim(`
|
|
6231
|
+
console.log(dim(` ${question} (${PICK_HINT})`));
|
|
6219
6232
|
const idx = await pickFromList(rows, at >= 0 ? at : 0);
|
|
6220
6233
|
if (idx < 0) return null;
|
|
6221
6234
|
if (idx === all.length) {
|
|
6222
|
-
const typed = (await ask("
|
|
6235
|
+
const typed = (await ask(" model name: ")).trim();
|
|
6223
6236
|
return typed || null;
|
|
6224
6237
|
}
|
|
6225
6238
|
return all[idx];
|
|
@@ -6229,7 +6242,7 @@ async function manageCodingProviders() {
|
|
|
6229
6242
|
const ps = await import("./coding-provider-setup.mjs");
|
|
6230
6243
|
const settings = await withPendingStatus("reading your providers", fetchWorkerSettings);
|
|
6231
6244
|
if (!settings) {
|
|
6232
|
-
console.log(red("
|
|
6245
|
+
console.log(red(" couldn't reach the API to read your current settings.\n"));
|
|
6233
6246
|
return;
|
|
6234
6247
|
}
|
|
6235
6248
|
const rows = ps.existingBackendRows(settings);
|
|
@@ -6246,10 +6259,10 @@ async function manageCodingProviders() {
|
|
|
6246
6259
|
);
|
|
6247
6260
|
const ADD = labels.length;
|
|
6248
6261
|
labels.push("add another provider — Claude API, OpenAI, Kimi…");
|
|
6249
|
-
console.log(dim(`
|
|
6262
|
+
console.log(dim(` Providers — pick one to change it, or add another (${PICK_HINT}):`));
|
|
6250
6263
|
const idx = await pickFromList(labels, 0);
|
|
6251
6264
|
if (idx < 0) {
|
|
6252
|
-
console.log(dim("
|
|
6265
|
+
console.log(dim(" (cancelled)\n"));
|
|
6253
6266
|
return;
|
|
6254
6267
|
}
|
|
6255
6268
|
if (idx === ADD) {
|
|
@@ -6264,7 +6277,7 @@ async function manageCodingProviders() {
|
|
|
6264
6277
|
1
|
|
6265
6278
|
);
|
|
6266
6279
|
if (action <= 0) {
|
|
6267
|
-
console.log(dim("
|
|
6280
|
+
console.log(dim(" (nothing changed)\n"));
|
|
6268
6281
|
return;
|
|
6269
6282
|
}
|
|
6270
6283
|
if (action === 1) await changeProviderModel(ps, rows, p);
|
|
@@ -6323,12 +6336,12 @@ async function changeProviderModel(ps, rows, p) {
|
|
|
6323
6336
|
if (models.length) {
|
|
6324
6337
|
model = await chooseCodingModelFrom(ps, models, `Which model should ${p.host} use?`, p.model);
|
|
6325
6338
|
if (model === null) {
|
|
6326
|
-
console.log(dim("
|
|
6339
|
+
console.log(dim(" (nothing changed)\n"));
|
|
6327
6340
|
return;
|
|
6328
6341
|
}
|
|
6329
6342
|
} else {
|
|
6330
6343
|
console.log(GUTTER + dim(`Enter keeps ${p.model || "it unset"}`));
|
|
6331
|
-
model = (await ask("
|
|
6344
|
+
model = (await ask(" model name: ")).trim();
|
|
6332
6345
|
}
|
|
6333
6346
|
if (!model) {
|
|
6334
6347
|
console.log(GUTTER + dim("(nothing changed)\n"));
|
|
@@ -6352,7 +6365,7 @@ async function changeProviderModel(ps, rows, p) {
|
|
|
6352
6365
|
})
|
|
6353
6366
|
);
|
|
6354
6367
|
if (!saved) {
|
|
6355
|
-
console.log(red("
|
|
6368
|
+
console.log(red(" couldn't save that — nothing changed.\n"));
|
|
6356
6369
|
return;
|
|
6357
6370
|
}
|
|
6358
6371
|
// THE FOLDER'S PICK NAMES THE OLD MODEL. Leaving it would send a name this provider no longer
|
|
@@ -6366,14 +6379,14 @@ async function changeProviderModel(ps, rows, p) {
|
|
|
6366
6379
|
await syncActiveCoderWhere();
|
|
6367
6380
|
console.log(GUTTER + dim("this folder's pick named the old model — cleared it."));
|
|
6368
6381
|
}
|
|
6369
|
-
console.log(accent(`
|
|
6382
|
+
console.log(accent(` ✓ ${p.host} → ${model}`) + dim(" · pick it in /model\n"));
|
|
6370
6383
|
}
|
|
6371
6384
|
|
|
6372
6385
|
/** Point a provider at a different URL, keeping its id so saved picks still resolve. */
|
|
6373
6386
|
async function changeProviderUrl(ps, rows, p) {
|
|
6374
6387
|
const md = await import("./model-doctor.mjs");
|
|
6375
6388
|
console.log("\n" + GUTTER + dim(`Enter keeps ${p.url}`));
|
|
6376
|
-
const typed = (await ask("
|
|
6389
|
+
const typed = (await ask(" provider URL: ")).trim();
|
|
6377
6390
|
if (!typed || typed === p.url) {
|
|
6378
6391
|
console.log(GUTTER + dim("(nothing changed)\n"));
|
|
6379
6392
|
return;
|
|
@@ -6398,12 +6411,12 @@ async function changeProviderUrl(ps, rows, p) {
|
|
|
6398
6411
|
})
|
|
6399
6412
|
);
|
|
6400
6413
|
if (!saved) {
|
|
6401
|
-
console.log(red("
|
|
6414
|
+
console.log(red(" couldn't save that — nothing changed.\n"));
|
|
6402
6415
|
return;
|
|
6403
6416
|
}
|
|
6404
6417
|
// The KEY still belongs to this row, because the row id did not change — which is the whole
|
|
6405
6418
|
// reason an id is not derived from the URL.
|
|
6406
|
-
console.log(accent(`
|
|
6419
|
+
console.log(accent(` ✓ ${p.host} → ${r.host}`) + dim(" · its key still applies\n"));
|
|
6407
6420
|
}
|
|
6408
6421
|
|
|
6409
6422
|
/** Remove a provider, and the keys that would be stranded by its removal. */
|
|
@@ -6436,7 +6449,7 @@ async function removeCodingProvider(ps, rows, p) {
|
|
|
6436
6449
|
})
|
|
6437
6450
|
);
|
|
6438
6451
|
if (!saved) {
|
|
6439
|
-
console.log(red("
|
|
6452
|
+
console.log(red(" couldn't save that — nothing was removed.\n"));
|
|
6440
6453
|
return;
|
|
6441
6454
|
}
|
|
6442
6455
|
// THE ROW GOES FIRST. If a key delete fails afterwards the key is merely orphaned, which is
|
|
@@ -6460,7 +6473,7 @@ async function removeCodingProvider(ps, rows, p) {
|
|
|
6460
6473
|
console.log(GUTTER + dim("this folder was using it — cleared the pick.\n"));
|
|
6461
6474
|
}
|
|
6462
6475
|
console.log(
|
|
6463
|
-
accent(`
|
|
6476
|
+
accent(` ✓ removed ${p.host}`) +
|
|
6464
6477
|
(stranded.length ? dim(` · ${deleted} key${deleted === 1 ? "" : "s"} deleted`) : "") +
|
|
6465
6478
|
"\n"
|
|
6466
6479
|
);
|
|
@@ -6501,7 +6514,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6501
6514
|
|
|
6502
6515
|
const settings = await withPendingStatus("reading your model settings", fetchWorkerSettings);
|
|
6503
6516
|
if (!settings) {
|
|
6504
|
-
console.log(red("
|
|
6517
|
+
console.log(red(" couldn't reach the API to read your current settings.\n"));
|
|
6505
6518
|
return;
|
|
6506
6519
|
}
|
|
6507
6520
|
// EVERY ROW THAT IS ALREADY THERE, including the synthesized one a pre-#123 account keeps in
|
|
@@ -6527,10 +6540,10 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6527
6540
|
// without Claude, which serves no embeddings).
|
|
6528
6541
|
const presets = ps.providerPresets(role);
|
|
6529
6542
|
const rowsShown = [...presets.map((p) => `${p.name}${dim(` · ${p.url}`)}`), "another URL…"];
|
|
6530
|
-
console.log(dim(`
|
|
6543
|
+
console.log(dim(` Which provider? (${PICK_HINT})`));
|
|
6531
6544
|
const pi = await pickFromList(rowsShown, 0);
|
|
6532
6545
|
if (pi < 0) {
|
|
6533
|
-
console.log(dim("
|
|
6546
|
+
console.log(dim(" (nothing saved)\n"));
|
|
6534
6547
|
return;
|
|
6535
6548
|
}
|
|
6536
6549
|
if (pi < presets.length) target = as.serverTarget("openai", presets[pi].url);
|
|
@@ -6538,12 +6551,12 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6538
6551
|
if (!target && !presetUrl) {
|
|
6539
6552
|
const dflt = as.defaultServerUrl(role, dialect, settings, existing);
|
|
6540
6553
|
console.log(dim(dialect === "ollama"
|
|
6541
|
-
? "
|
|
6542
|
-
: "
|
|
6554
|
+
? " An Ollama server: this machine, a box on your network, or https://ollama.com (needs a key)."
|
|
6555
|
+
: " Any OpenAI-compatible endpoint: OpenRouter, Together, vLLM, LM Studio, …"));
|
|
6543
6556
|
// The address goes ABOVE, in full, and Enter's meaning is said out loud (46d1524e).
|
|
6544
6557
|
console.log(dim(dflt ? ` Enter uses ${dflt} · Esc cancels\n` : " Esc cancels.\n"));
|
|
6545
6558
|
for (let attempt = 0; attempt < 3 && !target; attempt++) {
|
|
6546
|
-
const typed = await ask("
|
|
6559
|
+
const typed = await ask(" server URL: ");
|
|
6547
6560
|
if (typed === null || as.isCancelWord(typed)) break;
|
|
6548
6561
|
const raw = typed.trim() || dflt;
|
|
6549
6562
|
if (!raw) break;
|
|
@@ -6560,7 +6573,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6560
6573
|
}
|
|
6561
6574
|
}
|
|
6562
6575
|
if (!target) {
|
|
6563
|
-
console.log(dim("
|
|
6576
|
+
console.log(dim(" (nothing saved)\n"));
|
|
6564
6577
|
return;
|
|
6565
6578
|
}
|
|
6566
6579
|
const host = target.host;
|
|
@@ -6582,7 +6595,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6582
6595
|
// THE ROW CAP BEFORE ANY QUESTION about a new server: exceeding it is a silent 200 with no row,
|
|
6583
6596
|
// and asking for a key and a model first would be the worst order to discover that in.
|
|
6584
6597
|
if (!found && existing.length >= ps.MAX_BACKENDS) {
|
|
6585
|
-
console.log(yellow(`
|
|
6598
|
+
console.log(yellow(` ${ps.MAX_BACKENDS} servers is the limit — remove one in /model → manage providers first.\n`));
|
|
6586
6599
|
return;
|
|
6587
6600
|
}
|
|
6588
6601
|
const need = as.keyRequirement(target);
|
|
@@ -6627,7 +6640,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6627
6640
|
let probe = await list();
|
|
6628
6641
|
// THE STORED KEY WAS REFUSED — ask for the right one once, rather than saving nothing.
|
|
6629
6642
|
if ((probe.status === 401 || probe.status === 403) && !typedKey && key) {
|
|
6630
|
-
console.log(yellow(`
|
|
6643
|
+
console.log(yellow(` ${host} refused the stored key (HTTP ${probe.status}) — enter the right one.`));
|
|
6631
6644
|
if (!(await askKey()) || !key) return;
|
|
6632
6645
|
probe = await list();
|
|
6633
6646
|
}
|
|
@@ -6662,11 +6675,11 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6662
6675
|
? await chooseCodingModelFrom(ps, models, R.question, 0)
|
|
6663
6676
|
: await chooseFromModelList(models, R.question, Math.max(0, models.indexOf(current)));
|
|
6664
6677
|
if (model === null) {
|
|
6665
|
-
console.log(dim("
|
|
6678
|
+
console.log(dim(" (cancelled — nothing was saved)\n"));
|
|
6666
6679
|
return;
|
|
6667
6680
|
}
|
|
6668
6681
|
} else {
|
|
6669
|
-
model = String((await ask("
|
|
6682
|
+
model = String((await ask(" model name: ")) ?? "").trim();
|
|
6670
6683
|
}
|
|
6671
6684
|
if (!model) {
|
|
6672
6685
|
console.log(GUTTER + dim("(cancelled — nothing was saved)\n"));
|
|
@@ -6683,7 +6696,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6683
6696
|
if (!sw.changed) return;
|
|
6684
6697
|
plan = sw.plan;
|
|
6685
6698
|
} else if (as.serverTarget(target.dialect, String(settings.ragEmbedUrl ?? "")).root === target.root && !typedKey) {
|
|
6686
|
-
console.log(dim(`
|
|
6699
|
+
console.log(dim(` already embedding with ${model} on ${host} — nothing changed.\n`));
|
|
6687
6700
|
return;
|
|
6688
6701
|
}
|
|
6689
6702
|
}
|
|
@@ -6712,7 +6725,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6712
6725
|
} else {
|
|
6713
6726
|
const r = as.rowForSave(role, { existing: found.row, model });
|
|
6714
6727
|
if (r.full) {
|
|
6715
|
-
console.log(yellow(`
|
|
6728
|
+
console.log(yellow(` ${host} already offers ${ps.MAX_MODELS_PER_SERVER} models — remove one in /model → manage providers first.\n`));
|
|
6716
6729
|
return;
|
|
6717
6730
|
}
|
|
6718
6731
|
if (r.changed) rows = existing.map((b) => (b === found.row ? r.row : b));
|
|
@@ -6720,7 +6733,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6720
6733
|
if (rows !== existing) {
|
|
6721
6734
|
const ok = await withPendingStatus(`saving ${host}`, () => md.saveWorkerSettings(apiBase, workerKey, { agentCodingBackends: rows }));
|
|
6722
6735
|
if (!ok) {
|
|
6723
|
-
console.log(red(`
|
|
6736
|
+
console.log(red(` couldn't save ${host} — nothing changed.`));
|
|
6724
6737
|
console.log(GUTTER + dim("if this machine's local API is old, restart it with `gu api restart`.\n"));
|
|
6725
6738
|
return;
|
|
6726
6739
|
}
|
|
@@ -6729,7 +6742,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6729
6742
|
if (typedKey) {
|
|
6730
6743
|
const id = await withPendingStatus("saving the key", () => addCodingKey(keyName, key, rowId));
|
|
6731
6744
|
if (!id) {
|
|
6732
|
-
console.log(yellow(`
|
|
6745
|
+
console.log(yellow(` ${host} was saved, but its key was not — nothing else changed.`));
|
|
6733
6746
|
console.log(GUTTER + dim("try again, or add the key with ctrl+k.\n"));
|
|
6734
6747
|
return;
|
|
6735
6748
|
}
|
|
@@ -6747,7 +6760,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6747
6760
|
console.log(GUTTER + dim("the banner shows the new model next time gu starts.\n"));
|
|
6748
6761
|
} else if (rows !== existing || typedKey) {
|
|
6749
6762
|
// The SERVER landed — say so, or the user re-enters a key that is already stored.
|
|
6750
|
-
console.log(yellow(`
|
|
6763
|
+
console.log(yellow(` ${host} was saved, but the agent model was not pointed at it.`));
|
|
6751
6764
|
console.log(GUTTER + dim("run /agent-model to finish, or check the API is reachable.\n"));
|
|
6752
6765
|
}
|
|
6753
6766
|
return;
|
|
@@ -6760,7 +6773,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6760
6773
|
if (!(await saveEmbedSettings(fields))) return;
|
|
6761
6774
|
if (plan) announceEmbedSaved({ chosen: model, plan, url: target.roleUrl("embed") });
|
|
6762
6775
|
else {
|
|
6763
|
-
console.log(accent(`
|
|
6776
|
+
console.log(accent(` ✓ embedding server → ${host}`));
|
|
6764
6777
|
console.log(dim(` ${model} did not change, so no knowledge base rebuilds — the same model just answers from ${host}.\n`));
|
|
6765
6778
|
}
|
|
6766
6779
|
return;
|
|
@@ -6788,7 +6801,7 @@ async function useCoderInThisFolder({ ps, rows, beforeChoices, url, host, locali
|
|
|
6788
6801
|
(c) => c.model === model && (c.backendId ? c.backendId === backendId : sameHost(c.url, url))
|
|
6789
6802
|
);
|
|
6790
6803
|
if (!offered) {
|
|
6791
|
-
console.log(yellow(`
|
|
6804
|
+
console.log(yellow(` ${host} was saved, but this API is not offering ${model} yet.`));
|
|
6792
6805
|
console.log(GUTTER + dim("run /model — if it is missing there too, the API may be older than"));
|
|
6793
6806
|
console.log(GUTTER + dim("this feature; restart it with `gu api restart`.\n"));
|
|
6794
6807
|
return;
|
|
@@ -6802,7 +6815,7 @@ async function useCoderInThisFolder({ ps, rows, beforeChoices, url, host, locali
|
|
|
6802
6815
|
])
|
|
6803
6816
|
);
|
|
6804
6817
|
if (lost.length) {
|
|
6805
|
-
console.log(yellow(`
|
|
6818
|
+
console.log(yellow(` ⚠ this API dropped ${lost.length === 1 ? "a provider" : `${lost.length} providers`} when saving:`));
|
|
6806
6819
|
for (const u of lost) console.log(GUTTER + dim(` ${u}`));
|
|
6807
6820
|
if (ps.apiKeysBackendsByKind(after?.codingChoices)) {
|
|
6808
6821
|
console.log(GUTTER + dim("this API identifies a backend by its KIND, so it holds ONE"));
|
|
@@ -6826,8 +6839,8 @@ async function useCoderInThisFolder({ ps, rows, beforeChoices, url, host, locali
|
|
|
6826
6839
|
// The key question for this coder is answered — leaving the flag set would warn about a key
|
|
6827
6840
|
// that was just entered, which reads as the save having failed.
|
|
6828
6841
|
codingKeyMissingKnown = false;
|
|
6829
|
-
console.log(accent(`
|
|
6830
|
-
if (key) console.log(dim(`
|
|
6842
|
+
console.log(accent(` ✓ coding model → ${model}`) + dim(` · ${host}`));
|
|
6843
|
+
if (key) console.log(dim(` API key ${keyName || "saved"} ····${key.slice(-4)} · ${host}'s key\n`));
|
|
6831
6844
|
// A HOSTED SERVER NEEDS ITS WINDOW SET: with nothing set gu assumes 24,000 prompt tokens and
|
|
6832
6845
|
// compacts there — on a large hosted model that throws history away every turn.
|
|
6833
6846
|
if (ps.shouldMentionContextWindow(locality)) {
|
|
@@ -6847,7 +6860,7 @@ async function chooseModel() {
|
|
|
6847
6860
|
fetchAgentPayload([{ role: "user", content: "ping" }])
|
|
6848
6861
|
);
|
|
6849
6862
|
} catch (err) {
|
|
6850
|
-
console.log(red(`
|
|
6863
|
+
console.log(red(` ${describeApiFailure(err, "couldn't load models")}\n`));
|
|
6851
6864
|
return;
|
|
6852
6865
|
}
|
|
6853
6866
|
const allowed = Array.isArray(probe?.codingAllowed) ? probe.codingAllowed : [];
|
|
@@ -6868,7 +6881,7 @@ async function chooseModel() {
|
|
|
6868
6881
|
// choices while still occupying its URL, so sending this branch straight to the ADD flow
|
|
6869
6882
|
// would meet "already configured here" and dead-end. The manager fixes such a row, and falls
|
|
6870
6883
|
// through to add when there is genuinely nothing there.
|
|
6871
|
-
console.log(dim("
|
|
6884
|
+
console.log(dim(" No coding model configured."));
|
|
6872
6885
|
const go = await pickFromList(
|
|
6873
6886
|
[dim("leave it"), "add or change a provider — Claude API, OpenAI, Kimi…", "change the Ollama server this machine uses…"],
|
|
6874
6887
|
1
|
|
@@ -6886,7 +6899,7 @@ async function chooseModel() {
|
|
|
6886
6899
|
// symptom of being pointed at the wrong box, and the old message sent people to a web app a
|
|
6887
6900
|
// Client Expert machine does not have.
|
|
6888
6901
|
if (choices.length === 1) {
|
|
6889
|
-
console.log(dim(`
|
|
6902
|
+
console.log(dim(` Only one coding model is available: ${choices[0].model}.`));
|
|
6890
6903
|
// APPENDED AFTER the existing entry, here and below, so that entry keeps the index anyone
|
|
6891
6904
|
// who has used it already reaches for.
|
|
6892
6905
|
const go = await pickFromList(
|
|
@@ -6934,10 +6947,10 @@ async function chooseModel() {
|
|
|
6934
6947
|
// enough in name to have already been mixed up. The fix is to say which one THIS is — not to
|
|
6935
6948
|
// print the other two here: the agent model and the embedder are independent settings with
|
|
6936
6949
|
// their own commands, and listing them together implies a relationship that does not exist.
|
|
6937
|
-
console.log(dim(`
|
|
6950
|
+
console.log(dim(` Choose the CODING model — the one that writes code (${PICK_HINT}):`));
|
|
6938
6951
|
const idx = await pickFromList(labels, activeIdx >= 0 ? activeIdx : 0);
|
|
6939
6952
|
if (idx < 0) {
|
|
6940
|
-
console.log(dim("
|
|
6953
|
+
console.log(dim(" (cancelled)\n"));
|
|
6941
6954
|
return;
|
|
6942
6955
|
}
|
|
6943
6956
|
// The appended entries sit past the last model, so they are the only indices without a choice.
|
|
@@ -6967,7 +6980,7 @@ async function chooseModel() {
|
|
|
6967
6980
|
const where = showTag && !activeCoderWhere ? ` (${tags[idx]})` : "";
|
|
6968
6981
|
console.log(
|
|
6969
6982
|
accent(
|
|
6970
|
-
`
|
|
6983
|
+
` ✓ coding model → ${coderDisplayName(chosen.model, activeCoderWhere)}${where}${chosen.id === def ? " (default)" : ""}\n`
|
|
6971
6984
|
)
|
|
6972
6985
|
);
|
|
6973
6986
|
// DOES THIS ONE NEED AN API KEY? (task #181) Reported live on Windows 11 / Client Expert:
|
|
@@ -7020,7 +7033,7 @@ async function chooseModel() {
|
|
|
7020
7033
|
host: ks.provider,
|
|
7021
7034
|
});
|
|
7022
7035
|
console.log(
|
|
7023
|
-
dim("
|
|
7036
|
+
dim(" API key ") + accent(line.text) +
|
|
7024
7037
|
(line.note ? orange(` · ${line.note}`) : "") +
|
|
7025
7038
|
dim(` · ctrl+k to ${line.verb} it`)
|
|
7026
7039
|
);
|
|
@@ -7029,7 +7042,7 @@ async function chooseModel() {
|
|
|
7029
7042
|
// the more common one — "how do I change the model" — with no visible answer at all. Reported
|
|
7030
7043
|
// three times from exactly this view, twice after being told where the picker entry was.
|
|
7031
7044
|
// /provider is hidden (see HIDDEN_COMMANDS): point at the row of /model that does the same.
|
|
7032
|
-
console.log(dim("
|
|
7045
|
+
console.log(dim(" to change this provider's model or URL, or remove it: /model → add or change a provider\n"));
|
|
7033
7046
|
}
|
|
7034
7047
|
// Task #125: the GLOBAL lifetime total is per-backend, so re-fetch it for the newly
|
|
7035
7048
|
// selected backend NOW — otherwise the footer would keep showing the previous backend's
|
|
@@ -7383,13 +7396,13 @@ async function runSwitchSkillCommand(line) {
|
|
|
7383
7396
|
}
|
|
7384
7397
|
} else {
|
|
7385
7398
|
const options = ["claude", "gu"];
|
|
7386
|
-
console.log(dim(`
|
|
7399
|
+
console.log(dim(` Whose skills, MCP servers and hooks this folder uses (${PICK_HINT}):`));
|
|
7387
7400
|
const idx = await pickFromList(
|
|
7388
7401
|
options.map((o) => sourceLabel(o) + dim(` · ${where(o)}`) + (o === current ? dim(" · in use") : "")),
|
|
7389
7402
|
Math.max(0, options.indexOf(current))
|
|
7390
7403
|
);
|
|
7391
7404
|
if (idx < 0) {
|
|
7392
|
-
console.log(dim("
|
|
7405
|
+
console.log(dim(" (cancelled)\n"));
|
|
7393
7406
|
paintBar();
|
|
7394
7407
|
return;
|
|
7395
7408
|
}
|
|
@@ -7407,7 +7420,7 @@ async function runSwitchSkillCommand(line) {
|
|
|
7407
7420
|
_skillScan = { at: 0, skills: [] }; // the "/" menu and Tab follow at once
|
|
7408
7421
|
const say = (l) => console.log(GUTTER + l);
|
|
7409
7422
|
say(accent(`✓ ${pick === current ? "this folder uses" : "this folder now uses"} ${sourceLabel(pick)}`));
|
|
7410
|
-
say(dim(`
|
|
7423
|
+
say(dim(` skills, MCP servers and hooks are read from ${where(pick)}`));
|
|
7411
7424
|
// What was found there — and what was not, said rather than shown as an empty list.
|
|
7412
7425
|
try {
|
|
7413
7426
|
const sk = discoverSkills({ cwd: process.cwd(), reserved: COMMAND_NAMES });
|
|
@@ -7419,20 +7432,20 @@ async function runSwitchSkillCommand(line) {
|
|
|
7419
7432
|
say(dim(` ${count(sk.skills.length, "skill")}${sk.skills.length ? ` (${sk.skills.map((x) => "/" + x.name).join(", ")})` : ""} · ${count(running, "MCP server")} · ${count(nHooks(hk.user) + (hk.projectApproved ? nHooks(hk.project) : 0), "hook")}`));
|
|
7420
7433
|
// Nothing there at all is a normal state, not a fault: say so, and that gu needs none of it.
|
|
7421
7434
|
if (!sk.skills.length && !mcp.servers.length && !nHooks(hk.user) && !nHooks(hk.project) && !hk.notes.length) {
|
|
7422
|
-
say(dim("
|
|
7435
|
+
say(dim(" Nothing is set up there yet — gu works the same without any of them."));
|
|
7423
7436
|
}
|
|
7424
7437
|
for (const w of [...sk.warnings, ...mcp.warnings, ...hk.warnings]) say(yellow(" " + w));
|
|
7425
7438
|
const unsupported = mcp.servers.filter((x) => x.error);
|
|
7426
7439
|
if (unsupported.length) say(yellow(` ${count(unsupported.length, "MCP server")} cannot run here: ${unsupported.map((x) => `${x.name} (${x.error})`).join("; ")}`));
|
|
7427
7440
|
const waiting = mcp.servers.filter((x) => x.scope === "project" && !x.approved && !x.error);
|
|
7428
7441
|
if (waiting.length) say(yellow(` ${count(waiting.length, "project MCP server")} waiting for your approval: ${waiting.map((x) => x.name).join(", ")} — /mcp approve <name>`));
|
|
7429
|
-
if (hk.projectPending) say(yellow(`
|
|
7442
|
+
if (hk.projectPending) say(yellow(` this folder's ${hk.projectFile.replace(/^\.\//, "")} has hooks that will not run until you approve them — /hooks approve`));
|
|
7430
7443
|
if (hk.notes.length) say(yellow(` ${count(hk.notes.length, "hook")} in Claude's settings ${hk.notes.length === 1 ? "is" : "are"} NOT run by gu — /hooks says which and why`));
|
|
7431
|
-
if (pick === "claude" && (nHooks(hk.user) || nHooks(hk.project))) say(dim("
|
|
7444
|
+
if (pick === "claude" && (nHooks(hk.user) || nHooks(hk.project))) say(dim(" Claude's hooks get Claude's event and tool names from gu, so they record here as they do there."));
|
|
7432
7445
|
} catch (err) {
|
|
7433
|
-
say(yellow(`
|
|
7446
|
+
say(yellow(` (could not read it back: ${err.message})`));
|
|
7434
7447
|
}
|
|
7435
|
-
say(dim(`
|
|
7448
|
+
say(dim(` It holds from your next question. ${pick === "claude" ? "/switch-skill gu" : "/switch-skill claude"} switches back.`));
|
|
7436
7449
|
console.log("");
|
|
7437
7450
|
if (barPinned()) paintBar();
|
|
7438
7451
|
}
|
|
@@ -7581,7 +7594,7 @@ async function runUsePresetsCommand(line) {
|
|
|
7581
7594
|
console.log(GUTTER + dim("/save-preset pastes one in — copy it from another computer's /open-presets.") + "\n");
|
|
7582
7595
|
return;
|
|
7583
7596
|
}
|
|
7584
|
-
console.log(dim(`
|
|
7597
|
+
console.log(dim(` Presets (${ed.PICKER_HINT}):`));
|
|
7585
7598
|
const pick = await pickFromList(found, 0, { keys: ed.PICKER_KEYS });
|
|
7586
7599
|
if (!pick || pick.index < 0) {
|
|
7587
7600
|
console.log(GUTTER + dim("nothing changed.") + "\n");
|
|
@@ -7660,7 +7673,7 @@ async function runSaveSettingsCommand(line) {
|
|
|
7660
7673
|
// have to type is a name — and Enter keeps gu-settings.json, as before.
|
|
7661
7674
|
let typed = settingsArg(line, "/save-settings");
|
|
7662
7675
|
if (!typed) {
|
|
7663
|
-
const answer = await ask(`
|
|
7676
|
+
const answer = await ask(` file name in ${home} [${st.DEFAULT_SETTINGS_NAME}]: `);
|
|
7664
7677
|
if (answer === null) {
|
|
7665
7678
|
console.log(GUTTER + dim(" (cancelled — nothing was written)\n"));
|
|
7666
7679
|
return;
|
|
@@ -7720,7 +7733,7 @@ async function runLoadSettingsCommand(line) {
|
|
|
7720
7733
|
if (!typed) {
|
|
7721
7734
|
const found = st.settingsFilesIn(listJsonFiles(home));
|
|
7722
7735
|
if (found.length > 1) {
|
|
7723
|
-
console.log(dim(`
|
|
7736
|
+
console.log(dim(` Settings files in ${home} (${PICK_HINT}):`));
|
|
7724
7737
|
const i = await pickFromList(found, 0);
|
|
7725
7738
|
if (i < 0) {
|
|
7726
7739
|
console.log(GUTTER + dim(" (cancelled — nothing changed)\n"));
|
|
@@ -8070,11 +8083,11 @@ async function runClearLogs() {
|
|
|
8070
8083
|
const question = (q) => {
|
|
8071
8084
|
const hint = `(${PICK_HINT}):`;
|
|
8072
8085
|
console.log("");
|
|
8073
|
-
if (2 + q.length + 2 + hint.length < width) console.log(dim(`
|
|
8074
|
-
else console.log(dim(`
|
|
8086
|
+
if (2 + q.length + 2 + hint.length < width) console.log(dim(` ${q} ${hint}`));
|
|
8087
|
+
else console.log(dim(` ${q}\n ${hint}`));
|
|
8075
8088
|
};
|
|
8076
8089
|
const cancelled = () => {
|
|
8077
|
-
console.log(dim(`
|
|
8090
|
+
console.log(dim(` (stopped — nothing ${freed.length ? "further " : ""}removed)\n`));
|
|
8078
8091
|
paintBar();
|
|
8079
8092
|
};
|
|
8080
8093
|
|
|
@@ -8175,7 +8188,7 @@ async function saveEmbedSettings(fields) {
|
|
|
8175
8188
|
const saved = await withPendingStatus("saving your embedder", () =>
|
|
8176
8189
|
md.saveWorkerSettings(apiBase, workerKey, fields)
|
|
8177
8190
|
);
|
|
8178
|
-
if (!saved) console.log(red("
|
|
8191
|
+
if (!saved) console.log(red(" couldn't save that to your account — nothing changed.\n"));
|
|
8179
8192
|
return saved;
|
|
8180
8193
|
}
|
|
8181
8194
|
|
|
@@ -8193,7 +8206,7 @@ async function confirmEmbedSwitch(ec, current, chosen) {
|
|
|
8193
8206
|
}
|
|
8194
8207
|
const plan = ec.embedChangePlan({ current, chosen, indexes });
|
|
8195
8208
|
if (!plan.changed) {
|
|
8196
|
-
console.log(dim(`
|
|
8209
|
+
console.log(dim(` already using ${current} — nothing changed.\n`));
|
|
8197
8210
|
return { changed: false, plan: null };
|
|
8198
8211
|
}
|
|
8199
8212
|
// "Are you sure?" with nothing behind it is a prompt people learn to dismiss; the four projects
|
|
@@ -8201,7 +8214,7 @@ async function confirmEmbedSwitch(ec, current, chosen) {
|
|
|
8201
8214
|
for (const l of ec.describeEmbedChange(plan)) console.log(GUTTER + dim(l));
|
|
8202
8215
|
const go = await pickFromList([`switch to ${chosen}`, `keep ${current}`], plan.stale.length ? 1 : 0);
|
|
8203
8216
|
if (go !== 0) {
|
|
8204
|
-
console.log(dim("
|
|
8217
|
+
console.log(dim(" (kept)\n"));
|
|
8205
8218
|
return { changed: false, plan: null };
|
|
8206
8219
|
}
|
|
8207
8220
|
return { changed: true, plan };
|
|
@@ -8211,13 +8224,13 @@ async function confirmEmbedSwitch(ec, current, chosen) {
|
|
|
8211
8224
|
// shared recognition step. Returns { changed, chosen, plan }.
|
|
8212
8225
|
async function pickEmbedModel(ec, current) {
|
|
8213
8226
|
const rows = ec.embedChoiceRows(current);
|
|
8214
|
-
console.log(dim(`
|
|
8227
|
+
console.log(dim(` Choose the embedder (${PICK_HINT}):`));
|
|
8215
8228
|
const labels = rows.map(
|
|
8216
8229
|
(r) => `${r.model}${r.dims ? dim(` · ${r.dims}d`) : ""}${dim(` · ${r.note}`)}` + (r.current ? accent(" (current)") : "")
|
|
8217
8230
|
);
|
|
8218
8231
|
const idx = await pickFromList(labels, Math.max(0, rows.findIndex((r) => r.current)));
|
|
8219
8232
|
if (idx < 0) {
|
|
8220
|
-
console.log(dim("
|
|
8233
|
+
console.log(dim(" (cancelled)\n"));
|
|
8221
8234
|
return { changed: false, chosen: "", plan: null };
|
|
8222
8235
|
}
|
|
8223
8236
|
const chosen = rows[idx].model;
|
|
@@ -8231,21 +8244,21 @@ async function pickEmbedModel(ec, current) {
|
|
|
8231
8244
|
// a model that was never pulled fails at the first search, not here. Say it once rather than let
|
|
8232
8245
|
// it surface as "RAG is broken".
|
|
8233
8246
|
function announceEmbedSaved({ chosen, plan, url }) {
|
|
8234
|
-
console.log(accent(`
|
|
8247
|
+
console.log(accent(` ✓ embedding model → ${chosen}` + (url ? ` on ${hostOf(url)}` : "")));
|
|
8235
8248
|
if (plan?.stale.length) {
|
|
8236
8249
|
console.log(
|
|
8237
8250
|
dim(` ${plan.stale.length} knowledge base${plan.stale.length === 1 ? "" : "s"} ` +
|
|
8238
8251
|
"will rebuild on the next search — the first one will be slower.")
|
|
8239
8252
|
);
|
|
8240
8253
|
}
|
|
8241
|
-
console.log(dim(`
|
|
8254
|
+
console.log(dim(` Make sure it is pulled there: ollama pull ${chosen}\n`));
|
|
8242
8255
|
}
|
|
8243
8256
|
|
|
8244
8257
|
async function chooseEmbedModel() {
|
|
8245
8258
|
const ec = await import("./embed-model-command.mjs");
|
|
8246
8259
|
const settings = await withPendingStatus("reading your embedder", fetchWorkerSettings);
|
|
8247
8260
|
if (!settings) {
|
|
8248
|
-
console.log(red("
|
|
8261
|
+
console.log(red(" couldn't reach the API to read your current embedder.\n"));
|
|
8249
8262
|
return;
|
|
8250
8263
|
}
|
|
8251
8264
|
const current = ec.currentEmbedModel(settings);
|
|
@@ -8258,11 +8271,11 @@ async function chooseEmbedModel() {
|
|
|
8258
8271
|
const keyRequired = isOllamaCloudUrl(currentUrl) || /\/v1\/?$/i.test(currentUrl) ||
|
|
8259
8272
|
Boolean(String(settings.ragEmbedKeyId ?? "").trim());
|
|
8260
8273
|
const menu = ec.embedMenuOptions({ current, host, keyRequired });
|
|
8261
|
-
console.log(dim(`
|
|
8274
|
+
console.log(dim(` Embedding model (currently ${current} on ${host || currentUrl})`));
|
|
8262
8275
|
const mi = await pickFromList(menu.map((o) => o.label), 0);
|
|
8263
8276
|
const action = mi >= 0 ? menu[mi].action : "cancel";
|
|
8264
8277
|
if (action === "cancel") {
|
|
8265
|
-
console.log(dim("
|
|
8278
|
+
console.log(dim(" (cancelled)\n"));
|
|
8266
8279
|
return;
|
|
8267
8280
|
}
|
|
8268
8281
|
|
|
@@ -8303,12 +8316,12 @@ async function saveAgentModel(agentUrl, model, host) {
|
|
|
8303
8316
|
md.saveWorkerSettings(apiBase, workerKey, { agentModelUrl: agentUrl, agentModelName: model, agentModel: model })
|
|
8304
8317
|
);
|
|
8305
8318
|
if (!ok) {
|
|
8306
|
-
console.log(red(`
|
|
8319
|
+
console.log(red(` couldn't save the agent model — nothing changed.`));
|
|
8307
8320
|
console.log(GUTTER + dim("if this machine's local API is old, restart it with `gu api restart`.\n"));
|
|
8308
8321
|
return false;
|
|
8309
8322
|
}
|
|
8310
|
-
console.log(accent(`
|
|
8311
|
-
console.log(dim("
|
|
8323
|
+
console.log(accent(` ✓ agent model → ${model}`) + dim(` · ${host}`));
|
|
8324
|
+
console.log(dim(" takes effect on your next question.\n"));
|
|
8312
8325
|
return true;
|
|
8313
8326
|
}
|
|
8314
8327
|
|
|
@@ -8321,7 +8334,7 @@ async function askKeyName(prompt = " key name: ") {
|
|
|
8321
8334
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
8322
8335
|
const name = (await ask(prompt)).trim();
|
|
8323
8336
|
if (!am.looksLikeSecret(name)) return name;
|
|
8324
|
-
console.log(yellow("
|
|
8337
|
+
console.log(yellow(" that looks like the API key itself — the NAME is shown on screen, so it must not be the key."));
|
|
8325
8338
|
console.log(GUTTER + dim('type a short label like "work", or press Enter for none — the key comes next.'));
|
|
8326
8339
|
}
|
|
8327
8340
|
return "";
|
|
@@ -8342,7 +8355,7 @@ async function askServerKey({ host = "", nameHint = '"work", "personal"', option
|
|
|
8342
8355
|
console.log("\n" + GUTTER + dim(`A name just tells keys apart — ${nameHint}.`));
|
|
8343
8356
|
const name = (await askKeyName()).trim();
|
|
8344
8357
|
console.log(GUTTER + dim(SECRET_KEY_HINT));
|
|
8345
|
-
const secret = (await askSecret(`
|
|
8358
|
+
const secret = (await askSecret(` API key${host ? ` for ${host}` : ""}${name ? ` (${name})` : ""}: `)).trim();
|
|
8346
8359
|
if (!secret) {
|
|
8347
8360
|
console.log(GUTTER + dim(optional ? "(no key — the server will be called anonymously)" : "(cancelled — nothing was saved)\n"));
|
|
8348
8361
|
return { name, secret: "" };
|
|
@@ -8377,15 +8390,15 @@ async function chooseAgentProvider() {
|
|
|
8377
8390
|
const am = await import("./agent-model-command.mjs");
|
|
8378
8391
|
const settings = await withPendingStatus("reading your providers", fetchWorkerSettings);
|
|
8379
8392
|
if (!settings) {
|
|
8380
|
-
console.log(red("
|
|
8393
|
+
console.log(red(" couldn't reach the API to read your providers.\n"));
|
|
8381
8394
|
return;
|
|
8382
8395
|
}
|
|
8383
8396
|
const choices = am.agentProviderChoices(settings);
|
|
8384
8397
|
if (choices.length === 1) return addServer({ role: "agent", dialect: "openai" });
|
|
8385
|
-
console.log(dim(`
|
|
8398
|
+
console.log(dim(` Which OpenAI-compatible provider should the agent use? (${PICK_HINT})`));
|
|
8386
8399
|
const idx = await pickFromList(choices.map((c) => c.label), 0);
|
|
8387
8400
|
if (idx < 0) {
|
|
8388
|
-
console.log(dim("
|
|
8401
|
+
console.log(dim(" (cancelled)\n"));
|
|
8389
8402
|
return;
|
|
8390
8403
|
}
|
|
8391
8404
|
const pick = choices[idx];
|
|
@@ -8407,7 +8420,7 @@ async function chooseAgentModel() {
|
|
|
8407
8420
|
const am = await import("./agent-model-command.mjs");
|
|
8408
8421
|
const settings = await withPendingStatus("reading your agent model", fetchWorkerSettings);
|
|
8409
8422
|
if (!settings) {
|
|
8410
|
-
console.log(red("
|
|
8423
|
+
console.log(red(" couldn't reach the API to read your current agent model.\n"));
|
|
8411
8424
|
return;
|
|
8412
8425
|
}
|
|
8413
8426
|
const current = am.currentAgentModel(settings);
|
|
@@ -8493,12 +8506,12 @@ async function chooseAgentModel() {
|
|
|
8493
8506
|
// WHAT TO CHANGE, FIRST (#44). The ways to use an Ollama server or another provider used to sit
|
|
8494
8507
|
// after every model on this one — 130 rows down on api.openai.com — which read as "OpenAI models
|
|
8495
8508
|
// are all you can choose". A short menu of what to change comes before any list.
|
|
8496
|
-
console.log(dim(`
|
|
8509
|
+
console.log(dim(` Agent model — the one that TALKS: ${current || "unset"} on ${listHost}`));
|
|
8497
8510
|
const menu = am.agentMenuOptions({ host: listHost, modelCount: models.length, needsKey });
|
|
8498
8511
|
const first = await pickFromList(menu.map((o) => o.label), 0);
|
|
8499
8512
|
const firstAction = first >= 0 ? menu[first].action : "cancel";
|
|
8500
8513
|
if (firstAction === "cancel") {
|
|
8501
|
-
console.log(dim("
|
|
8514
|
+
console.log(dim(" (cancelled)\n"));
|
|
8502
8515
|
return;
|
|
8503
8516
|
}
|
|
8504
8517
|
if (firstAction === "ollama") return addServer({ role: "agent", dialect: "ollama" });
|
|
@@ -8512,7 +8525,7 @@ async function chooseAgentModel() {
|
|
|
8512
8525
|
}
|
|
8513
8526
|
|
|
8514
8527
|
const rows = am.agentModelRows(models, current);
|
|
8515
|
-
console.log(dim(`
|
|
8528
|
+
console.log(dim(` Agent model — the one that TALKS (currently ${current || "unset"})`));
|
|
8516
8529
|
// Strip whichever list endpoint was asked, so the header names the SERVER rather than the path.
|
|
8517
8530
|
console.log(
|
|
8518
8531
|
dim(` ${models.length} on ${url.replace(/\/api\/tags$/, "").replace(/\/models$/, "")} (${PICK_HINT}):`)
|
|
@@ -8541,12 +8554,12 @@ async function chooseAgentModel() {
|
|
|
8541
8554
|
// where they can be seen, rather than after every model.
|
|
8542
8555
|
const idx = await pickFromList(labels, Math.max(0, rows.findIndex((r) => r.current)));
|
|
8543
8556
|
if (idx < 0) {
|
|
8544
|
-
console.log(dim("
|
|
8557
|
+
console.log(dim(" (cancelled)\n"));
|
|
8545
8558
|
return;
|
|
8546
8559
|
}
|
|
8547
8560
|
const chosen = rows[idx].model;
|
|
8548
8561
|
if (chosen === current) {
|
|
8549
|
-
console.log(dim(`
|
|
8562
|
+
console.log(dim(` already using ${current} — nothing changed.\n`));
|
|
8550
8563
|
return;
|
|
8551
8564
|
}
|
|
8552
8565
|
if (rows[idx].embedder) {
|
|
@@ -8557,7 +8570,7 @@ async function chooseAgentModel() {
|
|
|
8557
8570
|
console.log(GUTTER + yellow("produce nonsense rather than an error."));
|
|
8558
8571
|
const go = await pickFromList([`use it anyway`, `pick something else`], 1);
|
|
8559
8572
|
if (go !== 0) {
|
|
8560
|
-
console.log(dim("
|
|
8573
|
+
console.log(dim(" (kept)\n"));
|
|
8561
8574
|
return;
|
|
8562
8575
|
}
|
|
8563
8576
|
}
|
|
@@ -8570,15 +8583,15 @@ async function chooseAgentModel() {
|
|
|
8570
8583
|
agentModel: chosen,
|
|
8571
8584
|
});
|
|
8572
8585
|
if (!saved) {
|
|
8573
|
-
console.log(red(`
|
|
8586
|
+
console.log(red(` couldn't save that to your account — still using ${current || "unset"}.`));
|
|
8574
8587
|
// The likeliest cause by far, and it is invisible otherwise: this field only became writable
|
|
8575
8588
|
// through the worker route in task #169, so an older local API silently ignores it.
|
|
8576
8589
|
console.log(GUTTER + dim("if this machine's local API predates /agent-model, restart it with"));
|
|
8577
8590
|
console.log(GUTTER + dim("`gu api restart` to pick up the current version.\n"));
|
|
8578
8591
|
return;
|
|
8579
8592
|
}
|
|
8580
|
-
console.log(accent(`
|
|
8581
|
-
console.log(dim("
|
|
8593
|
+
console.log(accent(` ✓ agent model → ${chosen}`));
|
|
8594
|
+
console.log(dim(" takes effect on your next question.\n"));
|
|
8582
8595
|
}
|
|
8583
8596
|
|
|
8584
8597
|
// Probe whether SSH KEY auth already works for a server (so we can tell the user to run
|
|
@@ -8603,7 +8616,7 @@ async function checkServerKeyAuth(s) {
|
|
|
8603
8616
|
if (ok) console.log(dim(" ✓ key auth works — deploys will connect without a password."));
|
|
8604
8617
|
else
|
|
8605
8618
|
console.log(
|
|
8606
|
-
yellow("
|
|
8619
|
+
yellow(" key auth not set up yet.") +
|
|
8607
8620
|
dim(` Run once in your terminal: `) +
|
|
8608
8621
|
`ssh-copy-id ${s.user}@${s.host}`
|
|
8609
8622
|
);
|
|
@@ -8803,20 +8816,20 @@ async function runS3Command(line) {
|
|
|
8803
8816
|
// and, for the secret, does not have to know it.
|
|
8804
8817
|
const keep = (typed, was) => (String(typed ?? "").trim() ? String(typed).trim() : was);
|
|
8805
8818
|
const location = keep(
|
|
8806
|
-
await ask(`
|
|
8819
|
+
await ask(` Bucket (ARN, s3://… or name)${current.location ? ` [${current.location}]` : ""}: `),
|
|
8807
8820
|
current.location
|
|
8808
8821
|
);
|
|
8809
8822
|
const region = keep(
|
|
8810
|
-
await ask(`
|
|
8823
|
+
await ask(` Region [${current.region || "ap-southeast-1"}]: `),
|
|
8811
8824
|
current.region || "ap-southeast-1"
|
|
8812
8825
|
);
|
|
8813
8826
|
const accessKeyId = keep(
|
|
8814
|
-
await ask(`
|
|
8827
|
+
await ask(` AWS access key id${current.accessKeyId ? ` [${s3.maskKeyId(current.accessKeyId)}]` : ""}: `),
|
|
8815
8828
|
current.accessKeyId
|
|
8816
8829
|
);
|
|
8817
8830
|
// askSecret, never ask: the echo is muted and the value never reaches the scrollback (#127).
|
|
8818
8831
|
const typedSecret = String(
|
|
8819
|
-
await askSecret(`
|
|
8832
|
+
await askSecret(` AWS secret${current.secretConfigured ? " [Enter keeps the stored one]" : ""}:`)
|
|
8820
8833
|
).trim();
|
|
8821
8834
|
|
|
8822
8835
|
if (!location || !accessKeyId || !(typedSecret || current.secretConfigured)) {
|
|
@@ -9013,16 +9026,16 @@ async function chooseServer() {
|
|
|
9013
9026
|
);
|
|
9014
9027
|
const data = await res.json().catch(() => ({}));
|
|
9015
9028
|
if (!res.ok) {
|
|
9016
|
-
console.log(red(`
|
|
9029
|
+
console.log(red(` couldn't load servers: ${data?.error || `HTTP ${res.status}`}\n`));
|
|
9017
9030
|
return;
|
|
9018
9031
|
}
|
|
9019
9032
|
servers = Array.isArray(data?.servers) ? data.servers : [];
|
|
9020
9033
|
} catch (err) {
|
|
9021
|
-
console.log(red(`
|
|
9034
|
+
console.log(red(` ${describeApiFailure(err, "couldn't load servers")}\n`));
|
|
9022
9035
|
return;
|
|
9023
9036
|
}
|
|
9024
9037
|
if (servers.length === 0) {
|
|
9025
|
-
console.log(dim("
|
|
9038
|
+
console.log(dim(" No deployment servers yet — add them in the web app → Servers.\n"));
|
|
9026
9039
|
return;
|
|
9027
9040
|
}
|
|
9028
9041
|
// Arrow-key picker: ↑/↓ to move, Enter to choose (no number typing). The list ends with
|
|
@@ -9030,21 +9043,21 @@ async function chooseServer() {
|
|
|
9030
9043
|
const labels = servers.map((s) => `${s.name} (${s.user}@${s.host})`);
|
|
9031
9044
|
labels.push("none (clear selection)");
|
|
9032
9045
|
const activeIdx = servers.findIndex((s) => s.host === selectedServer?.host);
|
|
9033
|
-
console.log(dim(`
|
|
9046
|
+
console.log(dim(` Choose a deployment server (${PICK_HINT}):`));
|
|
9034
9047
|
const idx = await pickFromList(labels, activeIdx >= 0 ? activeIdx : 0);
|
|
9035
9048
|
if (idx < 0) {
|
|
9036
|
-
console.log(dim("
|
|
9049
|
+
console.log(dim(" (cancelled)\n"));
|
|
9037
9050
|
return;
|
|
9038
9051
|
}
|
|
9039
9052
|
if (idx === servers.length) {
|
|
9040
9053
|
// the trailing "none" row
|
|
9041
9054
|
selectedServer = null;
|
|
9042
|
-
console.log(dim("
|
|
9055
|
+
console.log(dim(" ✓ deployment server cleared.\n"));
|
|
9043
9056
|
return;
|
|
9044
9057
|
}
|
|
9045
9058
|
const s = servers[idx];
|
|
9046
9059
|
selectedServer = { name: s.name, host: s.host, user: s.user };
|
|
9047
|
-
console.log(accent(`
|
|
9060
|
+
console.log(accent(` ✓ deploy target → ${s.name} (${s.user}@${s.host})`));
|
|
9048
9061
|
await checkServerKeyAuth(selectedServer);
|
|
9049
9062
|
console.log("");
|
|
9050
9063
|
}
|
|
@@ -9136,10 +9149,10 @@ async function showOtherAgents() {
|
|
|
9136
9149
|
return;
|
|
9137
9150
|
}
|
|
9138
9151
|
console.log("");
|
|
9139
|
-
console.log(dim(`
|
|
9152
|
+
console.log(dim(` Your other running agents — pick one to act on (${PICK_HINT}):`));
|
|
9140
9153
|
const which = await pickFromList(items.map((i) => i.label), 0);
|
|
9141
9154
|
if (which < 0) {
|
|
9142
|
-
console.log(dim("
|
|
9155
|
+
console.log(dim(" (cancelled)\n"));
|
|
9143
9156
|
paintBar();
|
|
9144
9157
|
return;
|
|
9145
9158
|
}
|
|
@@ -9162,10 +9175,10 @@ async function showOtherAgents() {
|
|
|
9162
9175
|
{ id: "stop", label: "Stop it", note: "ends the run — its unsaved work is lost" },
|
|
9163
9176
|
];
|
|
9164
9177
|
console.log("");
|
|
9165
|
-
console.log(dim(`
|
|
9178
|
+
console.log(dim(` ${shortName} (${PICK_HINT}):`));
|
|
9166
9179
|
const act = await pickFromList(actions.map((a) => a.label + dim(` · ${a.note}`)), 0);
|
|
9167
9180
|
if (act < 0) {
|
|
9168
|
-
console.log(dim("
|
|
9181
|
+
console.log(dim(" (cancelled)\n"));
|
|
9169
9182
|
paintBar();
|
|
9170
9183
|
return;
|
|
9171
9184
|
}
|
|
@@ -9463,9 +9476,9 @@ async function promptNewCodingKey(backendId = "", provider = "") {
|
|
|
9463
9476
|
const id = await addCodingKey(name, secret, backendId);
|
|
9464
9477
|
console.log(
|
|
9465
9478
|
id
|
|
9466
|
-
? accent(`
|
|
9479
|
+
? accent(` ✓ added ${name || "Unknown"}`) +
|
|
9467
9480
|
(backendId && provider ? dim(` · for ${provider}`) : "") + "\n"
|
|
9468
|
-
: red("
|
|
9481
|
+
: red(" couldn't save the key — try again, or set it in web Settings → Agent\n")
|
|
9469
9482
|
);
|
|
9470
9483
|
return id;
|
|
9471
9484
|
}
|
|
@@ -9558,9 +9571,9 @@ async function replaceCodingKey() {
|
|
|
9558
9571
|
console.log(
|
|
9559
9572
|
ok
|
|
9560
9573
|
? clearing
|
|
9561
|
-
? accent(`
|
|
9562
|
-
: accent(`
|
|
9563
|
-
: red("
|
|
9574
|
+
? accent(` ✓ ${ks.model} API key removed`) + dim(" — coding falls back to your local model\n")
|
|
9575
|
+
: accent(` ✓ ${ks.model} API key saved\n`)
|
|
9576
|
+
: red(" couldn't save the key — try again, or set it in web Settings → Agent\n")
|
|
9564
9577
|
);
|
|
9565
9578
|
paintBar();
|
|
9566
9579
|
}
|
|
@@ -9590,7 +9603,7 @@ async function manageServerKeys({ role = "coding", backendId, host, model = "" }
|
|
|
9590
9603
|
backendId ? fetchServerKeys(backendId) : fetchCodingKeys()
|
|
9591
9604
|
);
|
|
9592
9605
|
if (!keys) {
|
|
9593
|
-
console.log(red(`
|
|
9606
|
+
console.log(red(` couldn't read the keys for ${host} — is this machine's API up to date? (\`gu api restart\`)\n`));
|
|
9594
9607
|
paintBar();
|
|
9595
9608
|
return "";
|
|
9596
9609
|
}
|
|
@@ -9635,7 +9648,7 @@ async function manageServerKeys({ role = "coding", backendId, host, model = "" }
|
|
|
9635
9648
|
const fresh = await fetchServerKeys(backendId);
|
|
9636
9649
|
const added = fresh?.find((k) => k.id === id);
|
|
9637
9650
|
if (added?.default) {
|
|
9638
|
-
console.log(accent(`
|
|
9651
|
+
console.log(accent(` ✓ ${added.name} is ${host}'s default key\n`));
|
|
9639
9652
|
paintBar();
|
|
9640
9653
|
return id;
|
|
9641
9654
|
}
|
|
@@ -9647,7 +9660,7 @@ async function manageServerKeys({ role = "coding", backendId, host, model = "" }
|
|
|
9647
9660
|
if (!key.backendId && backendId) {
|
|
9648
9661
|
const ok = await withPendingStatus(`tying ${key.name} to ${host}`, () => bindKeyToServer(key.id, backendId));
|
|
9649
9662
|
if (!ok) {
|
|
9650
|
-
console.log(red(`
|
|
9663
|
+
console.log(red(` couldn't tie ${key.name} to ${host} — nothing changed.\n`));
|
|
9651
9664
|
paintBar();
|
|
9652
9665
|
return "";
|
|
9653
9666
|
}
|
|
@@ -9666,10 +9679,10 @@ async function applyKeyPick({ role, backendId, host, key }) {
|
|
|
9666
9679
|
let scope = "folder";
|
|
9667
9680
|
if (!backendId) {
|
|
9668
9681
|
await saveFolderKeyPick(role, key.id);
|
|
9669
|
-
console.log(accent(`
|
|
9682
|
+
console.log(accent(` ✓ ${folderLabel()}'s ${roleLabel} → ${key.name}`) + dim(` ····${key.tail}\n`));
|
|
9670
9683
|
} else if (key.default) {
|
|
9671
9684
|
await saveFolderKeyPick(role, "");
|
|
9672
|
-
console.log(accent(`
|
|
9685
|
+
console.log(accent(` ✓ ${folderLabel()}'s ${roleLabel} → ${key.name}`) + dim(` ····${key.tail} · ${host}'s default\n`));
|
|
9673
9686
|
} else {
|
|
9674
9687
|
const choices = sk.scopeChoices({ host, role });
|
|
9675
9688
|
console.log("\n" + GUTTER + dim(`Use ${key.name} for…`));
|
|
@@ -9683,16 +9696,16 @@ async function applyKeyPick({ role, backendId, host, key }) {
|
|
|
9683
9696
|
if (scope === "default") {
|
|
9684
9697
|
const ok = await withPendingStatus(`making ${key.name} the default for ${host}`, () => setServerDefaultKey(backendId, key.id));
|
|
9685
9698
|
if (!ok) {
|
|
9686
|
-
console.log(red("
|
|
9699
|
+
console.log(red(" couldn't change the default — nothing changed.\n"));
|
|
9687
9700
|
paintBar();
|
|
9688
9701
|
return "";
|
|
9689
9702
|
}
|
|
9690
9703
|
// This folder follows the default now, rather than pinning a copy of it.
|
|
9691
9704
|
await saveFolderKeyPick(role, "");
|
|
9692
|
-
console.log(accent(`
|
|
9705
|
+
console.log(accent(` ✓ ${key.name} is now ${host}'s default`) + dim(` ····${key.tail} · every folder without its own pick\n`));
|
|
9693
9706
|
} else {
|
|
9694
9707
|
await saveFolderKeyPick(role, key.id);
|
|
9695
|
-
console.log(accent(`
|
|
9708
|
+
console.log(accent(` ✓ ${folderLabel()}'s ${roleLabel} → ${key.name}`) + dim(` ····${key.tail} · this folder only\n`));
|
|
9696
9709
|
}
|
|
9697
9710
|
}
|
|
9698
9711
|
if (role === "coding") {
|
|
@@ -9746,7 +9759,7 @@ async function deleteServerKeyFlow({ role, key, host, folderKeyId, lastOfServer
|
|
|
9746
9759
|
}
|
|
9747
9760
|
const out = await deleteCodingKey(key.id);
|
|
9748
9761
|
if (!out.ok) {
|
|
9749
|
-
console.log(red("
|
|
9762
|
+
console.log(red(" couldn't delete the key — try again, or remove it in web Settings → Agent\n"));
|
|
9750
9763
|
paintBar();
|
|
9751
9764
|
return;
|
|
9752
9765
|
}
|
|
@@ -9755,7 +9768,7 @@ async function deleteServerKeyFlow({ role, key, host, folderKeyId, lastOfServer
|
|
|
9755
9768
|
const mine = r === "agent" ? sessionAgentKeyId : r === "embed" ? sessionEmbedKeyId : sessionCodingKeyId;
|
|
9756
9769
|
if (mine === key.id) await saveFolderKeyPick(r, "");
|
|
9757
9770
|
}
|
|
9758
|
-
console.log(accent(`
|
|
9771
|
+
console.log(accent(` ✓ deleted ${out.name || key.name}`) + dim(` ····${key.tail} · ${out.remaining} key(s) left\n`));
|
|
9759
9772
|
paintBar();
|
|
9760
9773
|
}
|
|
9761
9774
|
|
|
@@ -10023,7 +10036,7 @@ async function approvalPrompt(command) {
|
|
|
10023
10036
|
ok = (await runS3Command("/s3")) === true;
|
|
10024
10037
|
} catch (e) {
|
|
10025
10038
|
if (!wasPromptCancelled(e)) throw e;
|
|
10026
|
-
console.log(dim("
|
|
10039
|
+
console.log(dim(" (cancelled — the file is saved on this computer)\n"));
|
|
10027
10040
|
} finally {
|
|
10028
10041
|
following = wasFollowing;
|
|
10029
10042
|
}
|
|
@@ -12531,7 +12544,7 @@ async function main() {
|
|
|
12531
12544
|
// meaningfully refuse.
|
|
12532
12545
|
const steps = await import("./first-run-steps.mjs");
|
|
12533
12546
|
const step = steps.stepById("daemon");
|
|
12534
|
-
console.log(dim(`
|
|
12547
|
+
console.log(dim(` ${step.doing}…`));
|
|
12535
12548
|
let started = false;
|
|
12536
12549
|
let why = "";
|
|
12537
12550
|
try {
|
|
@@ -12539,7 +12552,7 @@ async function main() {
|
|
|
12539
12552
|
started = !!r.started;
|
|
12540
12553
|
localDaemonRunning = started || localDaemonRunning;
|
|
12541
12554
|
if (started) {
|
|
12542
|
-
console.log(dim(`
|
|
12555
|
+
console.log(dim(` started background worker (pid ${r.pid}) · logs ${ctl.LOG_FILE}`));
|
|
12543
12556
|
} else {
|
|
12544
12557
|
why = r.reason || "the worker did not report a pid";
|
|
12545
12558
|
}
|
|
@@ -12553,7 +12566,7 @@ async function main() {
|
|
|
12553
12566
|
await recordStepOutcome("daemon", started, why);
|
|
12554
12567
|
if (!started) {
|
|
12555
12568
|
console.log(
|
|
12556
|
-
yellow("
|
|
12569
|
+
yellow(" the background worker did not start") +
|
|
12557
12570
|
dim(` — ${why}\n ${step.why}.\n Try \`gu start\` to see the error in full.\n`)
|
|
12558
12571
|
);
|
|
12559
12572
|
}
|
|
@@ -12906,7 +12919,7 @@ async function main() {
|
|
|
12906
12919
|
} else {
|
|
12907
12920
|
await saveSessionSkip(resolve(process.cwd()), "skipDepsPrompt", true);
|
|
12908
12921
|
console.log(
|
|
12909
|
-
dim("
|
|
12922
|
+
dim(" Skipped — install later with `gu doctor`") +
|
|
12910
12923
|
dim(clientModeBanner ? " --client" : "") +
|
|
12911
12924
|
dim("; I won't ask again in this folder.\n")
|
|
12912
12925
|
);
|
|
@@ -12940,7 +12953,7 @@ async function main() {
|
|
|
12940
12953
|
);
|
|
12941
12954
|
if (skipSetup) {
|
|
12942
12955
|
console.log(
|
|
12943
|
-
dim("
|
|
12956
|
+
dim(" Set-up skipped (remembered for this folder) — `gu doctor` runs it ") +
|
|
12944
12957
|
dim("any time.\n")
|
|
12945
12958
|
);
|
|
12946
12959
|
}
|
|
@@ -12968,7 +12981,7 @@ async function main() {
|
|
|
12968
12981
|
await ctl2.setClientMode(true);
|
|
12969
12982
|
const st = await ctl2.daemonStatus();
|
|
12970
12983
|
console.log(
|
|
12971
|
-
dim("
|
|
12984
|
+
dim(" Skipped — this machine won't SERVE models; the agent still runs here and\n") +
|
|
12972
12985
|
dim(" calls your model machine over the network. Point the Agent model URL at\n") +
|
|
12973
12986
|
dim(" that machine (not 127.0.0.1) — `gu status` shows what it resolves to.\n") +
|
|
12974
12987
|
dim(" `gu doctor` sets models up here · `/host-mode` undoes this.\n")
|
|
@@ -13146,8 +13159,8 @@ async function main() {
|
|
|
13146
13159
|
const started = await tryStartLocalApi();
|
|
13147
13160
|
console.error(
|
|
13148
13161
|
started
|
|
13149
|
-
? dim("
|
|
13150
|
-
: dim("
|
|
13162
|
+
? dim(" Started it — run `gu` again.\n")
|
|
13163
|
+
: dim(" Start it with: gu-cli api start") +
|
|
13151
13164
|
dim("\n If that fails, check MongoDB is running and see ~/.gonext/local-api.log\n")
|
|
13152
13165
|
);
|
|
13153
13166
|
}
|
|
@@ -13596,7 +13609,7 @@ async function main() {
|
|
|
13596
13609
|
const direct = line === "/rag-local" ? "local" : line === "/rag-cloud" ? "both" : null;
|
|
13597
13610
|
let chosen = direct;
|
|
13598
13611
|
if (!chosen) {
|
|
13599
|
-
console.log(dim(`
|
|
13612
|
+
console.log(dim(` Where this folder's RAG knowledge base lives (${PICK_HINT}):`));
|
|
13600
13613
|
const idx = await pickFromList(
|
|
13601
13614
|
RAG_STORAGE.map(
|
|
13602
13615
|
(r) =>
|
|
@@ -13607,7 +13620,7 @@ async function main() {
|
|
|
13607
13620
|
Math.max(0, RAG_STORAGE.findIndex((r) => r.id === sessionRagStorage))
|
|
13608
13621
|
);
|
|
13609
13622
|
if (idx < 0) {
|
|
13610
|
-
console.log(dim("
|
|
13623
|
+
console.log(dim(" (cancelled)\n"));
|
|
13611
13624
|
continue;
|
|
13612
13625
|
}
|
|
13613
13626
|
chosen = RAG_STORAGE[idx].id;
|
|
@@ -13623,7 +13636,7 @@ async function main() {
|
|
|
13623
13636
|
// switching to cloud leaves the local shards on disk unread, and switching to local
|
|
13624
13637
|
// leaves the S3 ones untouched — either way the new store is built by the next index.
|
|
13625
13638
|
console.log(
|
|
13626
|
-
accent(`
|
|
13639
|
+
accent(` ✓ RAG storage → ${row.label}`) +
|
|
13627
13640
|
dim(`\n ${row.note}`) +
|
|
13628
13641
|
dim("\n Existing shards are not moved — the new store fills as this folder is re-indexed.\n")
|
|
13629
13642
|
);
|
|
@@ -13730,7 +13743,7 @@ async function main() {
|
|
|
13730
13743
|
if (!wasPromptCancelled(e)) throw e;
|
|
13731
13744
|
// The same words the pickers print, for the same event — a cancel should not read as one
|
|
13732
13745
|
// thing when the answer was typed and another when it was arrowed to.
|
|
13733
|
-
console.log(dim("
|
|
13746
|
+
console.log(dim(" (cancelled)\n"));
|
|
13734
13747
|
continue;
|
|
13735
13748
|
}
|
|
13736
13749
|
// SWITCH TO ANOTHER AGENT (task #211) — chosen with the mouse in showOtherAgents, delivered
|