@tiens.nguyen/gu-cli 1.0.686

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.
Files changed (43) hide show
  1. package/README.md +52 -0
  2. package/agent-model-command.mjs +259 -0
  3. package/agent-model-label.mjs +159 -0
  4. package/clear-state.mjs +149 -0
  5. package/client-expert-api.mjs +736 -0
  6. package/client-expert-run.mjs +892 -0
  7. package/client-expert-setup.mjs +616 -0
  8. package/coding-choice-tags.mjs +69 -0
  9. package/coding-key-prompt.mjs +229 -0
  10. package/coding-provider-setup.mjs +808 -0
  11. package/completed-flush.mjs +105 -0
  12. package/daemon-control.mjs +462 -0
  13. package/device-login.mjs +212 -0
  14. package/doctor-check.mjs +239 -0
  15. package/embed-model-command.mjs +157 -0
  16. package/first-run-steps.mjs +171 -0
  17. package/gonext_agent_chat.py +12299 -0
  18. package/gonext_mlx_embed.py +155 -0
  19. package/gonext_probe_agent.py +93 -0
  20. package/gonext_transcribe.py +130 -0
  21. package/gu-cli.mjs +4930 -0
  22. package/gu-repl.mjs +10326 -0
  23. package/job-pools.mjs +89 -0
  24. package/model-doctor.mjs +1494 -0
  25. package/node-version.mjs +40 -0
  26. package/ollama-setup.mjs +832 -0
  27. package/package.json +100 -0
  28. package/platform-tools.mjs +520 -0
  29. package/poll-errors.mjs +141 -0
  30. package/proxy-command.mjs +165 -0
  31. package/proxy-config.mjs +255 -0
  32. package/proxy-dispatcher.mjs +132 -0
  33. package/proxy-selftest.mjs +234 -0
  34. package/proxy-store.mjs +69 -0
  35. package/rag-job-config.mjs +59 -0
  36. package/rag-selftest.mjs +215 -0
  37. package/s3-setup.mjs +85 -0
  38. package/terminal-copy.mjs +248 -0
  39. package/terminal-hover.mjs +153 -0
  40. package/terminal-layout.mjs +2507 -0
  41. package/terminal-viewport.mjs +602 -0
  42. package/thinking_words.txt +1003 -0
  43. package/version-check.mjs +72 -0
