@tiens.nguyen/gu-cli 1.0.721 → 1.0.722

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gu-cli",
3
- "version": "1.0.721",
3
+ "version": "1.0.722",
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",
@@ -73,7 +73,6 @@
73
73
  "first-run-steps.mjs",
74
74
  "proxy-config.mjs",
75
75
  "agent-model-command.mjs",
76
- "ollama-host-snippets.mjs",
77
76
  "ollama-setup.mjs",
78
77
  "s3-setup.mjs",
79
78
  "doctor-check.mjs",
@@ -84,6 +83,8 @@
84
83
  "proxy-dispatcher.mjs",
85
84
  "model-doctor.mjs",
86
85
  "coding-key-prompt.mjs",
86
+ "server-keys.mjs",
87
+ "add-server.mjs",
87
88
  "powershell-alias.mjs",
88
89
  "install-path.mjs",
89
90
  "gonext_probe_agent.py",
@@ -0,0 +1,97 @@
1
+ /**
2
+ * A KEY BELONGS TO A SERVER URL, NOT TO A MODEL (task #245) — the terminal's decisions, pure.
3
+ *
4
+ * The rules the user set:
5
+ * · a server URL owns its keys (many, named) and its models (at most MAX_MODELS_PER_SERVER);
6
+ * · the first key chosen for a server becomes its DEFAULT for every later terminal / project;
7
+ * · picking another key in a folder applies to that folder only, PER ROLE (coding/agent/embed);
8
+ * · the switch picker loads the current server's keys only;
9
+ * · deleting the first key entered for a server warns — its model list was loaded with it.
10
+ *
11
+ * The API owns resolution (resolveKeyForRole in api/src/app.ts); this module only decides what
12
+ * the terminal shows and asks, so /model, /agent-model and /embed-model cannot drift apart.
13
+ */
14
+
15
+ /** The three roles, the folder-choice field each one's pick lives in, and how to say it. */
16
+ export const KEY_ROLES = {
17
+ coding: { field: "codingKeyId", label: "coding model" },
18
+ agent: { field: "agentKeyId", label: "agent model" },
19
+ embed: { field: "embedKeyId", label: "embedding model" },
20
+ };
21
+
22
+ export function roleOf(role) {
23
+ return KEY_ROLES[role] ?? KEY_ROLES.coding;
24
+ }
25
+
26
+ /**
27
+ * The key a folder is actually on, as the API will resolve it — for the "(in use)" mark and the
28
+ * bar's tail. Mirrors resolveCodingApiKey: the folder's pick (one of THIS server's keys) → the
29
+ * server's default → its first key → any of its keys → a key tied to no server.
30
+ * `keys` is GET /coding-keys?backendId=… (this server's keys, then unbound ones).
31
+ */
32
+ export function keyInUse(keys, backendId, folderKeyId = "") {
33
+ const list = Array.isArray(keys) ? keys : [];
34
+ const own = list.filter((k) => k && k.backendId === backendId);
35
+ const loose = list.filter((k) => k && !k.backendId);
36
+ return (
37
+ (folderKeyId && own.find((k) => k.id === folderKeyId)) ||
38
+ own.find((k) => k.default) ||
39
+ own.find((k) => k.first) ||
40
+ own[0] ||
41
+ loose[0] ||
42
+ null
43
+ );
44
+ }
45
+
46
+ /** What a picker row says about a key besides its name and tail. Order: most specific first. */
47
+ export function keyTags(k, { folderKeyId = "", inUseId = "" } = {}) {
48
+ const tags = [];
49
+ if (k.id === folderKeyId) tags.push("this folder");
50
+ else if (k.id === inUseId) tags.push("in use");
51
+ if (k.default) tags.push("default");
52
+ if (k.first) tags.push("first key");
53
+ if (!k.backendId) tags.push("not tied to a server yet");
54
+ return tags;
55
+ }
56
+
57
+ /**
58
+ * After a key is picked: WHERE does the choice apply? The first key chosen for a server already
59
+ * became its default (the API stamps it), so this is only asked when the pick differs from the
60
+ * server's default. Returns the rows to offer, in order; `scope` is what each one means.
61
+ */
62
+ export function scopeChoices({ host = "the server", role = "coding" } = {}) {
63
+ return [
64
+ { scope: "folder", label: `just this folder's ${roleOf(role).label}` },
65
+ { scope: "default", label: `make it the default for ${host} — every folder that has not picked its own` },
66
+ ];
67
+ }
68
+
69
+ /**
70
+ * The warnings before a key is deleted, from DELETE /coding-keys/:id?dryRun=1. Irreversible, so
71
+ * everything that changes is said first. The FIRST key gets its own line, as asked: "if later
72
+ * user delete that key we need to warn the user that it was the first key that user input for
73
+ * that server url".
74
+ */
75
+ export function deleteWarnings(preview, { host = "this server", isFolderPick = false, roleLabel = "", lastOfServer = false } = {}) {
76
+ const lines = [];
77
+ // No preview (an older API, or it could not be reached) still gets the warnings the terminal
78
+ // can work out itself — this folder's pick, the server's last key.
79
+ preview = preview && typeof preview === "object" ? preview : {};
80
+ if (preview.wasFirst) {
81
+ lines.push(`This was the FIRST key you entered for ${host} — its model list was loaded with it.`);
82
+ }
83
+ if (preview.wasDefault) {
84
+ lines.push(`It is ${host}'s default key — folders without their own pick fall back to another key of ${host}, or none.`);
85
+ }
86
+ if (isFolderPick) {
87
+ lines.push(`This folder uses it${roleLabel ? ` for the ${roleLabel}` : ""} — it will fall back to ${host}'s default.`);
88
+ }
89
+ if (lastOfServer) {
90
+ lines.push(`It is the only key stored for ${host} — every model there will have none until you add another.`);
91
+ }
92
+ const others = Number(preview.folders ?? 0) - (isFolderPick ? 1 : 0);
93
+ if (others > 0) {
94
+ lines.push(`${others} other folder${others === 1 ? "" : "s"} picked it${Array.isArray(preview.roles) && preview.roles.length ? ` (${preview.roles.join(", ")})` : ""}.`);
95
+ }
96
+ return lines;
97
+ }
@@ -1,36 +0,0 @@
1
- /**
2
- * "Is that an Ollama SERVER?" — the input check behind /embed-model's server prompt (task #169).
3
- *
4
- * WHY A SEPARATE FILE OF ONE FUNCTION. The embed picker asks for a URL and its retry line is
5
- * `"<typed>" is <why> — try host:port, e.g. 127.0.0.1:11434`, so the reason HAS to be a phrase
6
- * that finishes that sentence. Keeping the check next to its words is what stops a future edit
7
- * from returning a full sentence and printing "is The URL must not...".
8
- *
9
- * WHAT "OLLAMA-SHAPED" MEANS HERE. The python worker takes the saved root and appends
10
- * /v1/embeddings, then /api/embed, then /api/embeddings itself (gonext_agent_chat._embed).
11
- * Anything already carrying a path (/v1, /api/tags, someone pasting a dashboard link) would be
12
- * saved as-is and every suffix would 404 — so a path is a reason to ask AGAIN, not to strip
13
- * silently. Stripping hides the mistake; the retry teaches the shape.
14
- *
15
- * The vocabulary mirrors agent-model-command.ollamaAgentUrl ("not a URL", "not http(s)",
16
- * "no host") so the agent and embed prompts read the same when they fail the same way.
17
- *
18
- * Returns { ok: true } or { ok: false, why }.
19
- */
20
- export function checkOllamaHostInput(url) {
21
- const v = String(url ?? "").trim();
22
- if (!v) return { ok: false, why: "empty" };
23
- let u;
24
- try {
25
- u = new URL(v);
26
- } catch {
27
- return { ok: false, why: "not a URL" };
28
- }
29
- if (u.protocol !== "http:" && u.protocol !== "https:") return { ok: false, why: "not http(s)" };
30
- if (!u.hostname) return { ok: false, why: "no host" };
31
- if (u.username || u.password) return { ok: false, why: "carrying credentials — keys go in the key prompt" };
32
- const path = u.pathname.replace(/\/+$/, "");
33
- if (path) return { ok: false, why: `carrying "${path}" — I need just the server root` };
34
- if (u.search || u.hash) return { ok: false, why: "carrying a query — I need just the server root" };
35
- return { ok: true };
36
- }