@tiens.nguyen/gu-cli 1.0.898 → 1.0.900
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/gu-repl.mjs +27 -89
- package/package.json +1 -1
package/gu-repl.mjs
CHANGED
|
@@ -1053,7 +1053,6 @@ const COMMANDS = [
|
|
|
1053
1053
|
// already has for exactly this — /quit, /rag-local.
|
|
1054
1054
|
// /host-mode is gone (owner, 11-07): modes are chosen at setup; `gu clear` sets up again.
|
|
1055
1055
|
// /undo is HIDDEN_COMMANDS now — still works when typed.
|
|
1056
|
-
{ name: "/server", desc: "pick a deployment server (host/user) for this session" },
|
|
1057
1056
|
{ name: "/proxy", usage: "/proxy [show|set|on|off|clear|test]", desc: "route MODEL calls through an HTTP proxy (local calls always go direct)" },
|
|
1058
1057
|
{ name: "/s3", usage: "/s3 [test]", desc: "set up YOUR S3 bucket (RAG index, and the PDFs/downloads gu makes) and check the AWS keys work — without it, files are saved on this computer" },
|
|
1059
1058
|
{ name: "/context-window", usage: "/context-window [tokens|auto]", desc: "tell gu how big the coding model's context is — hosted models cannot be probed" },
|
|
@@ -7171,7 +7170,6 @@ async function useCoderInThisFolder({ ps, rows, beforeChoices, url, host, locali
|
|
|
7171
7170
|
console.log(GUTTER + dim("restart it with `gu api restart`, then add that provider again.\n"));
|
|
7172
7171
|
}
|
|
7173
7172
|
sessionCodingModel = offered.id;
|
|
7174
|
-
setModelFallback(null); // #343
|
|
7175
7173
|
sessionCodingLabel = model;
|
|
7176
7174
|
sessionCodingUrl = url;
|
|
7177
7175
|
// …and the host the bar puts in front of it (#263): left alone, the PREVIOUS coder's
|
|
@@ -7180,6 +7178,7 @@ async function useCoderInThisFolder({ ps, rows, beforeChoices, url, host, locali
|
|
|
7180
7178
|
// The folder follows the SERVER's key (task #245) — its default, which a first key already is.
|
|
7181
7179
|
sessionCodingKeyId = "";
|
|
7182
7180
|
sessionApiKeyTail = key ? key.slice(-4) : "";
|
|
7181
|
+
setModelFallback(null); // #343 — AFTER the name, server and tail: it repaints the bar (GH #397)
|
|
7183
7182
|
keyChoicePending = false;
|
|
7184
7183
|
await saveSessionCoding(cwd);
|
|
7185
7184
|
await saveSessionCodingKeyId(cwd);
|
|
@@ -7202,7 +7201,12 @@ async function useCoderInThisFolder({ ps, rows, beforeChoices, url, host, locali
|
|
|
7202
7201
|
// The bar still said "coding model not set · /model" under the success line of the FIRST model
|
|
7203
7202
|
// added on a fresh install, until the next turn repainted it (GH #397).
|
|
7204
7203
|
noModelAtAll = after?.payload ? nothingToAnswerWith(after.payload) : false;
|
|
7205
|
-
if (after?.payload)
|
|
7204
|
+
if (after?.payload) {
|
|
7205
|
+
agentMissing = after.payload.agentMissing === true;
|
|
7206
|
+
// The first model added IS the account default now. Startup read the default while there was
|
|
7207
|
+
// none, so picking "(default)" later left the bar with no model name at all (GH #397).
|
|
7208
|
+
seedDefaultCoder(after.payload);
|
|
7209
|
+
}
|
|
7206
7210
|
if (barPinned()) paintBar();
|
|
7207
7211
|
}
|
|
7208
7212
|
|
|
@@ -7462,7 +7466,6 @@ async function applyChosenCoder(chosen, { def = "", tag = "", quick = false } =
|
|
|
7462
7466
|
tail: sessionApiKeyTail,
|
|
7463
7467
|
};
|
|
7464
7468
|
sessionCodingModel = chosen.id === def ? "" : chosen.id;
|
|
7465
|
-
setModelFallback(null); // #343: the warning named the coder that was just replaced
|
|
7466
7469
|
// Remember the friendly name for the token footer, which should never show a raw
|
|
7467
7470
|
// "<kind>::<model>" wire id.
|
|
7468
7471
|
sessionCodingLabel = chosen.id === def ? "" : chosen.model;
|
|
@@ -7472,6 +7475,12 @@ async function applyChosenCoder(chosen, { def = "", tag = "", quick = false } =
|
|
|
7472
7475
|
// The server goes WITH the model, before any await (#263): set after the save below, the bar
|
|
7473
7476
|
// could repaint in between and show the NEW model under the OLD server for a moment.
|
|
7474
7477
|
activeCoderWhere = coderWhere(chosen.kind, chosen.url);
|
|
7478
|
+
// The old provider's key tail must not sit beside the new model while the key is looked up (it
|
|
7479
|
+
// was shown for a moment: Kimi with Qwen's tail). Blank until coderKeyState answers below.
|
|
7480
|
+
sessionApiKeyTail = "";
|
|
7481
|
+
// AFTER the fields above: this repaints the bar, and between the id and its name it showed the
|
|
7482
|
+
// raw "<row id>::<model>" wire id for a frame (GH #397). #343: the warning named the old coder.
|
|
7483
|
+
setModelFallback(null);
|
|
7475
7484
|
// WRITTEN NOW, not at the end of the next turn: `/model` then `/exit` is how the choice is
|
|
7476
7485
|
// made and then tested, and that path never reaches saveSession.
|
|
7477
7486
|
await saveSessionCoding(resolve(process.cwd()));
|
|
@@ -9827,39 +9836,6 @@ async function agentModelOnServer({ back = false, start = "menu" } = {}) {
|
|
|
9827
9836
|
console.log(dim(" takes effect on your next question.\n"));
|
|
9828
9837
|
}
|
|
9829
9838
|
|
|
9830
|
-
// Probe whether SSH KEY auth already works for a server (so we can tell the user to run
|
|
9831
|
-
// ssh-copy-id if not). BatchMode=yes = never prompt for a password → exit!=0 if no key.
|
|
9832
|
-
async function checkServerKeyAuth(s) {
|
|
9833
|
-
try {
|
|
9834
|
-
const ok = await new Promise((resolve) => {
|
|
9835
|
-
const p = spawn(
|
|
9836
|
-
"ssh",
|
|
9837
|
-
[
|
|
9838
|
-
"-o", "BatchMode=yes",
|
|
9839
|
-
"-o", "ConnectTimeout=6",
|
|
9840
|
-
"-o", "StrictHostKeyChecking=accept-new",
|
|
9841
|
-
`${s.user}@${s.host}`,
|
|
9842
|
-
"true",
|
|
9843
|
-
],
|
|
9844
|
-
{ stdio: "ignore" }
|
|
9845
|
-
);
|
|
9846
|
-
p.on("exit", (code) => resolve(code === 0));
|
|
9847
|
-
p.on("error", () => resolve(false));
|
|
9848
|
-
});
|
|
9849
|
-
if (ok) console.log(dim(" ✓ key auth works — deploys will connect without a password."));
|
|
9850
|
-
else
|
|
9851
|
-
console.log(
|
|
9852
|
-
yellow(" key auth not set up yet.") +
|
|
9853
|
-
dim(` Run once in your terminal: `) +
|
|
9854
|
-
`ssh-copy-id ${s.user}@${s.host}`
|
|
9855
|
-
);
|
|
9856
|
-
} catch {
|
|
9857
|
-
/* best-effort check — never block the picker */
|
|
9858
|
-
}
|
|
9859
|
-
}
|
|
9860
|
-
|
|
9861
|
-
// /server — pick a deployment target (from the web app's Servers registry) for this
|
|
9862
|
-
// session. Host/user only, never a secret; the agent deploys here with KEY auth (#69).
|
|
9863
9839
|
/**
|
|
9864
9840
|
* `/proxy` — configure the outbound proxy for MODEL calls (task #160).
|
|
9865
9841
|
*
|
|
@@ -10248,53 +10224,6 @@ async function runProxyCommand(line) {
|
|
|
10248
10224
|
}
|
|
10249
10225
|
}
|
|
10250
10226
|
|
|
10251
|
-
async function chooseServer() {
|
|
10252
|
-
let servers;
|
|
10253
|
-
try {
|
|
10254
|
-
const res = await withPendingStatus("loading servers", () =>
|
|
10255
|
-
fetch(`${apiBase}/api/worker/deploy-servers`, {
|
|
10256
|
-
headers: { "X-Worker-Key": workerKey },
|
|
10257
|
-
signal: AbortSignal.timeout(API_CALL_TIMEOUT_MS),
|
|
10258
|
-
})
|
|
10259
|
-
);
|
|
10260
|
-
const data = await res.json().catch(() => ({}));
|
|
10261
|
-
if (!res.ok) {
|
|
10262
|
-
console.log(red(` couldn't load servers: ${data?.error || `HTTP ${res.status}`}\n`));
|
|
10263
|
-
return;
|
|
10264
|
-
}
|
|
10265
|
-
servers = Array.isArray(data?.servers) ? data.servers : [];
|
|
10266
|
-
} catch (err) {
|
|
10267
|
-
console.log(red(` ${describeApiFailure(err, "couldn't load servers")}\n`));
|
|
10268
|
-
return;
|
|
10269
|
-
}
|
|
10270
|
-
if (servers.length === 0) {
|
|
10271
|
-
console.log(dim(" No deployment servers yet — add them in the web app → Servers.\n"));
|
|
10272
|
-
return;
|
|
10273
|
-
}
|
|
10274
|
-
// Arrow-key picker: ↑/↓ to move, Enter to choose (no number typing). The list ends with
|
|
10275
|
-
// a "none" row to clear the selection. Pre-highlight the current pick if there is one.
|
|
10276
|
-
const labels = servers.map((s) => `${s.name} (${s.user}@${s.host})`);
|
|
10277
|
-
labels.push("none (clear selection)");
|
|
10278
|
-
const activeIdx = servers.findIndex((s) => s.host === selectedServer?.host);
|
|
10279
|
-
console.log(dim(` Choose a deployment server (${PICK_HINT}):`));
|
|
10280
|
-
const idx = await pickFromList(labels, activeIdx >= 0 ? activeIdx : 0);
|
|
10281
|
-
if (idx < 0) {
|
|
10282
|
-
console.log(dim(" (cancelled)\n"));
|
|
10283
|
-
return;
|
|
10284
|
-
}
|
|
10285
|
-
if (idx === servers.length) {
|
|
10286
|
-
// the trailing "none" row
|
|
10287
|
-
selectedServer = null;
|
|
10288
|
-
console.log(dim(" ✓ deployment server cleared.\n"));
|
|
10289
|
-
return;
|
|
10290
|
-
}
|
|
10291
|
-
const s = servers[idx];
|
|
10292
|
-
selectedServer = { name: s.name, host: s.host, user: s.user };
|
|
10293
|
-
console.log(accent(` ✓ deploy target → ${s.name} (${s.user}@${s.host})`));
|
|
10294
|
-
await checkServerKeyAuth(selectedServer);
|
|
10295
|
-
console.log("");
|
|
10296
|
-
}
|
|
10297
|
-
|
|
10298
10227
|
/**
|
|
10299
10228
|
* Ask for the coding model's API key if the resolved coder needs one and none is stored.
|
|
10300
10229
|
*
|
|
@@ -14543,6 +14472,20 @@ async function main() {
|
|
|
14543
14472
|
// payload fields, which key-choice-regression.test.mjs forbids for exactly that reason.
|
|
14544
14473
|
activeCoderWhere = whereOfActiveCoder(probe?.codingChoices, sessionCodingModel);
|
|
14545
14474
|
}
|
|
14475
|
+
// THE KEY TAIL BESIDE THE MODEL, at startup too (GH #397). It was read only when the folder had
|
|
14476
|
+
// its own key choice, so the normal case — the server's default key — showed a model with no
|
|
14477
|
+
// key until the first /model switch.
|
|
14478
|
+
if (!sessionApiKeyTail) {
|
|
14479
|
+
try {
|
|
14480
|
+
const choices = Array.isArray(probe?.codingChoices) ? probe.codingChoices : [];
|
|
14481
|
+
const active = choices.find((c) => c.id === sessionCodingModel) ?? choices.find((c) => c.isDefault);
|
|
14482
|
+
const { codingNeedsKey } = await import("./coding-key-prompt.mjs");
|
|
14483
|
+
if (active && codingNeedsKey(active.kind, active.url)) {
|
|
14484
|
+
const kd = await fetchCodingKey(active.backendId || "");
|
|
14485
|
+
sessionApiKeyTail = kd?.key ? kd.key.slice(-4) : "";
|
|
14486
|
+
}
|
|
14487
|
+
} catch { /* the bar works without the tail */ }
|
|
14488
|
+
}
|
|
14546
14489
|
// Task #133: no RAG storage question any more — the terminal ALWAYS keeps the index on
|
|
14547
14490
|
// this machine. `/rag-cloud` opts into additionally mirroring it to the user's S3 bucket.
|
|
14548
14491
|
// Startup banner (task #131): the gu logo on the left, compact status facts on the
|
|
@@ -15479,11 +15422,6 @@ async function main() {
|
|
|
15479
15422
|
await syncEmbedWarning({ repair: false });
|
|
15480
15423
|
continue;
|
|
15481
15424
|
}
|
|
15482
|
-
if (line === "/server") {
|
|
15483
|
-
await chooseServer();
|
|
15484
|
-
await saveSession(cwd, history); // remember the pick for this folder
|
|
15485
|
-
continue;
|
|
15486
|
-
}
|
|
15487
15425
|
if (line === "/full-log" || line.startsWith("/full-log ")) {
|
|
15488
15426
|
await runFullLogCommand(line);
|
|
15489
15427
|
continue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiens.nguyen/gu-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.900",
|
|
4
4
|
"description": "gu: the coding agent in your terminal. It reads your repo, makes the change and runs the tests, with your own models (Ollama or any OpenAI-compatible provider). Free installs keep the agent, its API and its database on your machine. macOS, Linux and Windows.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|