@tiens.nguyen/gu-cli 1.0.703 → 1.0.704
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/agent-model-command.mjs +34 -0
- package/client-expert-api.mjs +10 -7
- package/gu-repl.mjs +50 -7
- package/package.json +1 -1
package/agent-model-command.mjs
CHANGED
|
@@ -466,3 +466,37 @@ export function agentProviderChoices(settings) {
|
|
|
466
466
|
out.push({ action: "add", label: "add a new OpenAI-compatible provider…" });
|
|
467
467
|
return out;
|
|
468
468
|
}
|
|
469
|
+
|
|
470
|
+
// ---------------------------------------------------------------------------------------------
|
|
471
|
+
// The Ollama CLOUD's key is its OWN — and a key is never a key's name.
|
|
472
|
+
// ---------------------------------------------------------------------------------------------
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* The key saved FOR this provider row, or null.
|
|
476
|
+
*
|
|
477
|
+
* Reported: /agent-model → Ollama → https://ollama.com listed "20 on https://ollama.com" and saved the
|
|
478
|
+
* agent without asking for a key; the coder on ollama.com then failed "REJECTED the coding API key".
|
|
479
|
+
* Two things hid it. ollama.com's /api/tags is PUBLIC (200 with no key), so a successful list proves
|
|
480
|
+
* nothing about a key. And "a key for this provider" counted UNBOUND keys — by design usable with any
|
|
481
|
+
* OpenAI-compatible provider — so the account's sk-… key was taken as ollama.com's, and refused there.
|
|
482
|
+
* For the Ollama cloud only a key BOUND to its row counts.
|
|
483
|
+
*/
|
|
484
|
+
export function boundKeyFor(keys, rowId) {
|
|
485
|
+
const id = String(rowId ?? "").trim();
|
|
486
|
+
if (!id || !Array.isArray(keys)) return null;
|
|
487
|
+
return keys.find((k) => k && k.backendId === id) ?? null;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Does what was typed as a key's NAME look like the key itself?
|
|
492
|
+
*
|
|
493
|
+
* Reported in the same log: a key named "sk-E8Iv…3E66" — the secret pasted into the name prompt — is
|
|
494
|
+
* printed in full wherever keys are listed ("API key sk-E8Iv… ····3E66"). Names are shown; secrets
|
|
495
|
+
* must not be. Caught on the known prefixes, and on anything long, unbroken and mixed like a token.
|
|
496
|
+
*/
|
|
497
|
+
export function looksLikeSecret(name) {
|
|
498
|
+
const v = String(name ?? "").trim();
|
|
499
|
+
if (!v) return false;
|
|
500
|
+
if (/^(sk-|sk_|pk-|rk-|ghp_|gho_|github_pat_|xox[abp]-|AKIA|AIza|hf_|gsk_|ollama_)/i.test(v)) return true;
|
|
501
|
+
return v.length >= 24 && !/\s/.test(v) && /[A-Za-z]/.test(v) && /\d/.test(v);
|
|
502
|
+
}
|
package/client-expert-api.mjs
CHANGED
|
@@ -753,14 +753,17 @@ export function mergeOllamaServerBackends(existing, fresh, { isCloud } = {}) {
|
|
|
753
753
|
const cloud = typeof isCloud === "function" ? isCloud : () => false;
|
|
754
754
|
const norm = (u) => String(u ?? "").trim().replace(/\/+$/, "").toLowerCase();
|
|
755
755
|
const rows = Array.isArray(existing) ? existing.filter(Boolean) : [];
|
|
756
|
-
const
|
|
757
|
-
const
|
|
756
|
+
const freshOllama = (Array.isArray(fresh) ? fresh : []).filter((f) => f.kind === "ollama");
|
|
757
|
+
const newUrls = new Set(freshOllama.map((f) => norm(f.url)));
|
|
758
|
+
// A row already at the NEW server's URL is the same provider — the new server takes it over, id and
|
|
759
|
+
// all, so a key bound to it (the Ollama cloud's own key) keeps working and no twin row appears.
|
|
760
|
+
const sameServer = rows.filter((b) => newUrls.has(norm(b.url)));
|
|
761
|
+
const replaced = [...sameServer, ...rows.filter((b) => b.kind === "ollama" && !cloud(b.url) && !sameServer.includes(b))];
|
|
762
|
+
const kept = rows.filter((b) => !replaced.includes(b));
|
|
758
763
|
const out = [];
|
|
759
|
-
for (const f of
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
out.push(id ? { ...f, id } : f);
|
|
763
|
-
}
|
|
764
|
+
for (const f of freshOllama) {
|
|
765
|
+
const id = (sameServer.find((b) => b.id && norm(b.url) === norm(f.url)) ?? replaced.find((b) => b.id))?.id;
|
|
766
|
+
out.push(id ? { ...f, id } : f);
|
|
764
767
|
}
|
|
765
768
|
out.push(...kept);
|
|
766
769
|
for (const f of Array.isArray(fresh) ? fresh : []) {
|
package/gu-repl.mjs
CHANGED
|
@@ -112,7 +112,7 @@ import {
|
|
|
112
112
|
shouldCancelPrompt,
|
|
113
113
|
} from "./terminal-layout.mjs";
|
|
114
114
|
import { completedFlush } from "./completed-flush.mjs";
|
|
115
|
-
import { codingBannerLabel, SECRET_KEY_HINT } from "./coding-key-prompt.mjs";
|
|
115
|
+
import { codingBannerLabel, isOllamaCloudUrl, SECRET_KEY_HINT } from "./coding-key-prompt.mjs";
|
|
116
116
|
import { codingChoiceTags, coderDisplayName, ollamaWhere } from "./coding-choice-tags.mjs";
|
|
117
117
|
import {
|
|
118
118
|
isDoubleClick,
|
|
@@ -4955,6 +4955,28 @@ async function reconfigureOllamaServer() {
|
|
|
4955
4955
|
console.log(GUTTER + dim("if this machine's local API is old, restart it with `gu api restart`.\n"));
|
|
4956
4956
|
return;
|
|
4957
4957
|
}
|
|
4958
|
+
// THE OLLAMA CLOUD NEEDS ITS OWN KEY. Reported: choosing https://ollama.com here listed its 20 models
|
|
4959
|
+
// (that list is public) and saved, without a key — and the next turn failed "REJECTED the coding API
|
|
4960
|
+
// key", having borrowed an unrelated sk-… key. Asked for now, bound to the row just saved.
|
|
4961
|
+
{
|
|
4962
|
+
const cloudUrls = [coder?.normalized?.url, agent?.normalized?.url].filter(
|
|
4963
|
+
(u, i, all) => u && all.indexOf(u) === i && isOllamaCloudUrl(u)
|
|
4964
|
+
);
|
|
4965
|
+
if (cloudUrls.length) {
|
|
4966
|
+
const fresh = await withPendingStatus("checking the Ollama cloud key", fetchWorkerSettings);
|
|
4967
|
+
const keys = await fetchCodingKeys();
|
|
4968
|
+
const ps = await import("./coding-provider-setup.mjs");
|
|
4969
|
+
for (const u of cloudUrls) {
|
|
4970
|
+
const row = ps
|
|
4971
|
+
.existingBackendRows(fresh ?? {})
|
|
4972
|
+
.find((b) => ps.ensureV1Root(String(b.url ?? "")).toLowerCase() === ps.ensureV1Root(u).toLowerCase());
|
|
4973
|
+
if (row && !am.boundKeyFor(keys, row.id)) {
|
|
4974
|
+
console.log(yellow(` ${hostOf(u)} is the Ollama cloud — it needs its own API key (the model list worked without one).`));
|
|
4975
|
+
await askKeyForRow(row, hostOf(u));
|
|
4976
|
+
}
|
|
4977
|
+
}
|
|
4978
|
+
}
|
|
4979
|
+
}
|
|
4958
4980
|
// THE ACCOUNT DEFAULT JUST CHANGED — say so in memory, or the bar reports the old one.
|
|
4959
4981
|
//
|
|
4960
4982
|
// The override is cleared just below, correctly: what was picked here IS the account default
|
|
@@ -5514,7 +5536,7 @@ async function addCodingProvider(roleName = "coding") {
|
|
|
5514
5536
|
// NOT DEFAULTED, by request. An empty name becomes "Unknown" at the API, which is the same
|
|
5515
5537
|
// thing every other add-a-key path produces — the URL default was asked for, this was not.
|
|
5516
5538
|
console.log("\n" + GUTTER + dim('A name just tells keys apart — "work", "personal".'));
|
|
5517
|
-
const name =
|
|
5539
|
+
const name = await askKeyName();
|
|
5518
5540
|
console.log(GUTTER + dim(SECRET_KEY_HINT));
|
|
5519
5541
|
const secret = (await askSecret(` API key${name ? ` for ${name}` : ""}: `)).trim();
|
|
5520
5542
|
if (!secret) {
|
|
@@ -6048,10 +6070,25 @@ async function saveAgentModel(agentUrl, model, host) {
|
|
|
6048
6070
|
return true;
|
|
6049
6071
|
}
|
|
6050
6072
|
|
|
6073
|
+
/**
|
|
6074
|
+
* Ask for a key's NAME — and refuse the key itself as a name: names are printed wherever keys are
|
|
6075
|
+
* listed, so a pasted secret there is a secret on screen (reported: "API key sk-E8Iv… ····3E66").
|
|
6076
|
+
*/
|
|
6077
|
+
async function askKeyName(prompt = " key name: ") {
|
|
6078
|
+
const am = await import("./agent-model-command.mjs");
|
|
6079
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
6080
|
+
const name = (await ask(prompt)).trim();
|
|
6081
|
+
if (!am.looksLikeSecret(name)) return name;
|
|
6082
|
+
console.log(yellow(" that looks like the API key itself — the NAME is shown on screen, so it must not be the key."));
|
|
6083
|
+
console.log(GUTTER + dim('type a short label like "work", or press Enter for none — the key comes next.'));
|
|
6084
|
+
}
|
|
6085
|
+
return "";
|
|
6086
|
+
}
|
|
6087
|
+
|
|
6051
6088
|
/** Ask for a key and store it BOUND to this provider row. Returns the secret, or "" if cancelled. */
|
|
6052
6089
|
async function askKeyForRow(row, host) {
|
|
6053
6090
|
console.log("\n" + GUTTER + dim('A name just tells keys apart — "work", "personal".'));
|
|
6054
|
-
const name =
|
|
6091
|
+
const name = await askKeyName();
|
|
6055
6092
|
console.log(GUTTER + dim(SECRET_KEY_HINT));
|
|
6056
6093
|
const secret = (await askSecret(` API key for ${host}${name ? ` (${name})` : ""}: `)).trim();
|
|
6057
6094
|
if (!secret) {
|
|
@@ -6186,9 +6223,15 @@ async function addOllamaAgent() {
|
|
|
6186
6223
|
: null;
|
|
6187
6224
|
let key = "";
|
|
6188
6225
|
let newRow = null;
|
|
6226
|
+
let hasBoundKey = false;
|
|
6189
6227
|
if (target.cloud) {
|
|
6190
|
-
|
|
6191
|
-
|
|
6228
|
+
// A KEY SAVED FOR THIS PROVIDER — not any key the account holds. ollama.com's model list answers
|
|
6229
|
+
// without a key, so listing cannot catch a borrowed one; only asking can.
|
|
6230
|
+
if (row) {
|
|
6231
|
+
const keys = await withPendingStatus(`reading the keys for ${target.host}`, fetchCodingKeys);
|
|
6232
|
+
hasBoundKey = Boolean(am.boundKeyFor(keys, row.id));
|
|
6233
|
+
}
|
|
6234
|
+
if (!hasBoundKey) {
|
|
6192
6235
|
if (!row) {
|
|
6193
6236
|
if (existing.length >= ps.MAX_BACKENDS) {
|
|
6194
6237
|
console.log(yellow(` ${ps.MAX_BACKENDS} providers is the limit — remove one in web Settings → Agent first.\n`));
|
|
@@ -6251,7 +6294,7 @@ async function addOllamaAgent() {
|
|
|
6251
6294
|
row = newRow;
|
|
6252
6295
|
console.log(GUTTER + dim(`${target.host} is also offered in /model now.`));
|
|
6253
6296
|
}
|
|
6254
|
-
if (target.cloud && key && !
|
|
6297
|
+
if (target.cloud && key && !hasBoundKey) {
|
|
6255
6298
|
const id = await withPendingStatus("saving the key", () => addCodingKey("Ollama cloud", key, String(row?.id ?? "")));
|
|
6256
6299
|
if (!id) {
|
|
6257
6300
|
console.log(red(" couldn't save the key — the agent was not changed.\n"));
|
|
@@ -7191,7 +7234,7 @@ async function deleteCodingKey(id) {
|
|
|
7191
7234
|
*/
|
|
7192
7235
|
async function promptNewCodingKey(backendId = "", provider = "") {
|
|
7193
7236
|
console.log("\n" + GUTTER + dim("A name just tells them apart — \"work\", \"personal\".\n"));
|
|
7194
|
-
const name =
|
|
7237
|
+
const name = await askKeyName(" Name for this key: ");
|
|
7195
7238
|
console.log(GUTTER + dim(SECRET_KEY_HINT));
|
|
7196
7239
|
const secret = (await askSecret(` API key${name ? ` for ${name}` : ""}: `)).trim();
|
|
7197
7240
|
if (!secret) {
|
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.704",
|
|
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",
|