@@ -0,0 +1,229 @@
1
+ // The startup Kimi-K3 (cloud coding model) API-key prompt (task #127). Extracted from the
2
+ // REPL so it's importable + testable with a mock `ask`/`save` — the REPL bin runs main() on
3
+ // import, so the logic can't live there and be tested directly.
4
+ //
5
+ // When the resolved agent CODE model is an OpenAI-compatible cloud coder with NO key stored,
6
+ // the API payload carries codingKeyMissing (+ codingKeyModel/codingKeyUrl). Here we ask the
7
+ // user for the key (masked, via the injected askSecret) and save it. If they skip, the state
8
+ // stays "waiting" SERVER-SIDE (coder configured, no key), so this fires again on the next
9
+ // `gu` start until the key is set. Until then the agent falls back to the chat model for
10
+ // coding (the coder is omitted from the job payload), so the terminal still works.
11
+
12
+ const identity = (s) => s;
13
+
14
+ /**
15
+ * The line that tells someone the key goes HERE (task #191).
16
+ *
17
+ * Reported as "its confused": the prompt was a bare `kimi-k3 API key:` and nothing else. It
18
+ * never said the key is typed or pasted at that point, never said how to finish, and — because
19
+ * askSecret mutes readline's echo so a secret can't reach the scrollback (task #127) — typing
20
+ * produced no visible change at that spot either. A prompt that does not react is read as a
21
+ * hang, not as a question.
22
+ *
23
+ * ONE STRING, TWO PROMPTS. The key is asked for in two places — at startup when none is stored,
24
+ * and on Ctrl+K to replace one — and they were two hand-written copies of the same idea. A hint
25
+ * added to one of them would have left the other exactly as confusing.
26
+ */
27
+ export const SECRET_KEY_HINT = "type or paste the key, then press Enter — it stays masked";
28
+
29
+ /**
30
+ * Hosts that serve an OpenAI-compatible API but authenticate NOTHING.
31
+ *
32
+ * The API marks a coder "key missing" whenever it is an OpenAI-compatible URL with no stored
33
+ * credential — which is right for a cloud provider and wrong for an Ollama. Ollama ignores the
34
+ * Authorization header entirely, so there is no key to enter: the prompt asks for something
35
+ * that does not exist and, worse, reappears on every start because it can never be satisfied.
36
+ *
37
+ * Reported live: a Client Expert machine whose coder is gemma4-quadro-49k on
38
+ * ollama1.gomarsic.cc being asked, at every launch, for that model's "API key".
39
+ *
40
+ * Matched on the HOST, not a substring: `evil.com/?x=gomarsic.cc` and
41
+ * `gomarsic.cc.attacker.net` must not be treated as ours — being wrong here would suppress a
42
+ * genuine prompt and leave someone's coder quietly unusable.
43
+ */
44
+ export function isKeylessCodingHost(url) {
45
+ let host;
46
+ try {
47
+ host = new URL(String(url ?? "")).hostname.toLowerCase();
48
+ } catch {
49
+ return false;
50
+ }
51
+ if (host === "127.0.0.1" || host === "localhost" || host === "::1" || host === "[::1]") return true;
52
+ return host === "gomarsic.cc" || host.endsWith(".gomarsic.cc");
53
+ }
54
+
55
+ /**
56
+ * @param payload the /agent-payload `payload` object (reads codingKeyMissing/codingKeyModel).
57
+ * @param deps.askSecret async (prompt) => string — masked input; "" means the user skipped.
58
+ * @param deps.saveCodingKey async (key) => boolean — persist the key (POST /coding-key).
59
+ * @param deps.log (line) => void — printer.
60
+ * @param deps.colors optional { dim, cyan, red } — identity in tests.
61
+ * @returns { asked, saved, model, url } — asked=false when there was no missing key.
62
+ * model/url are present whenever the coder became usable, so the caller can fix the
63
+ * already-printed banner and the bottom bar (task #190).
64
+ */
65
+ export async function promptCodingKeyIfMissing(payload, { askSecret, saveCodingKey, log, colors = {} }) {
66
+ const dim = colors.dim ?? identity;
67
+ const cyan = colors.cyan ?? identity;
68
+ const red = colors.red ?? identity;
69
+
70
+ if (!payload?.codingKeyMissing) return { asked: false, saved: false };
71
+ // Hoisted above the keyless branch: every path that makes the coder usable reports the model
72
+ // back (see the return shape) so the caller can correct a banner it already printed.
73
+ const model = String(payload.codingKeyModel || "the coding model").trim();
74
+ // A keyless host has no key to ask for — but NOT asking is only half the fix. The API omits
75
+ // the coder from the job payload while it believes a key is missing, so the agent silently
76
+ // falls back to the chat model and the coder the user chose is never used at all.
77
+ //
78
+ // So store the placeholder the request actually needs. Ollama ignores the Authorization
79
+ // header, while an OpenAI client refuses to send an empty key — "local" satisfies both, and
80
+ // is what the hosted API already uses for a local agent model. Once stored, the coder is
81
+ // included and this never runs again.
82
+ if (isKeylessCodingHost(payload.codingKeyUrl)) {
83
+ const ok = await saveCodingKey("local").catch(() => false);
84
+ return { asked: false, saved: Boolean(ok), keyless: true, model, url: payload.codingKeyUrl };
85
+ }
86
+
87
+ log(dim("\ngonext: your agent coding model (") + cyan(model) + dim(") needs an API key."));
88
+ log(dim(" Until it's set, coding falls back to your local model. Enter the key now,"));
89
+ log(dim(" or press Enter to skip (I'll ask again next time).\n"));
90
+
91
+ log(dim(` ${SECRET_KEY_HINT}`));
92
+ const key = await askSecret(` ${model} API key: `);
93
+ if (!key) {
94
+ log(dim(" ⏳ waiting for your ") + cyan(model) + dim(" API key — I'll ask again next time.\n"));
95
+ return { asked: true, saved: false };
96
+ }
97
+ const ok = await saveCodingKey(key);
98
+ log(
99
+ ok
100
+ ? cyan(` ✓ saved — ${model} is now your coding model.\n`)
101
+ : red(" couldn't save the key — try again next start, or set it in web Settings → Agent.\n")
102
+ );
103
+ return { asked: true, saved: ok, model, url: payload.codingKeyUrl };
104
+ }
105
+
106
+ /**
107
+ * What the startup banner's `coder` row should read (task #190).
108
+ *
109
+ * The banner is painted from the startup payload BEFORE promptCodingKeyIfMissing runs, and the
110
+ * API omits codingModelId entirely while it believes the key is missing. So the row used to
111
+ * print "—" and then, three lines later, the prompt said "kimi-k3 is now your coding model" —
112
+ * a header contradicted by the very next thing on screen.
113
+ *
114
+ * There is a coder either way; what differs is whether it can be USED yet. Name it, and say so.
115
+ */
116
+ export function codingBannerLabel(payload, { dim = identity, override = "" } = {}) {
117
+ const p = payload ?? {};
118
+ // A `/model` PICK WINS OVER THE ACCOUNT DEFAULT, because it is what the next turn will
119
+ // actually use. The payload carries the account's coder; the override is this folder's,
120
+ // restored from its session file at startup. Reading only the payload put two different
121
+ // coders on one screen — banner "gemma3:27b", bar "gemma4-quadro-49k:latest" — with the BAR
122
+ // right, which is the worse way round: the header is what you read first.
123
+ //
124
+ // Only possible since the pick started surviving an exit; before that the override was always
125
+ // empty at startup and the two could not disagree.
126
+ const pick = String(override ?? "").trim();
127
+ if (pick) return pick;
128
+ if (p.codingModelId) return String(p.codingModelId);
129
+ const name = String(p.codingKeyMissing ? p.codingKeyModel || "" : "").trim();
130
+ if (!name) return dim("—");
131
+ // A keyless host is never asked for anything — the prompt stores the placeholder itself and
132
+ // the coder is live moments later, so "(needs a key)" would be a lie with a shelf life of
133
+ // about one line.
134
+ if (isKeylessCodingHost(p.codingKeyUrl)) return name;
135
+ return name + dim(" (needs a key)");
136
+ }
137
+
138
+ /**
139
+ * What the worker should log when the key a turn asked for was NOT the key it got (#222, #223).
140
+ *
141
+ * Returns "" when nothing needs saying.
142
+ *
143
+ * WHY THIS IS A FUNCTION AND NOT AN INLINE TEMPLATE. There are three outcomes and they need
144
+ * three different sentences, because they need three different actions from the reader:
145
+ *
146
+ * · the key was DELETED since this folder picked it → pick one again
147
+ * · the key belongs to ANOTHER PROVIDER and was refused → add a key for THIS provider
148
+ * · nothing eligible existed at all → the turn has no coder; it fell back to the chat model
149
+ *
150
+ * Saying "is gone" for the second sends someone hunting for a key that is sitting in their list,
151
+ * perfectly intact. Reporting a fallback "to " with nothing after it, for the third, reads as a
152
+ * bug in the message rather than a fact about the turn.
153
+ *
154
+ * IT MUST NOT BE SILENT in any of the three. A turn that quietly runs on a different key than the
155
+ * one chosen is a bill with no explanation, which is the whole reason the id is recorded per job.
156
+ */
157
+ export function codingKeyFallbackLine(kj, jobId = "") {
158
+ if (!kj?.fellBack) return "";
159
+ const asked = String(kj.requestedKeyId ?? "") || "?";
160
+ const used = String(kj.keyId ?? "");
161
+ const why =
162
+ kj.reason === "other-provider" ? "is for a different coding provider" : "is gone";
163
+ const to = used
164
+ ? `falling back to ${used}`
165
+ : "no key for this provider — the coder will fall back to the chat model";
166
+ return `[gu-worker]${jobId ? ` job ${jobId}:` : ""} coding key ${asked} ${why} — ${to}`;
167
+ }
168
+
169
+ /**
170
+ * SHOULD STARTUP WARN THAT NOBODY CHOSE A KEY? — the decision, extracted so it can be TESTED.
171
+ *
172
+ * Asked for after the bug it guards against: "can we have a test tool to make sure this issue
173
+ * will not happen again … in case later i accidentally modify your code of this fix".
174
+ *
175
+ * The reported failure was a folder running an OpenAI coder while silently using the FIRST key on
176
+ * the account — the old ollama.com one — because nothing had ever chosen a key there and no
177
+ * screen said so. Guarding that with a regex over gu-repl.mjs would pin the WORDING, not the
178
+ * conduct: reword the line and the test fails for nothing; delete the call and leave a similar
179
+ * string behind and it passes while the warning is gone. So the rule lives here, in a function
180
+ * that can be called with arguments and checked.
181
+ *
182
+ * Returns { warn, using, why } — `using` is the key that will be used, `why` names the case:
183
+ * "" nothing to say
184
+ * "none-chosen" several keys stored, this folder picked none, the first one wins by ordering
185
+ *
186
+ * Silent when the coder needs NO key, when nothing is stored (a different prompt handles that),
187
+ * when a choice HAS been made, and when only one key exists — with one there is nothing to
188
+ * choose, and a warning on a correct setup is how warnings stop being read.
189
+ */
190
+ export function unchosenKeyWarning({ codingKind = "", selectedKeyId = "", keys = null } = {}) {
191
+ const quiet = { warn: false, using: null, why: "" };
192
+ if (codingKind !== "openai") return quiet; // Ollama and local MLX need no credential
193
+ if (!Array.isArray(keys) || keys.length < 2) return quiet;
194
+ if (String(selectedKeyId ?? "").trim()) return quiet;
195
+ return { warn: true, using: keys[0], why: "none-chosen" };
196
+ }
197
+
198
+ /**
199
+ * WHICH CODER IS THIS FOLDER ACTUALLY ON? — the account default is not the answer.
200
+ *
201
+ * REPORTED: the bar's "key not chosen" mark only appeared after running /model. The startup gate
202
+ * read `payload.codingKind`, and a payload fetched with NO override describes the ACCOUNT DEFAULT
203
+ * — on the reporting machine that was kimi-k3 at ollama.com, kind "ollama", which needs no key —
204
+ * while the folder was pointed at gpt-5, kind "openai". So the check answered a question about a
205
+ * coder the folder was not using, said "nothing to warn about", and stayed silent. /model looked
206
+ * right because it re-probes WITH the override.
207
+ *
208
+ * The folder's pick is in `sessionCodingModel`, and `codingChoices` already carries the kind of
209
+ * each — the startup path validates the pick against that same list a few lines earlier. So the
210
+ * answer needs no extra request, only the right question.
211
+ *
212
+ * Matched by id OR by bare model name, mirroring that validation: a session saved before ids were
213
+ * qualified ("<kind>::<model>", task #123) stores the bare form and is still a valid pick.
214
+ */
215
+ export function effectiveCoderKind({
216
+ sessionCodingModel = "",
217
+ choices = null,
218
+ defaultKind = "",
219
+ } = {}) {
220
+ const want = String(sessionCodingModel ?? "").trim();
221
+ const list = Array.isArray(choices) ? choices : [];
222
+ if (want) {
223
+ const hit = list.find((c) => c?.id === want || c?.model === want);
224
+ // A pick that matches nothing means the list could not be read, or the pick is stale and is
225
+ // about to be cleared by the caller — either way the account default is the honest fallback.
226
+ if (hit) return String(hit.kind ?? "");
227
+ }
228
+ return String(defaultKind ?? "");
229
+ }