@tiens.nguyen/gu-cli 1.0.741 → 1.0.743

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.
@@ -24,6 +24,8 @@ const identity = (s) => s;
24
24
  * and on Ctrl+K to replace one — and they were two hand-written copies of the same idea. A hint
25
25
  * added to one of them would have left the other exactly as confusing.
26
26
  */
27
+ import { keyInUse } from "./server-keys.mjs";
28
+
27
29
  export const SECRET_KEY_HINT = "type or paste the key, then press Enter — it stays masked";
28
30
 
29
31
  /**
@@ -217,13 +219,26 @@ export function codingKeyFallbackLine(kj, jobId = "") {
217
219
  * when a choice HAS been made, and when only one key exists — with one there is nothing to
218
220
  * choose, and a warning on a correct setup is how warnings stop being read.
219
221
  */
220
- export function unchosenKeyWarning({ codingKind = "", codingUrl = "", selectedKeyId = "", keys = null } = {}) {
222
+ export function unchosenKeyWarning({ codingKind = "", codingUrl = "", selectedKeyId = "", keys = null, backendId = "" } = {}) {
221
223
  const quiet = { warn: false, using: null, why: "" };
222
224
  // Local Ollama and MLX need no credential; the Ollama cloud does.
223
225
  if (!codingNeedsKey(codingKind, codingUrl)) return quiet;
224
- if (!Array.isArray(keys) || keys.length < 2) return quiet;
226
+ if (!Array.isArray(keys)) return quiet;
225
227
  if (String(selectedKeyId ?? "").trim()) return quiet;
226
- return { warn: true, using: keys[0], why: "none-chosen" };
228
+ // KEYS BELONG TO A SERVER (#245). Reported: the bar said "key not chosen" beside ollama.com while
229
+ // the key picker showed its one key, hsbc_ollama, "(in use)" — because this counted all seven keys
230
+ // on the ACCOUNT (Anthropic, OpenAI, …) where the API only ever chooses among THIS server's keys.
231
+ // With the server known: its keys, else the keys tied to no server; and a server default IS a
232
+ // choice. Without it (an older caller) the old account-wide count stands.
233
+ if (backendId) {
234
+ const own = keys.filter((k) => k && k.backendId === backendId);
235
+ const pool = own.length ? own : keys.filter((k) => k && !k.backendId);
236
+ if (own.some((k) => k.default)) return quiet;
237
+ if (pool.length < 2) return quiet;
238
+ return { warn: true, using: keyInUse(keys, backendId) ?? pool[0], why: "none-chosen", count: pool.length };
239
+ }
240
+ if (keys.length < 2) return quiet;
241
+ return { warn: true, using: keys[0], why: "none-chosen", count: keys.length };
227
242
  }
228
243
 
229
244
  /**
@@ -245,14 +260,14 @@ export function unchosenKeyWarning({ codingKind = "", codingUrl = "", selectedKe
245
260
  * qualified ("<kind>::<model>", task #123) stores the bare form and is still a valid pick.
246
261
  */
247
262
  /** The folder's coder — its kind AND its URL, which together decide whether it needs a key. */
248
- export function effectiveCoder({ sessionCodingModel = "", choices = null, defaultKind = "", defaultUrl = "" } = {}) {
263
+ export function effectiveCoder({ sessionCodingModel = "", choices = null, defaultKind = "", defaultUrl = "", defaultBackendId = "" } = {}) {
249
264
  const want = String(sessionCodingModel ?? "").trim();
250
265
  const list = Array.isArray(choices) ? choices : [];
251
266
  if (want) {
252
267
  const hit = list.find((c) => c?.id === want || c?.model === want);
253
- if (hit) return { kind: String(hit.kind ?? ""), url: String(hit.url ?? "") };
268
+ if (hit) return { kind: String(hit.kind ?? ""), url: String(hit.url ?? ""), backendId: String(hit.backendId ?? "") };
254
269
  }
255
- return { kind: String(defaultKind ?? ""), url: String(defaultUrl ?? "") };
270
+ return { kind: String(defaultKind ?? ""), url: String(defaultUrl ?? ""), backendId: String(defaultBackendId ?? "") };
256
271
  }
257
272
 
258
273
  export function effectiveCoderKind({
package/gu-repl.mjs CHANGED
@@ -6251,6 +6251,8 @@ async function chooseModel() {
6251
6251
  codingKind: "openai",
6252
6252
  selectedKeyId: sessionCodingKeyId,
6253
6253
  keys: keysNow,
6254
+ // Only THIS server's keys are a choice to make (#245) — not every key on the account.
6255
+ backendId: ks.backendId,
6254
6256
  }).warn;
6255
6257
  const line = codingKeyLine({
6256
6258
  keys: keysNow,
@@ -6272,7 +6274,8 @@ async function chooseModel() {
6272
6274
  // nothing else, so the screen that follows a pick answered the less common question and left
6273
6275
  // the more common one — "how do I change the model" — with no visible answer at all. Reported
6274
6276
  // three times from exactly this view, twice after being told where the picker entry was.
6275
- console.log(dim(" /provider to change this provider's model, URL, or remove it\n"));
6277
+ // /provider is hidden (see HIDDEN_COMMANDS): point at the row of /model that does the same.
6278
+ console.log(dim(" to change this provider's model or URL, or remove it: /model → add or change a provider\n"));
6276
6279
  }
6277
6280
  // Task #125: the GLOBAL lifetime total is per-backend, so re-fetch it for the newly
6278
6281
  // selected backend NOW — otherwise the footer would keep showing the previous backend's
@@ -10976,11 +10979,12 @@ async function main() {
10976
10979
  // machine an Ollama coder, which needs no key — while the folder was on gpt-5. Reading
10977
10980
  // it directly made this check answer about a coder nobody was using, and the mark only
10978
10981
  // appeared after /model, which re-probes WITH the override.
10979
- const { kind, url } = effectiveCoder({
10982
+ const { kind, url, backendId } = effectiveCoder({
10980
10983
  sessionCodingModel,
10981
10984
  choices: probe?.codingChoices,
10982
10985
  defaultKind: p?.codingKind,
10983
10986
  defaultUrl: probe?.codingChoices?.find?.((c) => c?.isDefault)?.url ?? "",
10987
+ defaultBackendId: probe?.codingChoices?.find?.((c) => c?.isDefault)?.backendId ?? "",
10984
10988
  });
10985
10989
  const allKeys =
10986
10990
  codingNeedsKey(kind, url) && !sessionCodingKeyId ? await fetchCodingKeys() : null;
@@ -10989,6 +10993,7 @@ async function main() {
10989
10993
  codingUrl: url,
10990
10994
  selectedKeyId: sessionCodingKeyId,
10991
10995
  keys: allKeys,
10996
+ backendId,
10992
10997
  });
10993
10998
  // THE BAR CARRIES IT TOO, from the same decision — a line printed at startup scrolls
10994
10999
  // away within a few turns, and the report was precisely that the warning has to be
@@ -10996,7 +11001,7 @@ async function main() {
10996
11001
  keyChoicePending = w.warn;
10997
11002
  if (w.warn) {
10998
11003
  console.log(
10999
- GUTTER + orange(` ${allKeys.length} API keys stored and this folder has not chosen one`) +
11004
+ GUTTER + orange(` ${w.count} API keys for this server and this folder has not chosen one`) +
11000
11005
  dim(` — using ${w.using.name} ····${w.using.tail}.`)
11001
11006
  );
11002
11007
  console.log(GUTTER + dim(" ctrl+k picks the right one; a wrong key fails mid-task.\n"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gu-cli",
3
- "version": "1.0.741",
3
+ "version": "1.0.743",
4
4
  "description": "Gu CLI — the gu terminal plus the local worker that runs agent / OCR / PDF / embedding jobs on your Mac (Ollama / MLX / OpenAI-compatible).",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -29,6 +29,8 @@
29
29
  const WIDE_RANGES =
30
30
  /[ᄀ-ᅟ⺀-〾ぁ-㏿㐀-䶿一-鿿ꀀ-꓏ꥠ-꥿가-힣豈-﫿︐-︙︰-﹯＀-⦆¢-₩]/u;
31
31
  const ZERO_WIDTH = /[\p{Mn}\p{Me}​-‏⁠]/u;
32
+ import { keyInUse } from "./server-keys.mjs";
33
+
32
34
  export function charWidth(ch) {
33
35
  if (!ch) return 0;
34
36
  if (ZERO_WIDTH.test(ch)) return 0;
@@ -237,6 +239,29 @@ export function codingKeyLine({
237
239
  if (keys.some((k) => k.backendId)) return `not tied to ${where}`;
238
240
  return "";
239
241
  };
242
+ if (!selectedId && backendId) {
243
+ // KEYS BELONG TO A SERVER (#245). Reported: /model said "test1 ····AqsA · not chosen for this
244
+ // folder — using the first key · that key is for another provider" while the picker under it
245
+ // showed claude2 "(in use · default · first key)" and the bar 6QAA. test1 was the first key on
246
+ // the ACCOUNT; the API hands a coder its SERVER's key — the one keyInUse names, the same answer
247
+ // the picker gives. "Not chosen" only when this server has several keys and no default.
248
+ const used = keyInUse(keys, backendId);
249
+ if (used) {
250
+ const own = keys.filter((k) => k && k.backendId === backendId);
251
+ const pool = own.length ? own : keys.filter((k) => k && !k.backendId);
252
+ const notes = [];
253
+ if (pool.length > 1 && !own.some((k) => k.default)) notes.push("not chosen for this folder — using the first key");
254
+ else if (used.default && own.length > 1) notes.push("this server's default");
255
+ const provider = noteFor(used);
256
+ if (provider) notes.push(provider);
257
+ return {
258
+ text: `${used.name} ····${used.tail}`,
259
+ verb: pool.length > 1 ? "change" : "replace",
260
+ isDefault: true,
261
+ ...(notes.length ? { note: notes.join(" · ") } : {}),
262
+ };
263
+ }
264
+ }
240
265
  if (!selectedId) {
241
266
  // NEVER PICKED — AND THAT IS WORTH SAYING OUT LOUD (reported). This folder rides the account
242
267
  // default, which is simply the FIRST key in the list. With several stored, the one being used