@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.
- package/README.md +52 -0
- package/agent-model-command.mjs +259 -0
- package/agent-model-label.mjs +159 -0
- package/clear-state.mjs +149 -0
- package/client-expert-api.mjs +736 -0
- package/client-expert-run.mjs +892 -0
- package/client-expert-setup.mjs +616 -0
- package/coding-choice-tags.mjs +69 -0
- package/coding-key-prompt.mjs +229 -0
- package/coding-provider-setup.mjs +808 -0
- package/completed-flush.mjs +105 -0
- package/daemon-control.mjs +462 -0
- package/device-login.mjs +212 -0
- package/doctor-check.mjs +239 -0
- package/embed-model-command.mjs +157 -0
- package/first-run-steps.mjs +171 -0
- package/gonext_agent_chat.py +12299 -0
- package/gonext_mlx_embed.py +155 -0
- package/gonext_probe_agent.py +93 -0
- package/gonext_transcribe.py +130 -0
- package/gu-cli.mjs +4930 -0
- package/gu-repl.mjs +10326 -0
- package/job-pools.mjs +89 -0
- package/model-doctor.mjs +1494 -0
- package/node-version.mjs +40 -0
- package/ollama-setup.mjs +832 -0
- package/package.json +100 -0
- package/platform-tools.mjs +520 -0
- package/poll-errors.mjs +141 -0
- package/proxy-command.mjs +165 -0
- package/proxy-config.mjs +255 -0
- package/proxy-dispatcher.mjs +132 -0
- package/proxy-selftest.mjs +234 -0
- package/proxy-store.mjs +69 -0
- package/rag-job-config.mjs +59 -0
- package/rag-selftest.mjs +215 -0
- package/s3-setup.mjs +85 -0
- package/terminal-copy.mjs +248 -0
- package/terminal-hover.mjs +153 -0
- package/terminal-layout.mjs +2507 -0
- package/terminal-viewport.mjs +602 -0
- package/thinking_words.txt +1003 -0
- 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
|
+
}
|