@tiens.nguyen/gu-cli 1.0.872 → 1.0.874
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 +8 -6
- package/coding-provider-setup.mjs +11 -6
- package/embed-model-command.mjs +2 -2
- package/gu-repl.mjs +431 -236
- package/package.json +2 -1
- package/skills-copy.mjs +266 -0
- package/skills.mjs +3 -0
package/gu-repl.mjs
CHANGED
|
@@ -543,6 +543,9 @@ const color256 = (n, s) => `\x1b[38;5;${n}m${s}\x1b[0m`;
|
|
|
543
543
|
// WAIT_COLORS_256 colour only when this run was started with `gu --color`. White goes through
|
|
544
544
|
// paint(), so NO_COLOR / TERM=dumb still turn it off.
|
|
545
545
|
const waitPaint = (n, s) => (REPL_COLOR ? color256(n, s) : white(s));
|
|
546
|
+
// The live thought line above it (#245) follows the same switch: white by default, the accent
|
|
547
|
+
// cyan only with `gu --color`.
|
|
548
|
+
const thoughtPaint = (s) => (REPL_COLOR ? accent(s) : white(s));
|
|
546
549
|
const orange = (s) => color256(208, s); // peak-token label (task #84)
|
|
547
550
|
// Compact token count: 950 → "950", 12300 → "12.3k", 1500000 → "1.5M" (task #83).
|
|
548
551
|
const fmtTokens = (n) => {
|
|
@@ -636,7 +639,7 @@ if (cliVerdict.kind === "run") {
|
|
|
636
639
|
" -v, --verbose show RAG index-file activity in the bottom bar (each shard written)\n" +
|
|
637
640
|
" --log log this run in detail (~/.gonext/worker.log). Without it only starts,\n" +
|
|
638
641
|
" stops and errors are logged. Ends when this gu closes.\n" +
|
|
639
|
-
" --color colour the thinking spinner and its playful word
|
|
642
|
+
" --color colour the live thought, the thinking spinner and its playful word.\n" +
|
|
640
643
|
" Without it they are plain white.\n" +
|
|
641
644
|
" --name <text> show <text> instead of the gu logo in the startup banner\n" +
|
|
642
645
|
" (this run only; letters, digits, space - _ are drawn as art)\n" +
|
|
@@ -1038,6 +1041,7 @@ const COMMANDS = [
|
|
|
1038
1041
|
{ name: "/hooks", usage: "/hooks [list|approve|unapprove]", desc: "hooks: commands gu runs on onStart, onAsk, onToolDone, onTurnEnd — list them, approve this project's" },
|
|
1039
1042
|
{ name: "/mcp", usage: "/mcp [list|tools|add|remove|enable|disable|approve|allow|disallow|reconnect]", desc: "MCP servers: see which connect, add one (asks step by step), choose which tools run without asking" },
|
|
1040
1043
|
{ name: "/switch-skill", usage: "/switch-skill [claude|gu]", desc: "whose skills, MCP servers and hooks this folder uses — Claude Code's (.claude) or gu's own (.gonext)" },
|
|
1044
|
+
{ name: "/skills-copy-to-claude", usage: "/skills-copy-to-claude [--dry-run] [name…]", desc: "copy gu's skills (.gonext) into Claude Code's folders (.claude), renamed to Claude's names, with their MCP servers" },
|
|
1041
1045
|
{ name: "/help", desc: "list these commands" },
|
|
1042
1046
|
{ name: "/exit", aliases: ["/quit"], desc: "quit" },
|
|
1043
1047
|
];
|
|
@@ -6232,7 +6236,7 @@ async function pickExtraModels(rest, preChecked = []) {
|
|
|
6232
6236
|
*
|
|
6233
6237
|
* Returns the model id, or null when the user cancelled.
|
|
6234
6238
|
*/
|
|
6235
|
-
async function chooseCodingModelFrom(ps, all, question, current) {
|
|
6239
|
+
async function chooseCodingModelFrom(ps, all, question, current, { back = false } = {}) {
|
|
6236
6240
|
const { models: coders, filtered } = ps.codingModelCandidates(all);
|
|
6237
6241
|
let list = coders;
|
|
6238
6242
|
let showingAll = filtered === 0;
|
|
@@ -6247,7 +6251,8 @@ async function chooseCodingModelFrom(ps, all, question, current) {
|
|
|
6247
6251
|
dim(` ${question} (${PICK_HINT})`) +
|
|
6248
6252
|
(showingAll || !filtered ? "" : dim(` · ${filtered} non-coding models hidden`))
|
|
6249
6253
|
);
|
|
6250
|
-
const idx = await pickFromList(rows, at >= 0 ? at : 0);
|
|
6254
|
+
const idx = await pickFromList(rows, at >= 0 ? at : 0, { back });
|
|
6255
|
+
if (idx === PICK_BACK) return BACK;
|
|
6251
6256
|
if (idx < 0) return null;
|
|
6252
6257
|
if (idx === list.length) {
|
|
6253
6258
|
const typed = (await ask(" model name: ")).trim();
|
|
@@ -6275,11 +6280,12 @@ async function chooseCodingModelFrom(ps, all, question, current) {
|
|
|
6275
6280
|
* stays available for the same reason it does there — a listed model may be unusable and an
|
|
6276
6281
|
* unlisted one may be exactly right.
|
|
6277
6282
|
*/
|
|
6278
|
-
async function chooseFromModelList(all, question, current = 0) {
|
|
6283
|
+
async function chooseFromModelList(all, question, current = 0, { back = false } = {}) {
|
|
6279
6284
|
const rows = [...all, "type a model name…"];
|
|
6280
6285
|
const at = typeof current === "string" ? all.indexOf(current) : Number(current) || 0;
|
|
6281
6286
|
console.log(dim(` ${question} (${PICK_HINT})`));
|
|
6282
|
-
const idx = await pickFromList(rows, at >= 0 ? at : 0);
|
|
6287
|
+
const idx = await pickFromList(rows, at >= 0 ? at : 0, { back });
|
|
6288
|
+
if (idx === PICK_BACK) return BACK;
|
|
6283
6289
|
if (idx < 0) return null;
|
|
6284
6290
|
if (idx === all.length) {
|
|
6285
6291
|
const typed = (await ask(" model name: ")).trim();
|
|
@@ -6288,51 +6294,65 @@ async function chooseFromModelList(all, question, current = 0) {
|
|
|
6288
6294
|
return all[idx];
|
|
6289
6295
|
}
|
|
6290
6296
|
|
|
6291
|
-
|
|
6297
|
+
/**
|
|
6298
|
+
* /model's provider manager. `back` (#242): opened from /model's list, so its first menu offers
|
|
6299
|
+
* "← back" and returns BACK. Inside it, each sub-menu's "← back" returns HERE — re-read, because a
|
|
6300
|
+
* step backed out of saves nothing but the list is cheap to trust fresh.
|
|
6301
|
+
*/
|
|
6302
|
+
async function manageCodingProviders({ back = false } = {}) {
|
|
6292
6303
|
const ps = await import("./coding-provider-setup.mjs");
|
|
6293
|
-
|
|
6294
|
-
|
|
6295
|
-
|
|
6296
|
-
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
return;
|
|
6304
|
-
}
|
|
6304
|
+
let selAt = 0;
|
|
6305
|
+
for (;;) {
|
|
6306
|
+
const settings = await withPendingStatus("reading your providers", fetchWorkerSettings);
|
|
6307
|
+
if (!settings) {
|
|
6308
|
+
console.log(red(" couldn't reach the API to read your current settings.\n"));
|
|
6309
|
+
return;
|
|
6310
|
+
}
|
|
6311
|
+
const rows = ps.existingBackendRows(settings);
|
|
6312
|
+
const providers = ps.manageableProviders(rows);
|
|
6313
|
+
// NOTHING TO MANAGE IS NOT A MENU. Straight to the add flow, exactly as before.
|
|
6314
|
+
if (providers.length === 0) return addServer({ role: "coding", dialect: "openai", back });
|
|
6305
6315
|
|
|
6306
|
-
|
|
6307
|
-
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
|
|
6313
|
-
|
|
6314
|
-
|
|
6315
|
-
|
|
6316
|
-
|
|
6317
|
-
|
|
6318
|
-
|
|
6319
|
-
|
|
6320
|
-
|
|
6321
|
-
|
|
6316
|
+
const labels = providers.map(
|
|
6317
|
+
(p) =>
|
|
6318
|
+
`${p.model || dim("(no model set)")}${dim(` · ${p.host}`)}${p.enabled ? "" : dim(" · off")}`
|
|
6319
|
+
);
|
|
6320
|
+
const ADD = labels.length;
|
|
6321
|
+
labels.push("add another provider — Claude API, OpenAI, Kimi…");
|
|
6322
|
+
console.log(dim(` Providers — pick one to change it, or add another (${PICK_HINT}):`));
|
|
6323
|
+
const idx = await pickFromList(labels, selAt, { back });
|
|
6324
|
+
if (idx === PICK_BACK) return BACK;
|
|
6325
|
+
if (idx < 0) {
|
|
6326
|
+
console.log(dim(" (cancelled)\n"));
|
|
6327
|
+
return;
|
|
6328
|
+
}
|
|
6329
|
+
selAt = idx;
|
|
6330
|
+
if (idx === ADD) {
|
|
6331
|
+
if ((await addServer({ role: "coding", dialect: "openai", back: true })) === BACK) continue;
|
|
6332
|
+
return;
|
|
6333
|
+
}
|
|
6322
6334
|
|
|
6323
|
-
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
|
|
6328
|
-
|
|
6329
|
-
|
|
6330
|
-
|
|
6331
|
-
|
|
6335
|
+
const p = providers[idx];
|
|
6336
|
+
for (;;) {
|
|
6337
|
+
console.log("\n" + GUTTER + dim(`${p.host} · ${p.model || "no model set"}`));
|
|
6338
|
+
const action = await pickFromList(
|
|
6339
|
+
[dim("keep it"), "change the model…", "change the URL…", "remove this provider…"],
|
|
6340
|
+
1,
|
|
6341
|
+
{ back: true }
|
|
6342
|
+
);
|
|
6343
|
+
if (action === PICK_BACK) break; // → the provider list
|
|
6344
|
+
if (action <= 0) {
|
|
6345
|
+
console.log(dim(" (nothing changed)\n"));
|
|
6346
|
+
return;
|
|
6347
|
+
}
|
|
6348
|
+
let r;
|
|
6349
|
+
if (action === 1) r = await changeProviderModel(ps, rows, p);
|
|
6350
|
+
else if (action === 2) r = await changeProviderUrl(ps, rows, p);
|
|
6351
|
+
else r = await removeCodingProvider(ps, rows, p);
|
|
6352
|
+
if (r === BACK) continue; // → this provider's actions
|
|
6353
|
+
return;
|
|
6354
|
+
}
|
|
6332
6355
|
}
|
|
6333
|
-
if (action === 1) await changeProviderModel(ps, rows, p);
|
|
6334
|
-
else if (action === 2) await changeProviderUrl(ps, rows, p);
|
|
6335
|
-
else await removeCodingProvider(ps, rows, p);
|
|
6336
6356
|
}
|
|
6337
6357
|
|
|
6338
6358
|
/** Point a provider at a different model. */
|
|
@@ -6353,9 +6373,14 @@ async function changeProviderModel(ps, rows, p) {
|
|
|
6353
6373
|
const r = await fetchCodingKey(p.id);
|
|
6354
6374
|
return r.keyId && r.keyId !== inUse.id ? { key: "", name: "" } : { key: r.key, name: String(inUse.name ?? "") };
|
|
6355
6375
|
});
|
|
6376
|
+
// AN OLLAMA ROW (#243) lists through Ollama's own /api/tags, the way it was added.
|
|
6377
|
+
const as = p.dialect === "ollama" ? await import("./add-server.mjs") : null;
|
|
6378
|
+
const ollamaTarget = as ? as.serverTarget("ollama", p.url) : null;
|
|
6356
6379
|
const probe = await withPendingStatus(`asking ${p.host} what it has`, async () => {
|
|
6357
6380
|
try {
|
|
6358
|
-
const lr =
|
|
6381
|
+
const lr = ollamaTarget?.ok
|
|
6382
|
+
? as.listRequest(ollamaTarget, own.key)
|
|
6383
|
+
: ps.modelListRequest(p.url, own.key); // the list's own dialect too (Claude's headers, #261)
|
|
6359
6384
|
const res = await proxyFetch(lr.url, { headers: lr.headers, signal: AbortSignal.timeout(10_000) });
|
|
6360
6385
|
if (!res.ok) return { status: res.status };
|
|
6361
6386
|
return { body: await res.json().catch(() => null) };
|
|
@@ -6365,7 +6390,7 @@ async function changeProviderModel(ps, rows, p) {
|
|
|
6365
6390
|
});
|
|
6366
6391
|
let models = [];
|
|
6367
6392
|
if (!probe.status && !probe.err) {
|
|
6368
|
-
const parsed = ps.parseProviderModels(probe.body);
|
|
6393
|
+
const parsed = ollamaTarget?.ok ? as.modelsForRole("coding", "ollama", probe.body) : ps.parseProviderModels(probe.body);
|
|
6369
6394
|
// The CURRENT model stays in the list: after switching to another one it becomes a perfectly
|
|
6370
6395
|
// good extra, and filtering it out here would make it unofferable without a second visit.
|
|
6371
6396
|
models = parsed.models;
|
|
@@ -6384,7 +6409,8 @@ async function changeProviderModel(ps, rows, p) {
|
|
|
6384
6409
|
|
|
6385
6410
|
let model = "";
|
|
6386
6411
|
if (models.length) {
|
|
6387
|
-
model = await chooseCodingModelFrom(ps, models, `Which model should ${p.host} use?`, p.model);
|
|
6412
|
+
model = await chooseCodingModelFrom(ps, models, `Which model should ${p.host} use?`, p.model, { back: true });
|
|
6413
|
+
if (model === BACK) return BACK;
|
|
6388
6414
|
if (model === null) {
|
|
6389
6415
|
console.log(dim(" (nothing changed)\n"));
|
|
6390
6416
|
return;
|
|
@@ -6437,10 +6463,13 @@ async function changeProviderUrl(ps, rows, p) {
|
|
|
6437
6463
|
// THE SAME PRESET LIST AS ADDING (#132), OpenAI-compatible ones only: an Ollama row has another
|
|
6438
6464
|
// kind, and this flow keeps the row's id and kind. "type a URL…" is the default row, so Enter
|
|
6439
6465
|
// still gets the old free-text question.
|
|
6440
|
-
|
|
6466
|
+
// An Ollama row (#243) is offered the Ollama presets (Ollama cloud) and keeps its kind too.
|
|
6467
|
+
const dialect = p.dialect === "ollama" ? "ollama" : "openai";
|
|
6468
|
+
const urlPresets = ps.providerPresets("coding").filter((x) => (x.dialect ?? "openai") === dialect);
|
|
6441
6469
|
const urlRows = [...urlPresets.map((x) => `${x.name}${dim(` · ${x.url}`)}`), "type a URL…"];
|
|
6442
6470
|
console.log("\n" + GUTTER + dim(`Which provider? (${PICK_HINT})`));
|
|
6443
|
-
const upi = await pickFromList(urlRows, urlRows.length - 1);
|
|
6471
|
+
const upi = await pickFromList(urlRows, urlRows.length - 1, { back: true });
|
|
6472
|
+
if (upi === PICK_BACK) return BACK;
|
|
6444
6473
|
if (upi < 0) {
|
|
6445
6474
|
console.log(GUTTER + dim("(nothing changed)\n"));
|
|
6446
6475
|
return;
|
|
@@ -6456,11 +6485,27 @@ async function changeProviderUrl(ps, rows, p) {
|
|
|
6456
6485
|
console.log(GUTTER + dim("(nothing changed)\n"));
|
|
6457
6486
|
return;
|
|
6458
6487
|
}
|
|
6459
|
-
|
|
6460
|
-
if (
|
|
6461
|
-
|
|
6462
|
-
|
|
6463
|
-
|
|
6488
|
+
let r;
|
|
6489
|
+
if (dialect === "ollama") {
|
|
6490
|
+
const as = await import("./add-server.mjs");
|
|
6491
|
+
const t = as.serverTarget("ollama", typed);
|
|
6492
|
+
r = t.ok ? { ok: true, url: t.url, host: t.host } : { ok: false, why: t.why };
|
|
6493
|
+
if (!r.ok) {
|
|
6494
|
+
console.log(GUTTER + dim(`"${typed}" is ${r.why} — try ollama.com, or host:port such as 192.168.1.5:11434`));
|
|
6495
|
+
console.log(GUTTER + dim("(nothing changed)\n"));
|
|
6496
|
+
return;
|
|
6497
|
+
}
|
|
6498
|
+
if (r.url === p.url) {
|
|
6499
|
+
console.log(GUTTER + dim("(nothing changed)\n"));
|
|
6500
|
+
return;
|
|
6501
|
+
}
|
|
6502
|
+
} else {
|
|
6503
|
+
r = ps.normalizeProviderUrl(typed);
|
|
6504
|
+
if (!r.ok) {
|
|
6505
|
+
for (const l of ps.describeUrlProblem(r.why, typed)) console.log(GUTTER + dim(l));
|
|
6506
|
+
console.log(GUTTER + dim("(nothing changed)\n"));
|
|
6507
|
+
return;
|
|
6508
|
+
}
|
|
6464
6509
|
}
|
|
6465
6510
|
const clash = rows.find(
|
|
6466
6511
|
(b) => ps.ensureV1Root(String(b.url ?? "")) === r.url && !ps.manageableProviders([b]).some((x) => x.id === p.id)
|
|
@@ -6569,7 +6614,7 @@ async function removeCodingProvider(ps, rows, p) {
|
|
|
6569
6614
|
* model skip listing and picking (/embed-model's curated list already chose; `plan` with it)
|
|
6570
6615
|
* newKey ask for a key even if the server has one (the stored one was refused)
|
|
6571
6616
|
*/
|
|
6572
|
-
async function addServer({ role = "coding", dialect = "openai", url: presetUrl = "", model: presetModel = "", plan: presetPlan = null, newKey = false } = {}) {
|
|
6617
|
+
async function addServer({ role = "coding", dialect = "openai", url: presetUrl = "", model: presetModel = "", plan: presetPlan = null, newKey = false, back = false } = {}) {
|
|
6573
6618
|
const as = await import("./add-server.mjs");
|
|
6574
6619
|
const ps = await import("./coding-provider-setup.mjs");
|
|
6575
6620
|
const sk = await import("./server-keys.mjs");
|
|
@@ -6591,166 +6636,190 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
|
|
|
6591
6636
|
? ((await fetchAgentPayload([{ role: "user", content: "ping" }]).catch(() => null))?.codingChoices ?? null)
|
|
6592
6637
|
: null;
|
|
6593
6638
|
|
|
6594
|
-
//
|
|
6595
|
-
|
|
6596
|
-
|
|
6597
|
-
|
|
6598
|
-
|
|
6599
|
-
|
|
6600
|
-
|
|
6639
|
+
// STEPS 1–4 ARE ONE LOOP (#242): "← back" on the model list returns to "Which provider?", and
|
|
6640
|
+
// "← back" there returns BACK to the menu that opened this (with `back`). Nothing is saved
|
|
6641
|
+
// before step 5, so going back leaves nothing behind — a key typed on the way is dropped.
|
|
6642
|
+
const providerPicker = !presetUrl && dialect === "openai";
|
|
6643
|
+
const ec = role === "embed" ? await import("./embed-model-command.mjs") : null;
|
|
6644
|
+
let target, host, found, need, key, keyName, typedKey, models, model;
|
|
6645
|
+
for (;;) {
|
|
6646
|
+
// --- 1. the server --------------------------------------------------------------------
|
|
6647
|
+
target = null;
|
|
6648
|
+
// The dialect the URL prompt reads the address in: the caller's, or Ollama's when that row was picked.
|
|
6649
|
+
let askDialect = dialect;
|
|
6650
|
+
if (presetUrl) {
|
|
6651
|
+
target = as.serverTarget(dialect, presetUrl);
|
|
6652
|
+
if (!target.ok) {
|
|
6653
|
+
console.log(red(` "${presetUrl}" is ${target.why} — nothing changed.\n`));
|
|
6654
|
+
return;
|
|
6655
|
+
}
|
|
6656
|
+
} else if (dialect === "openai") {
|
|
6657
|
+
// THE HOSTED PROVIDERS BY NAME (#261): Claude, OpenAI, Kimi — then any other URL. One picker in
|
|
6658
|
+
// the one add flow, so every role that adds a provider offers the same list (the embedder
|
|
6659
|
+
// without Claude, which serves no embeddings).
|
|
6660
|
+
const presets = ps.providerPresets(role);
|
|
6661
|
+
// #244: an Ollama on this machine or the network is a row here, like the hosted ones — the
|
|
6662
|
+
// menus no longer have a separate "use an Ollama server…" entrance. It asks for the address.
|
|
6663
|
+
const OLLAMA_HERE = presets.length;
|
|
6664
|
+
const rowsShown = [
|
|
6665
|
+
...presets.map((p) => `${p.name}${dim(` · ${p.url}`)}`),
|
|
6666
|
+
`Ollama — this machine or your network…${dim(" · e.g. 127.0.0.1:11434")}`,
|
|
6667
|
+
"another URL…",
|
|
6668
|
+
];
|
|
6669
|
+
// A preset names its own dialect (Ollama cloud is "ollama"); the rest are OpenAI-compatible.
|
|
6670
|
+
console.log(dim(` Which provider? (${PICK_HINT})`));
|
|
6671
|
+
const pi = await pickFromList(rowsShown, 0, { back });
|
|
6672
|
+
if (pi === PICK_BACK) return BACK;
|
|
6673
|
+
if (pi < 0) {
|
|
6674
|
+
console.log(dim(" (nothing saved)\n"));
|
|
6675
|
+
return;
|
|
6676
|
+
}
|
|
6677
|
+
if (pi < presets.length) target = as.serverTarget(presets[pi].dialect ?? "openai", presets[pi].url);
|
|
6678
|
+
else if (pi === OLLAMA_HERE) askDialect = "ollama";
|
|
6679
|
+
}
|
|
6680
|
+
if (!target && !presetUrl) {
|
|
6681
|
+
const dflt = as.defaultServerUrl(role, askDialect, settings, existing);
|
|
6682
|
+
console.log(dim(askDialect === "ollama"
|
|
6683
|
+
? " An Ollama server: this machine, a box on your network, or ollama.com (with a key)."
|
|
6684
|
+
: " Any OpenAI-compatible endpoint: OpenRouter, Together, vLLM, LM Studio, …"));
|
|
6685
|
+
// The address goes ABOVE, in full, and Enter's meaning is said out loud (46d1524e).
|
|
6686
|
+
console.log(dim(dflt ? ` Enter uses ${dflt} · Esc cancels\n` : " Esc cancels.\n"));
|
|
6687
|
+
for (let attempt = 0; attempt < 3 && !target; attempt++) {
|
|
6688
|
+
const typed = await ask(" server URL: ");
|
|
6689
|
+
if (typed === null || as.isCancelWord(typed)) break;
|
|
6690
|
+
const raw = typed.trim() || dflt;
|
|
6691
|
+
if (!raw) break;
|
|
6692
|
+
const t = as.serverTarget(askDialect, raw);
|
|
6693
|
+
if (!t.ok) {
|
|
6694
|
+
if (askDialect === "openai") for (const l of ps.describeUrlProblem(t.why, raw)) console.log(GUTTER + dim(l));
|
|
6695
|
+
else console.log(GUTTER + dim(`"${raw}" is ${t.why} — try host:port, e.g. 127.0.0.1:11434`));
|
|
6696
|
+
continue;
|
|
6697
|
+
}
|
|
6698
|
+
// SAY WHAT WE ASSUMED: a scheme we supplied or a path we stripped is a guess, and a wrong
|
|
6699
|
+
// guess shows up later as a timeout that reads like a dead server.
|
|
6700
|
+
if (t.root !== raw && t.url !== raw) console.log(GUTTER + dim(`using ${t.root}`));
|
|
6701
|
+
target = t;
|
|
6702
|
+
}
|
|
6601
6703
|
}
|
|
6602
|
-
|
|
6603
|
-
// THE HOSTED PROVIDERS BY NAME (#261): Claude, OpenAI, Kimi — then any other URL. One picker in
|
|
6604
|
-
// the one add flow, so every role that adds a provider offers the same list (the embedder
|
|
6605
|
-
// without Claude, which serves no embeddings).
|
|
6606
|
-
const presets = ps.providerPresets(role);
|
|
6607
|
-
const rowsShown = [...presets.map((p) => `${p.name}${dim(` · ${p.url}`)}`), "another URL…"];
|
|
6608
|
-
// A preset names its own dialect (Ollama cloud is "ollama"); the rest are OpenAI-compatible.
|
|
6609
|
-
console.log(dim(` Which provider? (${PICK_HINT})`));
|
|
6610
|
-
const pi = await pickFromList(rowsShown, 0);
|
|
6611
|
-
if (pi < 0) {
|
|
6704
|
+
if (!target) {
|
|
6612
6705
|
console.log(dim(" (nothing saved)\n"));
|
|
6613
6706
|
return;
|
|
6614
6707
|
}
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
|
|
6621
|
-
|
|
6622
|
-
//
|
|
6623
|
-
|
|
6624
|
-
|
|
6625
|
-
const typed = await ask(" server URL: ");
|
|
6626
|
-
if (typed === null || as.isCancelWord(typed)) break;
|
|
6627
|
-
const raw = typed.trim() || dflt;
|
|
6628
|
-
if (!raw) break;
|
|
6629
|
-
const t = as.serverTarget(dialect, raw);
|
|
6630
|
-
if (!t.ok) {
|
|
6631
|
-
if (dialect === "openai") for (const l of ps.describeUrlProblem(t.why, raw)) console.log(GUTTER + dim(l));
|
|
6632
|
-
else console.log(GUTTER + dim(`"${raw}" is ${t.why} — try host:port, e.g. 127.0.0.1:11434`));
|
|
6633
|
-
continue;
|
|
6634
|
-
}
|
|
6635
|
-
// SAY WHAT WE ASSUMED: a scheme we supplied or a path we stripped is a guess, and a wrong
|
|
6636
|
-
// guess shows up later as a timeout that reads like a dead server.
|
|
6637
|
-
if (t.root !== raw && t.url !== raw) console.log(GUTTER + dim(`using ${t.root}`));
|
|
6638
|
-
target = t;
|
|
6708
|
+
host = target.host;
|
|
6709
|
+
const refusal = as.roleRefusal(role, target);
|
|
6710
|
+
if (refusal.length) {
|
|
6711
|
+
for (const l of refusal) console.log(GUTTER + yellow(l));
|
|
6712
|
+
console.log("");
|
|
6713
|
+
return;
|
|
6714
|
+
}
|
|
6715
|
+
// Claude as the CODER: prompt caching needs Anthropic's SDK (#262) — offer it once, here.
|
|
6716
|
+
if (target.claude && role === "coding") {
|
|
6717
|
+
await md.ensureClaudeSdk({ log: (l) => console.log(GUTTER + dim(l.trim())), confirm: (q) => askYesNo(` ${q}`, true) });
|
|
6639
6718
|
}
|
|
6640
|
-
}
|
|
6641
|
-
if (!target) {
|
|
6642
|
-
console.log(dim(" (nothing saved)\n"));
|
|
6643
|
-
return;
|
|
6644
|
-
}
|
|
6645
|
-
const host = target.host;
|
|
6646
|
-
const refusal = as.roleRefusal(role, target);
|
|
6647
|
-
if (refusal.length) {
|
|
6648
|
-
for (const l of refusal) console.log(GUTTER + yellow(l));
|
|
6649
|
-
console.log("");
|
|
6650
|
-
return;
|
|
6651
|
-
}
|
|
6652
|
-
// Claude as the CODER: prompt caching needs Anthropic's SDK (#262) — offer it once, here.
|
|
6653
|
-
if (target.claude && role === "coding") {
|
|
6654
|
-
await md.ensureClaudeSdk({ log: (l) => console.log(GUTTER + dim(l.trim())), confirm: (q) => askYesNo(` ${q}`, true) });
|
|
6655
|
-
}
|
|
6656
|
-
|
|
6657
|
-
// --- 2. the server's key --------------------------------------------------------------
|
|
6658
|
-
// AN EXISTING SERVER IS REUSED, never stored twice (two rows for one URL would be two picker
|
|
6659
|
-
// entries identical in everything a person can see) — its keys come with it.
|
|
6660
|
-
const found = ps.providerRowForUrl(existing, target.url);
|
|
6661
|
-
// THE ROW CAP BEFORE ANY QUESTION about a new server: exceeding it is a silent 200 with no row,
|
|
6662
|
-
// and asking for a key and a model first would be the worst order to discover that in.
|
|
6663
|
-
if (!found && existing.length >= ps.MAX_BACKENDS) {
|
|
6664
|
-
console.log(yellow(` ${ps.MAX_BACKENDS} servers is the limit — remove one in /model → manage providers first.\n`));
|
|
6665
|
-
return;
|
|
6666
|
-
}
|
|
6667
|
-
const need = as.keyRequirement(target);
|
|
6668
|
-
let key = "";
|
|
6669
|
-
let keyName = "";
|
|
6670
|
-
let typedKey = false;
|
|
6671
|
-
if (found && !newKey) {
|
|
6672
|
-
// A KEY BELONGS TO THE SERVER: one already saved is used, whichever model it was saved for.
|
|
6673
|
-
const keys = await withPendingStatus(`reading the keys for ${host}`, () => fetchServerKeys(found.id));
|
|
6674
|
-
const folderKeyId = role === "agent" ? sessionAgentKeyId : role === "embed" ? sessionEmbedKeyId : sessionCodingKeyId;
|
|
6675
|
-
const inUse = sk.keyInUse(keys ?? [], found.id, folderKeyId);
|
|
6676
|
-
if (inUse && inUse.backendId === found.id) {
|
|
6677
|
-
key = (await fetchCodingKey(found.id)).key;
|
|
6678
|
-
if (key) console.log(GUTTER + dim(`using the key "${inUse.name}" already saved for ${host}.`));
|
|
6679
|
-
}
|
|
6680
|
-
}
|
|
6681
|
-
const askKey = async () => {
|
|
6682
|
-
const t = await askServerKey({ host, optional: need === "optional" });
|
|
6683
|
-
key = t.secret;
|
|
6684
|
-
keyName = t.name;
|
|
6685
|
-
typedKey = Boolean(t.secret);
|
|
6686
|
-
return t.secret || need !== "required";
|
|
6687
|
-
};
|
|
6688
|
-
if (!key && (need !== "none" || newKey)) {
|
|
6689
|
-
if (need === "required") console.log(GUTTER + dim(`${host} needs an API key.`));
|
|
6690
|
-
if (!(await askKey())) return;
|
|
6691
|
-
}
|
|
6692
6719
|
|
|
6693
|
-
|
|
6694
|
-
|
|
6695
|
-
|
|
6696
|
-
|
|
6697
|
-
|
|
6698
|
-
|
|
6699
|
-
|
|
6700
|
-
|
|
6701
|
-
|
|
6702
|
-
|
|
6703
|
-
|
|
6720
|
+
// --- 2. the server's key --------------------------------------------------------------
|
|
6721
|
+
// AN EXISTING SERVER IS REUSED, never stored twice (two rows for one URL would be two picker
|
|
6722
|
+
// entries identical in everything a person can see) — its keys come with it.
|
|
6723
|
+
found = ps.providerRowForUrl(existing, target.url);
|
|
6724
|
+
// THE ROW CAP BEFORE ANY QUESTION about a new server: exceeding it is a silent 200 with no row,
|
|
6725
|
+
// and asking for a key and a model first would be the worst order to discover that in.
|
|
6726
|
+
if (!found && existing.length >= ps.MAX_BACKENDS) {
|
|
6727
|
+
console.log(yellow(` ${ps.MAX_BACKENDS} servers is the limit — remove one in /model → manage providers first.\n`));
|
|
6728
|
+
return;
|
|
6729
|
+
}
|
|
6730
|
+
need = as.keyRequirement(target);
|
|
6731
|
+
key = "";
|
|
6732
|
+
keyName = "";
|
|
6733
|
+
typedKey = false;
|
|
6734
|
+
if (found && !newKey) {
|
|
6735
|
+
// A KEY BELONGS TO THE SERVER: one already saved is used, whichever model it was saved for.
|
|
6736
|
+
const keys = await withPendingStatus(`reading the keys for ${host}`, () => fetchServerKeys(found.id));
|
|
6737
|
+
const folderKeyId = role === "agent" ? sessionAgentKeyId : role === "embed" ? sessionEmbedKeyId : sessionCodingKeyId;
|
|
6738
|
+
const inUse = sk.keyInUse(keys ?? [], found.id, folderKeyId);
|
|
6739
|
+
if (inUse && inUse.backendId === found.id) {
|
|
6740
|
+
key = (await fetchCodingKey(found.id)).key;
|
|
6741
|
+
if (key) console.log(GUTTER + dim(`using the key "${inUse.name}" already saved for ${host}.`));
|
|
6742
|
+
}
|
|
6743
|
+
}
|
|
6744
|
+
const askKey = async () => {
|
|
6745
|
+
const t = await askServerKey({ host, optional: need === "optional" });
|
|
6746
|
+
key = t.secret;
|
|
6747
|
+
keyName = t.name;
|
|
6748
|
+
typedKey = Boolean(t.secret);
|
|
6749
|
+
return t.secret || need !== "required";
|
|
6750
|
+
};
|
|
6751
|
+
if (!key && (need !== "none" || newKey)) {
|
|
6752
|
+
if (need === "required") console.log(GUTTER + dim(`${host} needs an API key.`));
|
|
6753
|
+
if (!(await askKey())) return;
|
|
6754
|
+
}
|
|
6755
|
+
|
|
6756
|
+
// --- 3. its models, listed with that key ----------------------------------------------
|
|
6757
|
+
models = [];
|
|
6758
|
+
if (!presetModel) {
|
|
6759
|
+
const list = () =>
|
|
6760
|
+
withPendingStatus(`asking ${host} what it has`, async () => {
|
|
6761
|
+
const req = as.listRequest(target, key);
|
|
6762
|
+
try {
|
|
6763
|
+
const res = await proxyFetch(req.url, { headers: req.headers, signal: AbortSignal.timeout(12_000) });
|
|
6764
|
+
return res.ok ? { body: await res.json().catch(() => null) } : { status: res.status };
|
|
6765
|
+
} catch (err) {
|
|
6766
|
+
return { err };
|
|
6767
|
+
}
|
|
6768
|
+
});
|
|
6769
|
+
let probe = await list();
|
|
6770
|
+
// THE STORED KEY WAS REFUSED — ask for the right one once, rather than saving nothing.
|
|
6771
|
+
if ((probe.status === 401 || probe.status === 403) && !typedKey && key) {
|
|
6772
|
+
console.log(yellow(` ${host} refused the stored key (HTTP ${probe.status}) — enter the right one.`));
|
|
6773
|
+
if (!(await askKey()) || !key) return;
|
|
6774
|
+
probe = await list();
|
|
6775
|
+
}
|
|
6776
|
+
if (probe.status || probe.err) {
|
|
6777
|
+
const d = ps.describeProbeFailure(probe);
|
|
6778
|
+
for (const l of d.lines) console.log(GUTTER + (d.fatal ? red(l) : yellow(l)));
|
|
6779
|
+
// FATAL = THE KEY WAS REFUSED: storing a credential the server already rejected is storing
|
|
6780
|
+
// a problem for later. Anything else is survivable — the model id can be typed.
|
|
6781
|
+
if (d.fatal) {
|
|
6782
|
+
console.log(GUTTER + dim("nothing was saved — check the key and try again.\n"));
|
|
6783
|
+
return;
|
|
6704
6784
|
}
|
|
6705
|
-
|
|
6706
|
-
|
|
6707
|
-
|
|
6708
|
-
|
|
6709
|
-
|
|
6710
|
-
|
|
6711
|
-
|
|
6712
|
-
|
|
6713
|
-
if (probe.status || probe.err) {
|
|
6714
|
-
const d = ps.describeProbeFailure(probe);
|
|
6715
|
-
for (const l of d.lines) console.log(GUTTER + (d.fatal ? red(l) : yellow(l)));
|
|
6716
|
-
// FATAL = THE KEY WAS REFUSED: storing a credential the server already rejected is storing
|
|
6717
|
-
// a problem for later. Anything else is survivable — the model id can be typed.
|
|
6718
|
-
if (d.fatal) {
|
|
6719
|
-
console.log(GUTTER + dim("nothing was saved — check the key and try again.\n"));
|
|
6720
|
-
return;
|
|
6785
|
+
console.log(GUTTER + dim("carrying on — you can type the model id yourself."));
|
|
6786
|
+
} else {
|
|
6787
|
+
const parsed = as.modelsForRole(role, target.dialect, probe.body);
|
|
6788
|
+
models = parsed.models;
|
|
6789
|
+
for (const l of ps.describeModelProblem(parsed.why, { host, total: parsed.total })) console.log(GUTTER + dim(l));
|
|
6790
|
+
const rn = ps.retiredNote(parsed.retired, host);
|
|
6791
|
+
if (rn) console.log(GUTTER + dim(rn));
|
|
6792
|
+
if (models.length) console.log(GUTTER + dim(`${host} offers ${models.length} model${models.length === 1 ? "" : "s"}`));
|
|
6721
6793
|
}
|
|
6722
|
-
console.log(GUTTER + dim("carrying on — you can type the model id yourself."));
|
|
6723
|
-
} else {
|
|
6724
|
-
const parsed = as.modelsForRole(role, target.dialect, probe.body);
|
|
6725
|
-
models = parsed.models;
|
|
6726
|
-
for (const l of ps.describeModelProblem(parsed.why, { host, total: parsed.total })) console.log(GUTTER + dim(l));
|
|
6727
|
-
const rn = ps.retiredNote(parsed.retired, host);
|
|
6728
|
-
if (rn) console.log(GUTTER + dim(rn));
|
|
6729
|
-
if (models.length) console.log(GUTTER + dim(`${host} offers ${models.length} model${models.length === 1 ? "" : "s"}`));
|
|
6730
6794
|
}
|
|
6731
|
-
}
|
|
6732
6795
|
|
|
6733
|
-
|
|
6734
|
-
|
|
6735
|
-
|
|
6736
|
-
|
|
6737
|
-
|
|
6738
|
-
|
|
6739
|
-
|
|
6740
|
-
|
|
6741
|
-
|
|
6742
|
-
|
|
6743
|
-
|
|
6744
|
-
|
|
6796
|
+
// --- 4. the model ---------------------------------------------------------------------
|
|
6797
|
+
model = presetModel;
|
|
6798
|
+
if (!model) {
|
|
6799
|
+
if (models.length) {
|
|
6800
|
+
const am = await import("./agent-model-command.mjs");
|
|
6801
|
+
const current = role === "agent" ? am.currentAgentModel(settings) : role === "embed" ? ec.currentEmbedModel(settings) : sessionCodingLabel;
|
|
6802
|
+
const canBack = { back: providerPicker || back };
|
|
6803
|
+
model = R.filterCoders
|
|
6804
|
+
? await chooseCodingModelFrom(ps, models, R.question, 0, canBack)
|
|
6805
|
+
: await chooseFromModelList(models, R.question, Math.max(0, models.indexOf(current)), canBack);
|
|
6806
|
+
if (model === BACK) {
|
|
6807
|
+
if (providerPicker) continue; // → Which provider?
|
|
6808
|
+
return BACK;
|
|
6809
|
+
}
|
|
6810
|
+
if (model === null) {
|
|
6811
|
+
console.log(dim(" (cancelled — nothing was saved)\n"));
|
|
6812
|
+
return;
|
|
6813
|
+
}
|
|
6814
|
+
} else {
|
|
6815
|
+
model = String((await ask(" model name: ")) ?? "").trim();
|
|
6816
|
+
}
|
|
6817
|
+
if (!model) {
|
|
6818
|
+
console.log(GUTTER + dim("(cancelled — nothing was saved)\n"));
|
|
6745
6819
|
return;
|
|
6746
6820
|
}
|
|
6747
|
-
} else {
|
|
6748
|
-
model = String((await ask(" model name: ")) ?? "").trim();
|
|
6749
|
-
}
|
|
6750
|
-
if (!model) {
|
|
6751
|
-
console.log(GUTTER + dim("(cancelled — nothing was saved)\n"));
|
|
6752
|
-
return;
|
|
6753
6821
|
}
|
|
6822
|
+
break;
|
|
6754
6823
|
}
|
|
6755
6824
|
// THE EMBEDDER'S RECOGNITION WALL: a different model invalidates every index, so the projects
|
|
6756
6825
|
// that stop working are named BEFORE anything is saved. The same model is a pure server move.
|
|
@@ -7023,8 +7092,8 @@ async function chooseModel() {
|
|
|
7023
7092
|
[dim("leave it"), "add or change a provider — Claude API, OpenAI, Kimi…"],
|
|
7024
7093
|
1
|
|
7025
7094
|
);
|
|
7026
|
-
if (go === 1
|
|
7027
|
-
|
|
7095
|
+
if (go === 1 && (await manageCodingProviders({ back: true })) === BACK) return chooseModel();
|
|
7096
|
+
if (go !== 1) console.log("");
|
|
7028
7097
|
return;
|
|
7029
7098
|
}
|
|
7030
7099
|
// Shown even with ONE model: "only one is available" often means the wrong server, and the
|
|
@@ -7037,8 +7106,8 @@ async function chooseModel() {
|
|
|
7037
7106
|
[dim("keep it"), "add or change a provider — Claude API, OpenAI, Kimi…"],
|
|
7038
7107
|
0
|
|
7039
7108
|
);
|
|
7040
|
-
if (go === 1
|
|
7041
|
-
|
|
7109
|
+
if (go === 1 && (await manageCodingProviders({ back: true })) === BACK) return chooseModel();
|
|
7110
|
+
if (go !== 1) console.log("");
|
|
7042
7111
|
return;
|
|
7043
7112
|
}
|
|
7044
7113
|
const active = sessionCodingModel || def;
|
|
@@ -7073,7 +7142,7 @@ async function chooseModel() {
|
|
|
7073
7142
|
}
|
|
7074
7143
|
// The one appended entry sits past the last model.
|
|
7075
7144
|
if (idx >= choices.length) {
|
|
7076
|
-
await manageCodingProviders();
|
|
7145
|
+
if ((await manageCodingProviders({ back: true })) === BACK) return chooseModel();
|
|
7077
7146
|
return;
|
|
7078
7147
|
}
|
|
7079
7148
|
await applyChosenCoder(choices[idx], { def, tag: showTag ? tags[idx] : "" });
|
|
@@ -7619,6 +7688,91 @@ async function runSwitchSkillCommand(line) {
|
|
|
7619
7688
|
if (barPinned()) paintBar();
|
|
7620
7689
|
}
|
|
7621
7690
|
|
|
7691
|
+
/**
|
|
7692
|
+
* /skills-copy-to-claude [--dry-run] [name…] — gu's skills into Claude Code's folders (#241):
|
|
7693
|
+
* ~/.gonext/skills → ~/.claude/skills and ./.gonext/skills → ./.claude/skills, "gu-" names become
|
|
7694
|
+
* "claude-", and a skill's MCP server goes into ~/.claude.json (skills-copy.mjs). Shows the plan,
|
|
7695
|
+
* then asks; a Claude skill that differs is kept unless the user picks "replace" (backed up).
|
|
7696
|
+
*/
|
|
7697
|
+
async function runSkillsCopyCommand(line) {
|
|
7698
|
+
const sc = await import("./skills-copy.mjs");
|
|
7699
|
+
const words = line.trim().split(/\s+/).slice(1);
|
|
7700
|
+
const dryRun = words.includes("--dry-run");
|
|
7701
|
+
const names = words.filter((w) => !w.startsWith("--"));
|
|
7702
|
+
const home = homedir();
|
|
7703
|
+
const cwd = resolve(process.cwd());
|
|
7704
|
+
const tilde = (p) => (p.startsWith(home) ? "~" + p.slice(home.length) : p.startsWith(cwd) ? "." + p.slice(cwd.length) : p);
|
|
7705
|
+
const say = (l) => console.log(GUTTER + l);
|
|
7706
|
+
console.log("");
|
|
7707
|
+
let plan;
|
|
7708
|
+
try {
|
|
7709
|
+
plan = sc.planSkillsCopy({ cwd, home, names });
|
|
7710
|
+
} catch (err) {
|
|
7711
|
+
say(red(`couldn't read gu's skills — ${err.message}\n`));
|
|
7712
|
+
return;
|
|
7713
|
+
}
|
|
7714
|
+
for (const w of plan.warnings) say(yellow(w));
|
|
7715
|
+
if (plan.missing.length) say(yellow(`gu has no skill named ${plan.missing.join(", ")}`));
|
|
7716
|
+
if (!plan.items.length) {
|
|
7717
|
+
say(dim("gu has no skills to copy — ~/.gonext/skills and ./.gonext/skills have none.\n"));
|
|
7718
|
+
return;
|
|
7719
|
+
}
|
|
7720
|
+
const word = { new: accent("new"), same: dim("already there"), differs: yellow("Claude's differs") };
|
|
7721
|
+
say(bold("Copy gu's skills to Claude Code"));
|
|
7722
|
+
for (const it of plan.items) {
|
|
7723
|
+
const renamed = it.newName !== it.name ? dim(` (was ${it.name})`) : "";
|
|
7724
|
+
say(` ${it.newName}${renamed} ${word[it.status]}`);
|
|
7725
|
+
say(dim(` → ${tilde(it.to)}`));
|
|
7726
|
+
for (const s of it.servers) {
|
|
7727
|
+
say(dim(` MCP server ${s.name} → ~/.claude.json${s.scope === "local" ? " (this folder)" : ""} `) + word[s.status]);
|
|
7728
|
+
}
|
|
7729
|
+
if (it.guLines.length) {
|
|
7730
|
+
say(yellow(` SKILL.md line${it.guLines.length === 1 ? "" : "s"} ${it.guLines.join(", ")} still name${it.guLines.length === 1 ? "s" : ""} ~/.gonext — check the copy`));
|
|
7731
|
+
}
|
|
7732
|
+
}
|
|
7733
|
+
const todo = plan.items.filter((i) => i.status !== "same" || i.servers.some((s) => s.status !== "same"));
|
|
7734
|
+
const differs = plan.items.filter((i) => i.status === "differs" || i.servers.some((s) => s.status === "differs"));
|
|
7735
|
+
if (!todo.length) {
|
|
7736
|
+
say(dim("\n Nothing to copy — Claude Code already has all of them.\n"));
|
|
7737
|
+
return;
|
|
7738
|
+
}
|
|
7739
|
+
if (dryRun) {
|
|
7740
|
+
say(dim("\n --dry-run: nothing was written.\n"));
|
|
7741
|
+
return;
|
|
7742
|
+
}
|
|
7743
|
+
const fresh = todo.length - differs.length;
|
|
7744
|
+
const options = differs.length
|
|
7745
|
+
? [
|
|
7746
|
+
...(fresh ? [`Copy the ${fresh} new — keep Claude's own where it differs`] : []),
|
|
7747
|
+
`Copy all — replace Claude's ${differs.length} (old one${differs.length === 1 ? "" : "s"} backed up)`,
|
|
7748
|
+
"Cancel",
|
|
7749
|
+
]
|
|
7750
|
+
: [`Copy ${todo.length === 1 ? "it" : `all ${todo.length}`}`, "Cancel"];
|
|
7751
|
+
console.log(dim(`\n ${PICK_HINT}:`));
|
|
7752
|
+
const idx = await pickFromList(options, 0);
|
|
7753
|
+
if (idx < 0 || options[idx] === "Cancel") {
|
|
7754
|
+
console.log(dim(" (cancelled — nothing copied)\n"));
|
|
7755
|
+
paintBar();
|
|
7756
|
+
return;
|
|
7757
|
+
}
|
|
7758
|
+
const replace = options[idx].startsWith("Copy all — replace");
|
|
7759
|
+
const r = sc.applySkillsCopy(plan, { replace, cwd, home });
|
|
7760
|
+
const verb = { copied: "copied", replaced: "replaced", kept: "kept Claude's", already: "already there" };
|
|
7761
|
+
for (const d of r.done) {
|
|
7762
|
+
const mark = d.action === "copied" || d.action === "replaced" ? accent("✓") : dim("·");
|
|
7763
|
+
say(`${mark} ${d.newName} ${dim(verb[d.action] + (d.action === "copied" || d.action === "replaced" ? " → " + tilde(d.to) : ""))}`);
|
|
7764
|
+
if (d.backup) say(dim(` Claude's old one: ${tilde(d.backup)}`));
|
|
7765
|
+
}
|
|
7766
|
+
for (const s of r.servers) {
|
|
7767
|
+
if (s.action === "copied" || s.action === "replaced") say(`${accent("✓")} MCP server ${s.name} ${dim(verb[s.action] + " → ~/.claude.json")}`);
|
|
7768
|
+
}
|
|
7769
|
+
for (const e of r.errors) say(red(`✗ ${e}`));
|
|
7770
|
+
say(dim(" A new Claude Code session loads them."));
|
|
7771
|
+
say(dim(" In gu, /switch-skill claude makes this folder use them too."));
|
|
7772
|
+
console.log("");
|
|
7773
|
+
if (barPinned()) paintBar();
|
|
7774
|
+
}
|
|
7775
|
+
|
|
7622
7776
|
/**
|
|
7623
7777
|
* /undo — put back what a stopped turn changed (task #302): the last stopped turn's run, or the
|
|
7624
7778
|
* run named. Shows exactly what it will do and asks first — a file edited by hand since the turn
|
|
@@ -8594,13 +8748,14 @@ async function confirmEmbedSwitch(ec, current, chosen) {
|
|
|
8594
8748
|
|
|
8595
8749
|
// WHICH EMBEDDER, ASKED ONCE. The model sub-flow of /embed-model: the curated picker, then the
|
|
8596
8750
|
// shared recognition step. Returns { changed, chosen, plan }.
|
|
8597
|
-
async function pickEmbedModel(ec, current) {
|
|
8751
|
+
async function pickEmbedModel(ec, current, { back = false } = {}) {
|
|
8598
8752
|
const rows = ec.embedChoiceRows(current);
|
|
8599
8753
|
console.log(dim(` Choose the embedder (${PICK_HINT}):`));
|
|
8600
8754
|
const labels = rows.map(
|
|
8601
8755
|
(r) => `${r.model}${r.dims ? dim(` · ${r.dims}d`) : ""}${dim(` · ${r.note}`)}` + (r.current ? accent(" (current)") : "")
|
|
8602
8756
|
);
|
|
8603
|
-
const idx = await pickFromList(labels, Math.max(0, rows.findIndex((r) => r.current)));
|
|
8757
|
+
const idx = await pickFromList(labels, Math.max(0, rows.findIndex((r) => r.current)), { back });
|
|
8758
|
+
if (idx === PICK_BACK) return BACK;
|
|
8604
8759
|
if (idx < 0) {
|
|
8605
8760
|
console.log(dim(" (cancelled)\n"));
|
|
8606
8761
|
return { changed: false, chosen: "", plan: null };
|
|
@@ -8654,14 +8809,19 @@ async function chooseEmbedModel() {
|
|
|
8654
8809
|
// EVERY ARM IS THE ONE ADD-SERVER SCREEN (task #245) — the same URL → key → models → save as
|
|
8655
8810
|
// /model and /agent-model. The curated arm has already chosen the model (and passed the
|
|
8656
8811
|
// recognition wall), so it only asks WHERE; the other two list what the server has.
|
|
8812
|
+
// "← back" in any arm returns to this menu (#242).
|
|
8657
8813
|
if (action === "model") {
|
|
8658
|
-
const
|
|
8814
|
+
const picked = await pickEmbedModel(ec, current, { back: true });
|
|
8815
|
+
if (picked === BACK) return chooseEmbedModel();
|
|
8816
|
+
const { changed, chosen, plan } = picked;
|
|
8659
8817
|
if (!changed) return;
|
|
8660
8818
|
await addServer({ role: "embed", dialect: "ollama", model: chosen, plan });
|
|
8661
8819
|
return;
|
|
8662
8820
|
}
|
|
8663
|
-
if (action === "
|
|
8664
|
-
|
|
8821
|
+
if (action === "provider") {
|
|
8822
|
+
if ((await addServer({ role: "embed", dialect: "openai", back: true })) === BACK) return chooseEmbedModel();
|
|
8823
|
+
return;
|
|
8824
|
+
}
|
|
8665
8825
|
|
|
8666
8826
|
if (action === "key") {
|
|
8667
8827
|
// THE SAME KEY MANAGER as /model's ctrl+k and /agent-model (task #245): this server's keys
|
|
@@ -8674,7 +8834,7 @@ async function chooseEmbedModel() {
|
|
|
8674
8834
|
await addServer({ role: "embed", dialect: /\/v1\/?$/i.test(currentUrl) ? "openai" : "ollama", url: currentUrl, model: current, newKey: true });
|
|
8675
8835
|
return;
|
|
8676
8836
|
} else {
|
|
8677
|
-
await manageServerKeys({ role: "embed", backendId: found.id, host: host || currentUrl, model: current });
|
|
8837
|
+
if ((await manageServerKeys({ role: "embed", backendId: found.id, host: host || currentUrl, model: current, back: true })) === BACK) return chooseEmbedModel();
|
|
8678
8838
|
}
|
|
8679
8839
|
// The pre-#245 account-wide pick would shadow what was just chosen; the server decides now.
|
|
8680
8840
|
if (String(settings.ragEmbedKeyId ?? "").trim()) await saveEmbedSettings({ ragEmbedKeyId: "" });
|
|
@@ -8758,7 +8918,7 @@ async function enterAgentProviderKey(row, host) {
|
|
|
8758
8918
|
/**
|
|
8759
8919
|
* #44: switch the agent to a provider already saved on this machine, or add a new one.
|
|
8760
8920
|
*/
|
|
8761
|
-
async function chooseAgentProvider() {
|
|
8921
|
+
async function chooseAgentProvider({ back = false } = {}) {
|
|
8762
8922
|
const am = await import("./agent-model-command.mjs");
|
|
8763
8923
|
const settings = await withPendingStatus("reading your providers", fetchWorkerSettings);
|
|
8764
8924
|
if (!settings) {
|
|
@@ -8766,17 +8926,25 @@ async function chooseAgentProvider() {
|
|
|
8766
8926
|
return;
|
|
8767
8927
|
}
|
|
8768
8928
|
const choices = am.agentProviderChoices(settings);
|
|
8769
|
-
if (choices.length === 1) return addServer({ role: "agent", dialect: "openai" });
|
|
8770
|
-
|
|
8771
|
-
|
|
8772
|
-
|
|
8773
|
-
|
|
8774
|
-
return;
|
|
8929
|
+
if (choices.length === 1) return addServer({ role: "agent", dialect: "openai", back });
|
|
8930
|
+
let at = 0;
|
|
8931
|
+
for (;;) {
|
|
8932
|
+
console.log(dim(` Which OpenAI-compatible provider should the agent use? (${PICK_HINT})`));
|
|
8933
|
+
const idx = await pickFromList(choices.map((c) => c.label), at, { back });
|
|
8934
|
+
if (idx === PICK_BACK) return BACK;
|
|
8935
|
+
if (idx < 0) {
|
|
8936
|
+
console.log(dim(" (cancelled)\n"));
|
|
8937
|
+
return;
|
|
8938
|
+
}
|
|
8939
|
+
at = idx;
|
|
8940
|
+
const pick = choices[idx];
|
|
8941
|
+
// "← back" inside either screen returns to this list (#242).
|
|
8942
|
+
const r = pick.action === "add"
|
|
8943
|
+
? await addServer({ role: "agent", dialect: "openai", back: true })
|
|
8944
|
+
// A server already set up: the same screen, URL known — its key is reused, its models listed.
|
|
8945
|
+
: await addServer({ role: "agent", dialect: String(pick.row.kind ?? "") === "ollama" ? "ollama" : "openai", url: String(pick.row.url ?? ""), back: true });
|
|
8946
|
+
if (r !== BACK) return;
|
|
8775
8947
|
}
|
|
8776
|
-
const pick = choices[idx];
|
|
8777
|
-
if (pick.action === "add") return addServer({ role: "agent", dialect: "openai" });
|
|
8778
|
-
// A server already set up: the same screen, URL known — its key is reused, its models listed.
|
|
8779
|
-
await addServer({ role: "agent", dialect: String(pick.row.kind ?? "") === "ollama" ? "ollama" : "openai", url: String(pick.row.url ?? "") });
|
|
8780
8948
|
}
|
|
8781
8949
|
|
|
8782
8950
|
/**
|
|
@@ -8869,7 +9037,6 @@ async function chooseAgentModel() {
|
|
|
8869
9037
|
);
|
|
8870
9038
|
const action = go >= 0 ? options[go].action : "leave";
|
|
8871
9039
|
if (action === "key") await enterAgentProviderKey(source.row, listHost);
|
|
8872
|
-
else if (action === "ollama") await addServer({ role: "agent", dialect: "ollama" });
|
|
8873
9040
|
else if (action === "provider") await addServer({ role: "agent", dialect: "openai" });
|
|
8874
9041
|
else console.log("");
|
|
8875
9042
|
return;
|
|
@@ -8886,13 +9053,17 @@ async function chooseAgentModel() {
|
|
|
8886
9053
|
console.log(dim(" (cancelled)\n"));
|
|
8887
9054
|
return;
|
|
8888
9055
|
}
|
|
8889
|
-
|
|
8890
|
-
if (firstAction === "provider")
|
|
9056
|
+
// "← back" in any of these returns to this menu (#242) — read again, as on a first open.
|
|
9057
|
+
if (firstAction === "provider") {
|
|
9058
|
+
if ((await chooseAgentProvider({ back: true })) === BACK) return chooseAgentModel();
|
|
9059
|
+
return;
|
|
9060
|
+
}
|
|
8891
9061
|
if (firstAction === "key") {
|
|
8892
9062
|
// Task #245: one key manager for every role — this server's keys, this folder's AGENT pick.
|
|
8893
9063
|
const rowId = String(source.row?.id ?? "").trim() || String(source.row?.kind ?? "").trim();
|
|
8894
9064
|
if (!rowId) return enterAgentProviderKey(source.row, listHost);
|
|
8895
|
-
await manageServerKeys({ role: "agent", backendId: rowId, host: listHost, model: am.currentAgentModel(settings) });
|
|
9065
|
+
const r = await manageServerKeys({ role: "agent", backendId: rowId, host: listHost, model: am.currentAgentModel(settings), back: true });
|
|
9066
|
+
if (r === BACK) return chooseAgentModel();
|
|
8896
9067
|
return;
|
|
8897
9068
|
}
|
|
8898
9069
|
|
|
@@ -8924,7 +9095,8 @@ async function chooseAgentModel() {
|
|
|
8924
9095
|
});
|
|
8925
9096
|
// No other entries here any more (#44): the ways to change the provider are on the menu above,
|
|
8926
9097
|
// where they can be seen, rather than after every model.
|
|
8927
|
-
const idx = await pickFromList(labels, Math.max(0, rows.findIndex((r) => r.current)));
|
|
9098
|
+
const idx = await pickFromList(labels, Math.max(0, rows.findIndex((r) => r.current)), { back: true });
|
|
9099
|
+
if (idx === PICK_BACK) return chooseAgentModel();
|
|
8928
9100
|
if (idx < 0) {
|
|
8929
9101
|
console.log(dim(" (cancelled)\n"));
|
|
8930
9102
|
return;
|
|
@@ -10217,7 +10389,7 @@ async function replaceCodingKey() {
|
|
|
10217
10389
|
* `role` is "coding" | "agent" | "embed"; `backendId` the server's row id; `host` its name.
|
|
10218
10390
|
* Returns the id now in use for this folder's role, or "" when nothing changed.
|
|
10219
10391
|
*/
|
|
10220
|
-
async function manageServerKeys({ role = "coding", backendId, host, model = "" }) {
|
|
10392
|
+
async function manageServerKeys({ role = "coding", backendId, host, model = "", back = false }) {
|
|
10221
10393
|
const sk = await import("./server-keys.mjs");
|
|
10222
10394
|
const { label: roleLabel } = sk.roleOf(role);
|
|
10223
10395
|
const folderKeyId = role === "agent" ? sessionAgentKeyId : role === "embed" ? sessionEmbedKeyId : sessionCodingKeyId;
|
|
@@ -10240,8 +10412,9 @@ async function manageServerKeys({ role = "coding", backendId, host, model = "" }
|
|
|
10240
10412
|
rows.push(dim("add a new key…"));
|
|
10241
10413
|
console.log("\n" + GUTTER + dim(`Keys for ${host} — ${folderLabel()}'s ${roleLabel}${model ? ` (${model})` : ""}\n`));
|
|
10242
10414
|
console.log(GUTTER + dim("Enter chooses · ctrl+d deletes the highlighted key\n"));
|
|
10243
|
-
const picked = await pickFromList(rows, Math.max(0, keys.findIndex((k) => k.id === inUse?.id)), { allowDelete: true });
|
|
10415
|
+
const picked = await pickFromList(rows, Math.max(0, keys.findIndex((k) => k.id === inUse?.id)), { allowDelete: true, back });
|
|
10244
10416
|
const idx = picked?.index ?? -1;
|
|
10417
|
+
if (idx === PICK_BACK) return BACK;
|
|
10245
10418
|
if (idx < 0) {
|
|
10246
10419
|
console.log(GUTTER + dim("(cancelled — nothing changed)\n"));
|
|
10247
10420
|
paintBar();
|
|
@@ -11100,7 +11273,24 @@ function pickManyFromList(items, preChecked = [], { max = 0 } = {}) {
|
|
|
11100
11273
|
});
|
|
11101
11274
|
}
|
|
11102
11275
|
|
|
11276
|
+
/**
|
|
11277
|
+
* "← back" (#242): with `opts.back`, a last row that returns to the menu this one was opened from.
|
|
11278
|
+
* Choosing it resolves PICK_BACK (-2) — still < 0, so a caller that does not handle it reads it as a
|
|
11279
|
+
* cancel and no existing picker can change meaning. Esc still cancels the whole command.
|
|
11280
|
+
*/
|
|
11281
|
+
const PICK_BACK = -2;
|
|
11282
|
+
const BACK_ROW = "← back";
|
|
11283
|
+
/** What a flow opened from another menu returns when the user chose "← back" at its first question. */
|
|
11284
|
+
const BACK = Symbol("back");
|
|
11285
|
+
|
|
11103
11286
|
function pickFromList(items, initialSel = 0, opts = {}) {
|
|
11287
|
+
if (opts.back && process.stdin.isTTY && items.length) {
|
|
11288
|
+
const n = items.length;
|
|
11289
|
+
return pickFromList([...items, dim(BACK_ROW)], initialSel, { ...opts, back: false }).then((v) => {
|
|
11290
|
+
if (v && typeof v === "object") return v.index === n ? { ...v, index: PICK_BACK } : v;
|
|
11291
|
+
return v === n ? PICK_BACK : v;
|
|
11292
|
+
});
|
|
11293
|
+
}
|
|
11104
11294
|
return new Promise((resolve) => {
|
|
11105
11295
|
if (!process.stdin.isTTY || items.length === 0) {
|
|
11106
11296
|
resolve(-1);
|
|
@@ -12311,7 +12501,8 @@ async function runAgentTurn(history, { attachJobId = "", ownJob = false } = {})
|
|
|
12311
12501
|
// Only worth probing when the mouse is captured — thoughtRow exists for hover/click
|
|
12312
12502
|
// hit-testing, and with `/hover` off nothing reads it (and nothing would consume the reply).
|
|
12313
12503
|
if (hoverMouse && process.stdin.isTTY && now - lastDsrAt > 400) { lastDsrAt = now; ui("\x1b[6n"); }
|
|
12314
|
-
// Line 1:
|
|
12504
|
+
// Line 1 (#245: white by default; the cyan below only with `gu --color`, like the spinner row):
|
|
12505
|
+
// cyan ● + cyan phase/seconds — the logo's colour, which is what this REPL uses for
|
|
12315
12506
|
// WHAT IS LIVE RIGHT NOW: the ">> " prompt is cyan while you are typing and goes dim once
|
|
12316
12507
|
// submitted, and a selection is drawn on the same cyan. Green is left to mean SUCCEEDED (the
|
|
12317
12508
|
// ✓ bullets), which is what it means everywhere else here — a running step is not a finished
|
|
@@ -12330,7 +12521,7 @@ async function runAgentTurn(history, { attachJobId = "", ownJob = false } = {})
|
|
|
12330
12521
|
const clipRow = (r) => clipVisible(r, Math.max(20, process.stdout.columns || 80) - 1);
|
|
12331
12522
|
const contentRows = [];
|
|
12332
12523
|
for (let i = 0; i < LEAD_ROWS; i++) contentRows.push(""); // the separator — see LEAD_ROWS
|
|
12333
|
-
contentRows.push(clipRow(`${markLine(
|
|
12524
|
+
contentRows.push(clipRow(`${markLine(thoughtPaint(BULLET))}${hovering ? hoverThought(label) : thoughtPaint(label)}`));
|
|
12334
12525
|
for (const dr of detailRows) contentRows.push(clipRow(dim(dr)));
|
|
12335
12526
|
for (const wl of expLines) contentRows.push(clipRow(GUTTER + dim(wl)));
|
|
12336
12527
|
for (let i = 0; i < WORD_GAP; i++) contentRows.push(""); // see WORD_GAP
|
|
@@ -14000,7 +14191,7 @@ async function main() {
|
|
|
14000
14191
|
red("gu: this machine has no models set up yet.") +
|
|
14001
14192
|
dim(
|
|
14002
14193
|
"\n Nothing can run until it knows which Ollama server to use." +
|
|
14003
|
-
"\n Start `gu`, then `/agent-model` → use
|
|
14194
|
+
"\n Start `gu`, then `/agent-model` → use another provider → Ollama, `/embed-model` for RAG," +
|
|
14004
14195
|
"\n and `/model` → add a provider → another URL… for the coding model." +
|
|
14005
14196
|
`\n The agent server needs a ${REQUIRED_AGENT_PREFIX} model (e.g. ${REQUIRED_AGENT_MODEL})` +
|
|
14006
14197
|
`\n and any ONE of these embedders: ${ACCEPTED_EMBED_MODELS.map((m) => m.replace(/:latest$/, "")).join(", ")};` +
|
|
@@ -14535,6 +14726,10 @@ async function main() {
|
|
|
14535
14726
|
await runSwitchSkillCommand(line);
|
|
14536
14727
|
continue;
|
|
14537
14728
|
}
|
|
14729
|
+
if (line === "/skills-copy-to-claude" || line.startsWith("/skills-copy-to-claude ")) {
|
|
14730
|
+
await runSkillsCopyCommand(line);
|
|
14731
|
+
continue;
|
|
14732
|
+
}
|
|
14538
14733
|
if (line === "/hooks" || line.startsWith("/hooks ")) {
|
|
14539
14734
|
const { runHooksCommand } = await import("./hooks-command.mjs");
|
|
14540
14735
|
const style = { ok: accent, warn: yellow, dim };
|