@tiens.nguyen/gu-cli 1.0.698 → 1.0.699
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 +92 -0
- package/gu-repl.mjs +251 -7
- package/package.json +1 -1
package/agent-model-command.mjs
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { modelRetired, ensureV1Root } from "./coding-provider-setup.mjs";
|
|
21
|
+
import { isOllamaCloudUrl } from "./coding-key-prompt.mjs";
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
/**
|
|
@@ -257,3 +258,94 @@ export function agentListSource(settings) {
|
|
|
257
258
|
export function agentModelsFromBody(dialect, body, now = Date.now()) {
|
|
258
259
|
return dialect === "openai" ? agentModelsFromProvider(body, now) : parseOllamaTags(body);
|
|
259
260
|
}
|
|
261
|
+
|
|
262
|
+
// ---------------------------------------------------------------------------------------------
|
|
263
|
+
// #40 and #41 — the agent model on an OpenAI-compatible provider WITH ITS KEY, and on any Ollama.
|
|
264
|
+
// ---------------------------------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Does listing (and using) this agent source need an API key?
|
|
268
|
+
*
|
|
269
|
+
* Reported (#40): /agent-model on api.openai.com said "HTTP 401" — the list was fetched with no
|
|
270
|
+
* Authorization header at all, so a provider that requires a key could never be listed, however
|
|
271
|
+
* correct the stored key was. An OpenAI-compatible row needs one; so does the Ollama CLOUD. A local
|
|
272
|
+
* or LAN Ollama does not.
|
|
273
|
+
*/
|
|
274
|
+
export function agentSourceNeedsKey(source) {
|
|
275
|
+
const row = source?.row;
|
|
276
|
+
if (!row) return false;
|
|
277
|
+
return source.dialect === "openai" || isOllamaCloudUrl(String(row.url ?? ""));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** The headers a model-list request needs — the bearer only when there is a key to send. */
|
|
281
|
+
export function agentListHeaders(key) {
|
|
282
|
+
const k = String(key ?? "").trim();
|
|
283
|
+
return k ? { Authorization: `Bearer ${k}` } : {};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* An Ollama server as typed → where the agent points, and where its models are listed (#41).
|
|
288
|
+
*
|
|
289
|
+
* Forgiving about what people paste — "127.0.0.1:11434", "http://box:11434/v1", ".../api/tags",
|
|
290
|
+
* "ollama.com" — and strict about the result: the agent field gets `${root}/v1` (the shape every
|
|
291
|
+
* existing install stores, see daemon-control.mjs), the list is `${root}/api/tags`. A bare host is
|
|
292
|
+
* http, except ollama.com, which only answers https.
|
|
293
|
+
*
|
|
294
|
+
* Returns { ok: true, root, agentUrl, tagsUrl, host, cloud } or { ok: false, why }.
|
|
295
|
+
*/
|
|
296
|
+
export function ollamaAgentUrl(raw) {
|
|
297
|
+
let v = String(raw ?? "").trim();
|
|
298
|
+
if (!v) return { ok: false, why: "empty" };
|
|
299
|
+
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(v)) {
|
|
300
|
+
const bareHost = v.split(/[/:]/)[0].toLowerCase();
|
|
301
|
+
v = `${bareHost === "ollama.com" || bareHost.endsWith(".ollama.com") ? "https" : "http"}://${v}`;
|
|
302
|
+
}
|
|
303
|
+
let u;
|
|
304
|
+
try {
|
|
305
|
+
u = new URL(v);
|
|
306
|
+
} catch {
|
|
307
|
+
return { ok: false, why: "not a URL" };
|
|
308
|
+
}
|
|
309
|
+
if (u.protocol !== "http:" && u.protocol !== "https:") return { ok: false, why: "not http(s)" };
|
|
310
|
+
if (!u.hostname) return { ok: false, why: "no host" };
|
|
311
|
+
const path = u.pathname.replace(/\/+$/, "").replace(/\/(v1|api\/tags|api)$/i, "").replace(/\/+$/, "");
|
|
312
|
+
const root = `${u.protocol}//${u.host}${path}`;
|
|
313
|
+
return {
|
|
314
|
+
ok: true,
|
|
315
|
+
root,
|
|
316
|
+
agentUrl: `${root}/v1`,
|
|
317
|
+
tagsUrl: `${root}/api/tags`,
|
|
318
|
+
host: u.host,
|
|
319
|
+
cloud: isOllamaCloudUrl(root),
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* The server to offer by default: the one the agent already uses when that is an Ollama, else this
|
|
325
|
+
* machine's own Ollama.
|
|
326
|
+
*/
|
|
327
|
+
export function ollamaAgentDefault(settings) {
|
|
328
|
+
const source = agentListSource(settings);
|
|
329
|
+
if (source.url && source.dialect === "ollama") {
|
|
330
|
+
const r = ollamaAgentUrl(String(settings?.agentModelUrl ?? ""));
|
|
331
|
+
if (r.ok) return r.root;
|
|
332
|
+
}
|
|
333
|
+
return "http://127.0.0.1:11434";
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* The ways forward /agent-model offers when the list could not be read — and in which order.
|
|
338
|
+
*
|
|
339
|
+
* A KEY PROBLEM GETS ITS OWN ENTRY, FIRST (#40): a 401, or a provider that needs a key with none
|
|
340
|
+
* stored, is fixed by entering the key — not by "adding a provider", which for an already-configured
|
|
341
|
+
* URL could only answer "already configured here". Returns [{ action, label }]; "leave" is always
|
|
342
|
+
* first so Esc and Enter-on-the-top-row both change nothing.
|
|
343
|
+
*/
|
|
344
|
+
export function agentRecoveryOptions({ keyProblem = false, host = "" } = {}) {
|
|
345
|
+
return [
|
|
346
|
+
{ action: "leave", label: "leave it" },
|
|
347
|
+
...(keyProblem ? [{ action: "key", label: `enter the API key for ${host || "this provider"}…` }] : []),
|
|
348
|
+
{ action: "ollama", label: "use an Ollama server for the agent…" },
|
|
349
|
+
{ action: "provider", label: "add an OpenAI-compatible provider for the agent…" },
|
|
350
|
+
];
|
|
351
|
+
}
|
package/gu-repl.mjs
CHANGED
|
@@ -5459,6 +5459,14 @@ async function addCodingProvider(roleName = "coding") {
|
|
|
5459
5459
|
// ALREADY CONFIGURED is its own answer. Saving a second row for the same URL would give the
|
|
5460
5460
|
// picker two rows that are identical in everything a person can see.
|
|
5461
5461
|
const clash = existing.find((b) => ps.ensureV1Root(String(b.url ?? "")) === r.url);
|
|
5462
|
+
if (clash && roleName === "agent") {
|
|
5463
|
+
// FOR THE AGENT, "already configured" IS THE ANSWER, NOT A REFUSAL (#40). The provider and
|
|
5464
|
+
// its key may well be there already; what is missing is the agent pointing at it. Sending the
|
|
5465
|
+
// user to /model — the CODING picker — left no way to finish from here.
|
|
5466
|
+
console.log(GUTTER + dim(`${r.host} is already configured — using it for the agent.`));
|
|
5467
|
+
await pointAgentAtProvider(clash, r.host);
|
|
5468
|
+
return;
|
|
5469
|
+
}
|
|
5462
5470
|
if (clash) {
|
|
5463
5471
|
console.log(GUTTER + dim(`${r.host} is already configured here — pick its model in /model.`));
|
|
5464
5472
|
continue;
|
|
@@ -5998,6 +6006,212 @@ async function chooseEmbedModel() {
|
|
|
5998
6006
|
console.log(dim(` Make sure it is pulled there: ollama pull ${chosen}\n`));
|
|
5999
6007
|
}
|
|
6000
6008
|
|
|
6009
|
+
/** Save the agent model on this machine: the URL the API sends turns to, and the model name. */
|
|
6010
|
+
async function saveAgentModel(agentUrl, model, host) {
|
|
6011
|
+
const md = await import("./model-doctor.mjs");
|
|
6012
|
+
const ok = await withPendingStatus("pointing the agent model at it", () =>
|
|
6013
|
+
md.saveWorkerSettings(apiBase, workerKey, { agentModelUrl: agentUrl, agentModelName: model, agentModel: model })
|
|
6014
|
+
);
|
|
6015
|
+
if (!ok) {
|
|
6016
|
+
console.log(red(` couldn't save the agent model — nothing changed.`));
|
|
6017
|
+
console.log(GUTTER + dim("if this machine's local API is old, restart it with `gu api restart`.\n"));
|
|
6018
|
+
return false;
|
|
6019
|
+
}
|
|
6020
|
+
console.log(accent(` ✓ agent model → ${model}`) + dim(` · ${host}`));
|
|
6021
|
+
console.log(dim(" takes effect on your next question.\n"));
|
|
6022
|
+
return true;
|
|
6023
|
+
}
|
|
6024
|
+
|
|
6025
|
+
/** Ask for a key and store it BOUND to this provider row. Returns the secret, or "" if cancelled. */
|
|
6026
|
+
async function askKeyForRow(row, host) {
|
|
6027
|
+
console.log("\n" + GUTTER + dim('A name just tells keys apart — "work", "personal".'));
|
|
6028
|
+
const name = (await ask(" key name: ")).trim();
|
|
6029
|
+
console.log(GUTTER + dim(SECRET_KEY_HINT));
|
|
6030
|
+
const secret = (await askSecret(` API key for ${host}${name ? ` (${name})` : ""}: `)).trim();
|
|
6031
|
+
if (!secret) {
|
|
6032
|
+
console.log(GUTTER + dim("(cancelled — nothing was saved)\n"));
|
|
6033
|
+
return "";
|
|
6034
|
+
}
|
|
6035
|
+
const id = await withPendingStatus("saving the key", () => addCodingKey(name, secret, String(row?.id ?? "")));
|
|
6036
|
+
if (!id) {
|
|
6037
|
+
console.log(red(" couldn't save the key — nothing changed.\n"));
|
|
6038
|
+
return "";
|
|
6039
|
+
}
|
|
6040
|
+
return secret;
|
|
6041
|
+
}
|
|
6042
|
+
|
|
6043
|
+
/** #40: the key was the problem — enter it, then list again. */
|
|
6044
|
+
async function enterAgentProviderKey(row, host) {
|
|
6045
|
+
if (!row) return;
|
|
6046
|
+
if (!(await askKeyForRow(row, host))) return;
|
|
6047
|
+
await chooseAgentModel();
|
|
6048
|
+
}
|
|
6049
|
+
|
|
6050
|
+
/**
|
|
6051
|
+
* #40: point the agent at a provider that is ALREADY configured — its key if it has none (or a
|
|
6052
|
+
* rejected one), its model list, and the agent settings.
|
|
6053
|
+
*/
|
|
6054
|
+
async function pointAgentAtProvider(row, host) {
|
|
6055
|
+
const ps = await import("./coding-provider-setup.mjs");
|
|
6056
|
+
const am = await import("./agent-model-command.mjs");
|
|
6057
|
+
const { proxyFetch } = await import("./proxy-dispatcher.mjs");
|
|
6058
|
+
const url = ps.ensureV1Root(String(row.url ?? ""));
|
|
6059
|
+
let key = (await withPendingStatus(`reading the key for ${host}`, () => fetchCodingKey(String(row.id ?? "")))).key;
|
|
6060
|
+
if (!key) {
|
|
6061
|
+
console.log(GUTTER + dim(`no API key is stored for ${host} yet.`));
|
|
6062
|
+
key = await askKeyForRow(row, host);
|
|
6063
|
+
if (!key) return;
|
|
6064
|
+
}
|
|
6065
|
+
const list = async (k) =>
|
|
6066
|
+
withPendingStatus(`asking ${host} for its models`, async () => {
|
|
6067
|
+
try {
|
|
6068
|
+
const res = await proxyFetch(ps.modelsUrlFor(url), { headers: am.agentListHeaders(k), signal: AbortSignal.timeout(12_000) });
|
|
6069
|
+
return res.ok ? { body: await res.json().catch(() => null) } : { status: res.status };
|
|
6070
|
+
} catch (err) {
|
|
6071
|
+
return { err };
|
|
6072
|
+
}
|
|
6073
|
+
});
|
|
6074
|
+
let probe = await list(key);
|
|
6075
|
+
if (probe.status === 401 || probe.status === 403) {
|
|
6076
|
+
console.log(yellow(` ${host} rejected the stored key (HTTP ${probe.status}) — enter the right one.`));
|
|
6077
|
+
key = await askKeyForRow(row, host);
|
|
6078
|
+
if (!key) return;
|
|
6079
|
+
probe = await list(key);
|
|
6080
|
+
}
|
|
6081
|
+
let models = [];
|
|
6082
|
+
if (probe.body) {
|
|
6083
|
+
models = am.agentModelsFromProvider(probe.body).map((m) => m.model);
|
|
6084
|
+
} else {
|
|
6085
|
+
for (const l of ps.describeProbeFailure(probe).lines) console.log(GUTTER + yellow(l));
|
|
6086
|
+
console.log(GUTTER + dim("carrying on — you can type the model id yourself."));
|
|
6087
|
+
}
|
|
6088
|
+
const model = models.length ? await chooseFromModelList(models, "Which model talks?", 0) : (await ask(" model id: ")).trim();
|
|
6089
|
+
if (!model) {
|
|
6090
|
+
console.log(dim(" (cancelled — nothing was saved)\n"));
|
|
6091
|
+
return;
|
|
6092
|
+
}
|
|
6093
|
+
await saveAgentModel(url, model, host);
|
|
6094
|
+
}
|
|
6095
|
+
|
|
6096
|
+
/**
|
|
6097
|
+
* #41: the agent model on an OLLAMA server — this machine's, a LAN box, or the Ollama cloud.
|
|
6098
|
+
*
|
|
6099
|
+
* Local and LAN Ollama take no key: the agent URL and model are saved, and the API sends its
|
|
6100
|
+
* placeholder, which Ollama ignores. The CLOUD (https://ollama.com) needs one, and the API finds an
|
|
6101
|
+
* agent's key through the provider row whose URL matches — so a cloud server reuses the row that is
|
|
6102
|
+
* already there (a kimi-k3 coder on ollama.com has one), or gets one, with the key bound to it.
|
|
6103
|
+
*/
|
|
6104
|
+
async function addOllamaAgent() {
|
|
6105
|
+
const am = await import("./agent-model-command.mjs");
|
|
6106
|
+
const ps = await import("./coding-provider-setup.mjs");
|
|
6107
|
+
const md = await import("./model-doctor.mjs");
|
|
6108
|
+
const { proxyFetch } = await import("./proxy-dispatcher.mjs");
|
|
6109
|
+
const settings = await withPendingStatus("reading your model settings", fetchWorkerSettings);
|
|
6110
|
+
if (!settings) {
|
|
6111
|
+
console.log(red(" couldn't reach the API to read your current settings.\n"));
|
|
6112
|
+
return;
|
|
6113
|
+
}
|
|
6114
|
+
const dflt = am.ollamaAgentDefault(settings);
|
|
6115
|
+
console.log(dim(" An Ollama server: this machine, a box on your network, or https://ollama.com (needs a key)."));
|
|
6116
|
+
console.log(dim(` Enter uses ${dflt} · Esc cancels\n`));
|
|
6117
|
+
|
|
6118
|
+
let target = null;
|
|
6119
|
+
for (let attempt = 0; attempt < 3 && !target; attempt++) {
|
|
6120
|
+
const typed = (await ask(" Ollama server: ")).trim();
|
|
6121
|
+
const r = am.ollamaAgentUrl(typed || dflt);
|
|
6122
|
+
if (!r.ok) {
|
|
6123
|
+
console.log(GUTTER + dim(`"${typed}" is ${r.why} — try host:port, e.g. 127.0.0.1:11434`));
|
|
6124
|
+
continue;
|
|
6125
|
+
}
|
|
6126
|
+
target = r;
|
|
6127
|
+
}
|
|
6128
|
+
if (!target) {
|
|
6129
|
+
console.log(dim(" (nothing saved)\n"));
|
|
6130
|
+
return;
|
|
6131
|
+
}
|
|
6132
|
+
|
|
6133
|
+
// THE CLOUD NEEDS A KEY, and the key has to belong to a provider row with this URL.
|
|
6134
|
+
const existing = ps.existingBackendRows(settings);
|
|
6135
|
+
let row = target.cloud
|
|
6136
|
+
? existing.find((b) => ps.ensureV1Root(String(b.url ?? "")).toLowerCase() === target.agentUrl.toLowerCase()) ?? null
|
|
6137
|
+
: null;
|
|
6138
|
+
let key = "";
|
|
6139
|
+
let newRow = null;
|
|
6140
|
+
if (target.cloud) {
|
|
6141
|
+
if (row) key = (await withPendingStatus(`reading the key for ${target.host}`, () => fetchCodingKey(String(row.id ?? "")))).key;
|
|
6142
|
+
if (!key) {
|
|
6143
|
+
if (!row) {
|
|
6144
|
+
if (existing.length >= ps.MAX_BACKENDS) {
|
|
6145
|
+
console.log(yellow(` ${ps.MAX_BACKENDS} providers is the limit — remove one in web Settings → Agent first.\n`));
|
|
6146
|
+
return;
|
|
6147
|
+
}
|
|
6148
|
+
newRow = { id: ps.mintProviderId(existing.map((b) => String(b.id ?? ""))), kind: "ollama", enabled: true, url: target.agentUrl, defaultModel: "", models: [] };
|
|
6149
|
+
}
|
|
6150
|
+
console.log(GUTTER + dim(`${target.host} is the Ollama cloud — it needs an API key.`));
|
|
6151
|
+
console.log(GUTTER + dim(SECRET_KEY_HINT));
|
|
6152
|
+
key = (await askSecret(` API key for ${target.host}: `)).trim();
|
|
6153
|
+
if (!key) {
|
|
6154
|
+
console.log(GUTTER + dim("(cancelled — nothing was saved)\n"));
|
|
6155
|
+
return;
|
|
6156
|
+
}
|
|
6157
|
+
}
|
|
6158
|
+
}
|
|
6159
|
+
|
|
6160
|
+
// ITS MODELS, LIVE — with the key when it is the cloud.
|
|
6161
|
+
const probe = await withPendingStatus(`asking ${target.host} for its models`, async () => {
|
|
6162
|
+
try {
|
|
6163
|
+
const res = await proxyFetch(target.tagsUrl, { headers: am.agentListHeaders(key), signal: AbortSignal.timeout(12_000) });
|
|
6164
|
+
return res.ok ? { body: await res.json().catch(() => null) } : { status: res.status };
|
|
6165
|
+
} catch (err) {
|
|
6166
|
+
return { err: err?.name === "TimeoutError" ? "no response in 12s" : (err?.message ?? String(err)) };
|
|
6167
|
+
}
|
|
6168
|
+
});
|
|
6169
|
+
if (!probe.body) {
|
|
6170
|
+
const why = probe.status ? `HTTP ${probe.status}` : probe.err;
|
|
6171
|
+
console.log(red(` couldn't list the models on ${target.host} — ${why}.`));
|
|
6172
|
+
if (probe.status === 401 || probe.status === 403) console.log(GUTTER + dim("the key was rejected — nothing was saved.\n"));
|
|
6173
|
+
else console.log(GUTTER + dim("check the server is running and reachable (and /proxy if this machine needs one) — nothing was saved.\n"));
|
|
6174
|
+
return;
|
|
6175
|
+
}
|
|
6176
|
+
const models = am.parseOllamaTags(probe.body);
|
|
6177
|
+
if (!models.length) {
|
|
6178
|
+
console.log(yellow(` ${target.host} answered but lists no models — pull one there first (ollama pull …).\n`));
|
|
6179
|
+
return;
|
|
6180
|
+
}
|
|
6181
|
+
const rows = am.agentModelRows(models, am.currentAgentModel(settings));
|
|
6182
|
+
console.log(dim(` ${models.length} on ${target.root} (${PICK_HINT}):`));
|
|
6183
|
+
const idx = await pickFromList(
|
|
6184
|
+
rows.map((r) => r.model + (r.embedder ? dim(" · embedding model, not for chat") : "") + (r.current ? accent(" (current)") : "")),
|
|
6185
|
+
Math.max(0, rows.findIndex((r) => r.current))
|
|
6186
|
+
);
|
|
6187
|
+
if (idx < 0) {
|
|
6188
|
+
console.log(dim(" (cancelled — nothing was saved)\n"));
|
|
6189
|
+
return;
|
|
6190
|
+
}
|
|
6191
|
+
const model = rows[idx].model;
|
|
6192
|
+
|
|
6193
|
+
// SAVE: the cloud's row and key first (the agent's key is found through them), then the agent.
|
|
6194
|
+
if (newRow) {
|
|
6195
|
+
const ok = await withPendingStatus("saving the Ollama cloud provider", () =>
|
|
6196
|
+
md.saveWorkerSettings(apiBase, workerKey, { agentCodingBackends: [...existing, { ...newRow, defaultModel: model }] })
|
|
6197
|
+
);
|
|
6198
|
+
if (!ok) {
|
|
6199
|
+
console.log(red(" couldn't save the provider — nothing changed.\n"));
|
|
6200
|
+
return;
|
|
6201
|
+
}
|
|
6202
|
+
row = newRow;
|
|
6203
|
+
console.log(GUTTER + dim(`${target.host} is also offered in /model now.`));
|
|
6204
|
+
}
|
|
6205
|
+
if (target.cloud && key && !(await fetchCodingKey(String(row?.id ?? ""))).key) {
|
|
6206
|
+
const id = await withPendingStatus("saving the key", () => addCodingKey("Ollama cloud", key, String(row?.id ?? "")));
|
|
6207
|
+
if (!id) {
|
|
6208
|
+
console.log(red(" couldn't save the key — the agent was not changed.\n"));
|
|
6209
|
+
return;
|
|
6210
|
+
}
|
|
6211
|
+
}
|
|
6212
|
+
await saveAgentModel(target.agentUrl, model, target.host);
|
|
6213
|
+
}
|
|
6214
|
+
|
|
6001
6215
|
/**
|
|
6002
6216
|
* `/agent-model` — switch the model that TALKS, from what the Ollama box actually has (#169).
|
|
6003
6217
|
*
|
|
@@ -6029,7 +6243,23 @@ async function chooseAgentModel() {
|
|
|
6029
6243
|
let models = [];
|
|
6030
6244
|
let failure = null;
|
|
6031
6245
|
let proxied = false;
|
|
6032
|
-
|
|
6246
|
+
// THE KEY, WHEN THIS PROVIDER NEEDS ONE (#40). The list used to be fetched with no Authorization
|
|
6247
|
+
// at all, so api.openai.com answered 401 however right the stored key was. Asked for the key
|
|
6248
|
+
// bound to THIS provider row — the same one the API will send with each turn.
|
|
6249
|
+
const needsKey = am.agentSourceNeedsKey(source);
|
|
6250
|
+
const listHost = url ? hostOf(url) : "";
|
|
6251
|
+
let listKey = "";
|
|
6252
|
+
let keyProblem = false;
|
|
6253
|
+
if (needsKey) {
|
|
6254
|
+
listKey = (await withPendingStatus(`reading the key for ${listHost}`, () => fetchCodingKey(String(source.row?.id ?? "")))).key;
|
|
6255
|
+
if (!listKey) {
|
|
6256
|
+
keyProblem = true;
|
|
6257
|
+
failure = `no API key is stored for ${listHost}`;
|
|
6258
|
+
}
|
|
6259
|
+
}
|
|
6260
|
+
if (failure) {
|
|
6261
|
+
// already decided — no request without the key it needs
|
|
6262
|
+
} else if (!url) {
|
|
6033
6263
|
failure = "no agent model URL is configured for this machine";
|
|
6034
6264
|
} else {
|
|
6035
6265
|
try {
|
|
@@ -6041,9 +6271,10 @@ async function chooseAgentModel() {
|
|
|
6041
6271
|
// terminal that ignored the command. The host is named in the label because on a slow
|
|
6042
6272
|
// one that IS the useful information while you wait.
|
|
6043
6273
|
const r = await withPendingStatus(`asking ${hostOf(url)} for its models`, () =>
|
|
6044
|
-
proxyFetch(url, { signal: AbortSignal.timeout(10000) })
|
|
6274
|
+
proxyFetch(url, { headers: am.agentListHeaders(listKey), signal: AbortSignal.timeout(10000) })
|
|
6045
6275
|
);
|
|
6046
6276
|
if (!r.ok) failure = `HTTP ${r.status}`;
|
|
6277
|
+
if (!r.ok && needsKey && (r.status === 401 || r.status === 403)) keyProblem = true;
|
|
6047
6278
|
else models = am.agentModelsFromBody(source.dialect, await r.json().catch(() => null));
|
|
6048
6279
|
// A 200 with nothing in it is not a working list. Saying "the box answered but listed no
|
|
6049
6280
|
// models" is a different diagnosis from "unreachable", and the user can act on it.
|
|
@@ -6061,11 +6292,18 @@ async function chooseAgentModel() {
|
|
|
6061
6292
|
// printed the diagnosis and stopped, leaving no way forward from the place the problem was
|
|
6062
6293
|
// found. /model had the same dead end before #223, and this is the same remedy: offer the
|
|
6063
6294
|
// way out from where the reader already is.
|
|
6295
|
+
if (keyProblem) console.log(GUTTER + dim(`${listHost} needs an API key — enter one below to list its models.`));
|
|
6296
|
+
// By ACTION, not by position: the key entry is only there when a key is the problem (#40), and
|
|
6297
|
+
// the Ollama entry is new (#41) — indices would mean different things on different days.
|
|
6298
|
+
const options = am.agentRecoveryOptions({ keyProblem, host: listHost });
|
|
6064
6299
|
const go = await pickFromList(
|
|
6065
|
-
|
|
6066
|
-
1
|
|
6300
|
+
options.map((o, i) => (i === 0 ? dim(o.label) : o.label)),
|
|
6301
|
+
keyProblem ? 1 : 0
|
|
6067
6302
|
);
|
|
6068
|
-
|
|
6303
|
+
const action = go >= 0 ? options[go].action : "leave";
|
|
6304
|
+
if (action === "key") await enterAgentProviderKey(source.row, listHost);
|
|
6305
|
+
else if (action === "ollama") await addOllamaAgent();
|
|
6306
|
+
else if (action === "provider") await addCodingProvider("agent");
|
|
6069
6307
|
else console.log("");
|
|
6070
6308
|
return;
|
|
6071
6309
|
}
|
|
@@ -6101,17 +6339,23 @@ async function chooseAgentModel() {
|
|
|
6101
6339
|
// because "which model on the box I am pointed at" and "I want a different provider" are
|
|
6102
6340
|
// different questions, and only the first is answered by the rows above.
|
|
6103
6341
|
labels.push("add an OpenAI-compatible provider for the agent…");
|
|
6342
|
+
// #41, appended AFTER the provider entry so no existing index moves.
|
|
6343
|
+
labels.push("use an Ollama server for the agent…");
|
|
6104
6344
|
const idx = await pickFromList(labels, Math.max(0, rows.findIndex((r) => r.current)));
|
|
6105
6345
|
if (idx < 0) {
|
|
6106
6346
|
console.log(dim(" (cancelled)\n"));
|
|
6107
6347
|
return;
|
|
6108
6348
|
}
|
|
6109
|
-
// labels is now
|
|
6110
|
-
// dereference undefined and take the picker down, so
|
|
6349
|
+
// labels is now TWO LONGER than rows. Reading rows[idx] for an appended entry would
|
|
6350
|
+
// dereference undefined and take the picker down, so they are answered before rows is touched.
|
|
6111
6351
|
if (idx === rows.length) {
|
|
6112
6352
|
await addCodingProvider("agent");
|
|
6113
6353
|
return;
|
|
6114
6354
|
}
|
|
6355
|
+
if (idx === rows.length + 1) {
|
|
6356
|
+
await addOllamaAgent();
|
|
6357
|
+
return;
|
|
6358
|
+
}
|
|
6115
6359
|
const chosen = rows[idx].model;
|
|
6116
6360
|
if (chosen === current) {
|
|
6117
6361
|
console.log(dim(` already using ${current} — nothing changed.\n`));
|
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.699",
|
|
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",
|