@tiens.nguyen/gu-cli 1.0.695 → 1.0.697
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/coding-choice-tags.mjs +33 -0
- package/coding-key-prompt.mjs +51 -8
- package/gu-cli.mjs +3 -17
- package/gu-repl.mjs +50 -15
- package/package.json +1 -1
package/coding-choice-tags.mjs
CHANGED
|
@@ -67,3 +67,36 @@ export function codingChoiceTags(choices) {
|
|
|
67
67
|
});
|
|
68
68
|
return { tags, show: new Set(tags).size > 1 };
|
|
69
69
|
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* WHERE AN OLLAMA CODER RUNS — "ollama.com", "127.0.0.1", "ollama1.gomarsic.cc" — or "" for any
|
|
73
|
+
* other kind.
|
|
74
|
+
*
|
|
75
|
+
* Reported: "/model ✓ coding model → kimi-k3 (ollama)". The tag was the API's provider label, the
|
|
76
|
+
* first word of the host, so https://ollama.com read "ollama" — the same word as the KIND — and a
|
|
77
|
+
* paid cloud model looked exactly like one on this machine. Whether it is local or cloud decides
|
|
78
|
+
* whether it costs money and needs a key, so it is shown as a prefix wherever the coder is named.
|
|
79
|
+
*
|
|
80
|
+
* The Ollama default port (11434) is left off; any other port is kept, because two local servers on
|
|
81
|
+
* different ports are different servers.
|
|
82
|
+
*/
|
|
83
|
+
export function ollamaWhere(kind, url) {
|
|
84
|
+
if (kind !== "ollama") return "";
|
|
85
|
+
let u;
|
|
86
|
+
try {
|
|
87
|
+
u = new URL(String(url ?? ""));
|
|
88
|
+
} catch {
|
|
89
|
+
return "";
|
|
90
|
+
}
|
|
91
|
+
const host = u.hostname.toLowerCase().replace(/\.$/, "").replace(/^\[|\]$/g, "");
|
|
92
|
+
if (host === "ollama.com" || host.endsWith(".ollama.com")) return "ollama.com";
|
|
93
|
+
const local = host === "localhost" || host === "127.0.0.1" || host === "::1";
|
|
94
|
+
const port = u.port && u.port !== "11434" ? `:${u.port}` : "";
|
|
95
|
+
return (local ? "127.0.0.1" : host) + port;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The coder's name as shown: "ollama.com/kimi-k3" for an Ollama coder, the bare name otherwise. */
|
|
99
|
+
export function coderDisplayName(name, where) {
|
|
100
|
+
const n = String(name ?? "").trim();
|
|
101
|
+
return where && n ? `${where}/${n}` : n;
|
|
102
|
+
}
|
package/coding-key-prompt.mjs
CHANGED
|
@@ -52,6 +52,34 @@ export function isKeylessCodingHost(url) {
|
|
|
52
52
|
return host === "gomarsic.cc" || host.endsWith(".gomarsic.cc");
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* TRUE only for the Ollama CLOUD (https://ollama.com, or a subdomain of it). Mirrors
|
|
57
|
+
* isOllamaCloudUrl in api/src/store/settingsTypes.ts, web/src/lib/coderChoices.mjs and
|
|
58
|
+
* _isOllamaCloudUrl in gu-cli.mjs — keep them in step.
|
|
59
|
+
*/
|
|
60
|
+
export function isOllamaCloudUrl(url) {
|
|
61
|
+
const v = String(url ?? "").trim();
|
|
62
|
+
if (!/^https:\/\//i.test(v)) return false;
|
|
63
|
+
let host;
|
|
64
|
+
try {
|
|
65
|
+
host = new URL(v).hostname.toLowerCase().replace(/\.$/, "");
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
return host === "ollama.com" || host.endsWith(".ollama.com");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* DOES THIS CODER NEED AN API KEY? — an OpenAI-compatible provider, or Ollama's paid CLOUD.
|
|
74
|
+
*
|
|
75
|
+
* Reported: /model on kimi-k3 at https://ollama.com said "needs no API key — nothing to set", so
|
|
76
|
+
* there was no way to give it one. Every "needs a key" check had been `kind === "openai"`, on the
|
|
77
|
+
* belief that Ollama never authenticates — true of a local or LAN Ollama, false of ollama.com.
|
|
78
|
+
*/
|
|
79
|
+
export function codingNeedsKey(kind, url) {
|
|
80
|
+
return kind === "openai" || (kind === "ollama" && isOllamaCloudUrl(url));
|
|
81
|
+
}
|
|
82
|
+
|
|
55
83
|
/**
|
|
56
84
|
* @param payload the /agent-payload `payload` object (reads codingKeyMissing/codingKeyModel).
|
|
57
85
|
* @param deps.askSecret async (prompt) => string — masked input; "" means the user skipped.
|
|
@@ -113,8 +141,10 @@ export async function promptCodingKeyIfMissing(payload, { askSecret, saveCodingK
|
|
|
113
141
|
*
|
|
114
142
|
* There is a coder either way; what differs is whether it can be USED yet. Name it, and say so.
|
|
115
143
|
*/
|
|
116
|
-
export function codingBannerLabel(payload, { dim = identity, override = "" } = {}) {
|
|
144
|
+
export function codingBannerLabel(payload, { dim = identity, override = "", where = "" } = {}) {
|
|
117
145
|
const p = payload ?? {};
|
|
146
|
+
// "ollama.com/kimi-k3" rather than "kimi-k3": local or cloud, visible in the header too.
|
|
147
|
+
const named = (n) => (where && n ? `${where}/${n}` : n);
|
|
118
148
|
// A `/model` PICK WINS OVER THE ACCOUNT DEFAULT, because it is what the next turn will
|
|
119
149
|
// actually use. The payload carries the account's coder; the override is this folder's,
|
|
120
150
|
// restored from its session file at startup. Reading only the payload put two different
|
|
@@ -124,15 +154,15 @@ export function codingBannerLabel(payload, { dim = identity, override = "" } = {
|
|
|
124
154
|
// Only possible since the pick started surviving an exit; before that the override was always
|
|
125
155
|
// empty at startup and the two could not disagree.
|
|
126
156
|
const pick = String(override ?? "").trim();
|
|
127
|
-
if (pick) return pick;
|
|
128
|
-
if (p.codingModelId) return String(p.codingModelId);
|
|
157
|
+
if (pick) return named(pick);
|
|
158
|
+
if (p.codingModelId) return named(String(p.codingModelId));
|
|
129
159
|
const name = String(p.codingKeyMissing ? p.codingKeyModel || "" : "").trim();
|
|
130
160
|
if (!name) return dim("—");
|
|
131
161
|
// A keyless host is never asked for anything — the prompt stores the placeholder itself and
|
|
132
162
|
// the coder is live moments later, so "(needs a key)" would be a lie with a shelf life of
|
|
133
163
|
// about one line.
|
|
134
|
-
if (isKeylessCodingHost(p.codingKeyUrl)) return name;
|
|
135
|
-
return name + dim(" (needs a key)");
|
|
164
|
+
if (isKeylessCodingHost(p.codingKeyUrl)) return named(name);
|
|
165
|
+
return named(name) + dim(" (needs a key)");
|
|
136
166
|
}
|
|
137
167
|
|
|
138
168
|
/**
|
|
@@ -187,9 +217,10 @@ export function codingKeyFallbackLine(kj, jobId = "") {
|
|
|
187
217
|
* when a choice HAS been made, and when only one key exists — with one there is nothing to
|
|
188
218
|
* choose, and a warning on a correct setup is how warnings stop being read.
|
|
189
219
|
*/
|
|
190
|
-
export function unchosenKeyWarning({ codingKind = "", selectedKeyId = "", keys = null } = {}) {
|
|
220
|
+
export function unchosenKeyWarning({ codingKind = "", codingUrl = "", selectedKeyId = "", keys = null } = {}) {
|
|
191
221
|
const quiet = { warn: false, using: null, why: "" };
|
|
192
|
-
|
|
222
|
+
// Local Ollama and MLX need no credential; the Ollama cloud does.
|
|
223
|
+
if (!codingNeedsKey(codingKind, codingUrl)) return quiet;
|
|
193
224
|
if (!Array.isArray(keys) || keys.length < 2) return quiet;
|
|
194
225
|
if (String(selectedKeyId ?? "").trim()) return quiet;
|
|
195
226
|
return { warn: true, using: keys[0], why: "none-chosen" };
|
|
@@ -200,7 +231,8 @@ export function unchosenKeyWarning({ codingKind = "", selectedKeyId = "", keys =
|
|
|
200
231
|
*
|
|
201
232
|
* REPORTED: the bar's "key not chosen" mark only appeared after running /model. The startup gate
|
|
202
233
|
* read `payload.codingKind`, and a payload fetched with NO override describes the ACCOUNT DEFAULT
|
|
203
|
-
* — on the reporting machine
|
|
234
|
+
* — on the reporting machine kimi-k3 at ollama.com, kind "ollama" (which, it turned out later,
|
|
235
|
+
* DOES need a key: see codingNeedsKey) —
|
|
204
236
|
* while the folder was pointed at gpt-5, kind "openai". So the check answered a question about a
|
|
205
237
|
* coder the folder was not using, said "nothing to warn about", and stayed silent. /model looked
|
|
206
238
|
* right because it re-probes WITH the override.
|
|
@@ -212,6 +244,17 @@ export function unchosenKeyWarning({ codingKind = "", selectedKeyId = "", keys =
|
|
|
212
244
|
* Matched by id OR by bare model name, mirroring that validation: a session saved before ids were
|
|
213
245
|
* qualified ("<kind>::<model>", task #123) stores the bare form and is still a valid pick.
|
|
214
246
|
*/
|
|
247
|
+
/** The folder's coder — its kind AND its URL, which together decide whether it needs a key. */
|
|
248
|
+
export function effectiveCoder({ sessionCodingModel = "", choices = null, defaultKind = "", defaultUrl = "" } = {}) {
|
|
249
|
+
const want = String(sessionCodingModel ?? "").trim();
|
|
250
|
+
const list = Array.isArray(choices) ? choices : [];
|
|
251
|
+
if (want) {
|
|
252
|
+
const hit = list.find((c) => c?.id === want || c?.model === want);
|
|
253
|
+
if (hit) return { kind: String(hit.kind ?? ""), url: String(hit.url ?? "") };
|
|
254
|
+
}
|
|
255
|
+
return { kind: String(defaultKind ?? ""), url: String(defaultUrl ?? "") };
|
|
256
|
+
}
|
|
257
|
+
|
|
215
258
|
export function effectiveCoderKind({
|
|
216
259
|
sessionCodingModel = "",
|
|
217
260
|
choices = null,
|
package/gu-cli.mjs
CHANGED
|
@@ -20,7 +20,7 @@ import { execFile as execFileCallback, spawn } from "node:child_process";
|
|
|
20
20
|
import { createHash } from "node:crypto";
|
|
21
21
|
import { createTaskPool, createFolderQueue } from "./job-pools.mjs";
|
|
22
22
|
import { ragJobConfig } from "./rag-job-config.mjs";
|
|
23
|
-
import { codingKeyFallbackLine } from "./coding-key-prompt.mjs";
|
|
23
|
+
import { codingKeyFallbackLine, isOllamaCloudUrl } from "./coding-key-prompt.mjs";
|
|
24
24
|
// Pure layout helpers — safe to import here (no side effects on load, unlike gu-repl.mjs).
|
|
25
25
|
// Shared so the mid-run picker and the REPL's post-stop advice cannot word the same budget
|
|
26
26
|
// differently.
|
|
@@ -3265,22 +3265,8 @@ function _isLocalCodingUrl(baseURL) {
|
|
|
3265
3265
|
return !host.includes(".") && !host.includes(":");
|
|
3266
3266
|
}
|
|
3267
3267
|
|
|
3268
|
-
/**
|
|
3269
|
-
|
|
3270
|
-
* box, a self-hosted remote) has no auth and gets no key. Mirrors isOllamaCloudUrl in
|
|
3271
|
-
* web/src/lib/coderChoices.mjs — keep the two in step. */
|
|
3272
|
-
function _isOllamaCloudUrl(baseURL) {
|
|
3273
|
-
const v = String(baseURL || "").trim();
|
|
3274
|
-
if (!/^https:\/\//i.test(v)) return false;
|
|
3275
|
-
let host = "";
|
|
3276
|
-
try {
|
|
3277
|
-
host = new URL(v).hostname.toLowerCase().replace(/\.$/, "");
|
|
3278
|
-
} catch {
|
|
3279
|
-
return false;
|
|
3280
|
-
}
|
|
3281
|
-
return host === "ollama.com" || host.endsWith(".ollama.com");
|
|
3282
|
-
}
|
|
3283
|
-
|
|
3268
|
+
/** The Ollama cloud check lives in coding-key-prompt.mjs, shared with the terminal. */
|
|
3269
|
+
const _isOllamaCloudUrl = isOllamaCloudUrl;
|
|
3284
3270
|
|
|
3285
3271
|
/** Resolve to "ollama" | "openai" | "local". `kind` is the user's Settings choice
|
|
3286
3272
|
* ("ollama" | "openai" | "" = Auto). Only Auto costs a probe. */
|
package/gu-repl.mjs
CHANGED
|
@@ -113,7 +113,7 @@ import {
|
|
|
113
113
|
} from "./terminal-layout.mjs";
|
|
114
114
|
import { completedFlush } from "./completed-flush.mjs";
|
|
115
115
|
import { codingBannerLabel, SECRET_KEY_HINT } from "./coding-key-prompt.mjs";
|
|
116
|
-
import { codingChoiceTags } from "./coding-choice-tags.mjs";
|
|
116
|
+
import { codingChoiceTags, coderDisplayName, ollamaWhere } from "./coding-choice-tags.mjs";
|
|
117
117
|
import {
|
|
118
118
|
isDoubleClick,
|
|
119
119
|
clickRun,
|
|
@@ -1973,11 +1973,14 @@ let keyChoicePending = false;
|
|
|
1973
1973
|
const identityLineText = () =>
|
|
1974
1974
|
identityRow(
|
|
1975
1975
|
{
|
|
1976
|
-
coder:
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1976
|
+
coder: coderDisplayName(
|
|
1977
|
+
activeCoderName({
|
|
1978
|
+
sessionLabel: sessionCodingLabel,
|
|
1979
|
+
sessionModel: sessionCodingModel,
|
|
1980
|
+
accountDefault: defaultCodingModel,
|
|
1981
|
+
}),
|
|
1982
|
+
activeCoderWhere,
|
|
1983
|
+
),
|
|
1981
1984
|
folder: folderLabel(),
|
|
1982
1985
|
keyUnchosen: keyChoicePending,
|
|
1983
1986
|
},
|
|
@@ -3480,7 +3483,15 @@ if (process.stdin.isTTY) {
|
|
|
3480
3483
|
// a running turn owns the screen, and a masked prompt drawn over either corrupts both.
|
|
3481
3484
|
if (key && key.ctrl && key.name === "k" && !slashOverlayActive && !following
|
|
3482
3485
|
&& !listPickerActive && !approvalActive) {
|
|
3483
|
-
void
|
|
3486
|
+
// NOT a bare `void`: this runs outside the command loop that turns Esc into "(cancelled)",
|
|
3487
|
+
// so an Esc at the key's name or secret escaped as an unhandled rejection and ENDED THE
|
|
3488
|
+
// PROCESS (reported: "PromptCancelled: cancelled … Node.js v22"). Cancel is caught here; any
|
|
3489
|
+
// other failure is reported and the session goes on — a keypress must not kill the REPL.
|
|
3490
|
+
replaceCodingKey().catch((e) => {
|
|
3491
|
+
if (wasPromptCancelled(e)) console.log(dim(" (cancelled)\n"));
|
|
3492
|
+
else console.log(red(` ctrl+k failed: ${e?.message ?? e}\n`));
|
|
3493
|
+
paintBar();
|
|
3494
|
+
});
|
|
3484
3495
|
return;
|
|
3485
3496
|
}
|
|
3486
3497
|
if (key && key.ctrl && key.name === "t" && !slashOverlayActive) {
|
|
@@ -5780,8 +5791,11 @@ async function chooseModel() {
|
|
|
5780
5791
|
// OpenAI-compatible providers are both "OpenAI-compatible", so the kind names neither.
|
|
5781
5792
|
const { tags, show: showTag } = codingChoiceTags(choices);
|
|
5782
5793
|
const labels = choices.map((c, i) => {
|
|
5783
|
-
|
|
5784
|
-
|
|
5794
|
+
// An Ollama row says WHERE it runs in front of the name ("ollama.com/kimi-k3"), so the kind tag
|
|
5795
|
+
// beside it would only repeat "Ollama". Other rows keep the tag rule.
|
|
5796
|
+
const where = ollamaWhere(c.kind, c.url);
|
|
5797
|
+
const tag = showTag && !where ? ` ${dim(`· ${tags[i]}`)}` : "";
|
|
5798
|
+
return `${coderDisplayName(c.model, where)}${tag}${c.id === def ? " (default)" : ""}`;
|
|
5785
5799
|
});
|
|
5786
5800
|
const activeIdx = choices.findIndex((c) => c.id === active);
|
|
5787
5801
|
labels.push("change the Ollama server this machine uses…");
|
|
@@ -5827,9 +5841,12 @@ async function chooseModel() {
|
|
|
5827
5841
|
// WRITTEN NOW, not at the end of the next turn: `/model` then `/exit` is how the choice is
|
|
5828
5842
|
// made and then tested, and that path never reaches saveSession.
|
|
5829
5843
|
await saveSessionCoding(resolve(process.cwd()));
|
|
5830
|
-
|
|
5844
|
+
activeCoderWhere = ollamaWhere(chosen.kind, chosen.url);
|
|
5845
|
+
const where = showTag && !activeCoderWhere ? ` (${tags[idx]})` : "";
|
|
5831
5846
|
console.log(
|
|
5832
|
-
accent(
|
|
5847
|
+
accent(
|
|
5848
|
+
` ✓ coding model → ${coderDisplayName(chosen.model, activeCoderWhere)}${where}${chosen.id === def ? " (default)" : ""}\n`
|
|
5849
|
+
)
|
|
5833
5850
|
);
|
|
5834
5851
|
// DOES THIS ONE NEED AN API KEY? (task #181) Reported live on Windows 11 / Client Expert:
|
|
5835
5852
|
// picking kimi-k3 here and asking a question just span — the coder needs a key, none was
|
|
@@ -6779,7 +6796,9 @@ async function coderKeyState(override = "") {
|
|
|
6779
6796
|
const active = override
|
|
6780
6797
|
? choices.find((c) => c.id === override)
|
|
6781
6798
|
: choices.find((c) => c.isDefault);
|
|
6782
|
-
|
|
6799
|
+
// Kind AND url: an Ollama coder at https://ollama.com needs a key; a local one does not.
|
|
6800
|
+
const { codingNeedsKey } = await import("./coding-key-prompt.mjs");
|
|
6801
|
+
const needsKey = codingNeedsKey(active?.kind, active?.url);
|
|
6783
6802
|
return {
|
|
6784
6803
|
needsKey,
|
|
6785
6804
|
model: p.codingKeyModel || active?.model || "the coding model",
|
|
@@ -7667,6 +7686,8 @@ let defaultCodingModel = "";
|
|
|
7667
7686
|
// another backend must not keep showing the previous backend's total). Empty = let the API
|
|
7668
7687
|
// fall back to the account-default URL from settings (older behaviour).
|
|
7669
7688
|
let sessionCodingUrl = "";
|
|
7689
|
+
/** "ollama.com" / "127.0.0.1" / a host — where the ACTIVE coder runs, when it is an Ollama one. */
|
|
7690
|
+
let activeCoderWhere = "";
|
|
7670
7691
|
// The account-default backend's URL (from the startup probe's codingChoices), used when no
|
|
7671
7692
|
// /model override is active. Set once at startup alongside defaultCodingModel.
|
|
7672
7693
|
let defaultCodingUrl = "";
|
|
@@ -9327,6 +9348,17 @@ async function main() {
|
|
|
9327
9348
|
sessionCodingUrl = "";
|
|
9328
9349
|
}
|
|
9329
9350
|
}
|
|
9351
|
+
{
|
|
9352
|
+
// WHERE THE CODER RUNS, for the banner and the bar: the folder's pick, else the default.
|
|
9353
|
+
const choices = Array.isArray(probe?.codingChoices) ? probe.codingChoices : [];
|
|
9354
|
+
const active =
|
|
9355
|
+
(sessionCodingModel && choices.find((c) => c.id === sessionCodingModel || c.model === sessionCodingModel)) ||
|
|
9356
|
+
choices.find((c) => c.isDefault) ||
|
|
9357
|
+
null;
|
|
9358
|
+
// No choice list (an older API): no prefix, rather than guess from the ACCOUNT default's raw
|
|
9359
|
+
// payload fields, which key-choice-regression.test.mjs forbids for exactly that reason.
|
|
9360
|
+
activeCoderWhere = active ? ollamaWhere(active.kind, active.url) : "";
|
|
9361
|
+
}
|
|
9330
9362
|
// Task #133: no RAG storage question any more — the terminal ALWAYS keeps the index on
|
|
9331
9363
|
// this machine. `/rag-cloud` opts into additionally mirroring it to the user's S3 bucket.
|
|
9332
9364
|
// Startup banner (task #131): the gu logo on the left, compact status facts on the
|
|
@@ -9375,6 +9407,7 @@ async function main() {
|
|
|
9375
9407
|
sessionLabel: sessionCodingLabel,
|
|
9376
9408
|
sessionModel: sessionCodingModel,
|
|
9377
9409
|
}),
|
|
9410
|
+
where: activeCoderWhere,
|
|
9378
9411
|
}),
|
|
9379
9412
|
lbl("RAG") +
|
|
9380
9413
|
(p.ragEnabled
|
|
@@ -9561,7 +9594,7 @@ async function main() {
|
|
|
9561
9594
|
// folder has not chosen, and more than one is stored.
|
|
9562
9595
|
try {
|
|
9563
9596
|
if (!keyRes?.saved) {
|
|
9564
|
-
const { unchosenKeyWarning,
|
|
9597
|
+
const { unchosenKeyWarning, effectiveCoder, codingNeedsKey } = await import(
|
|
9565
9598
|
"./coding-key-prompt.mjs"
|
|
9566
9599
|
);
|
|
9567
9600
|
// THE RULE LIVES IN THE MODULE so it can be tested by calling it. Open-coded here it
|
|
@@ -9574,15 +9607,17 @@ async function main() {
|
|
|
9574
9607
|
// machine an Ollama coder, which needs no key — while the folder was on gpt-5. Reading
|
|
9575
9608
|
// it directly made this check answer about a coder nobody was using, and the mark only
|
|
9576
9609
|
// appeared after /model, which re-probes WITH the override.
|
|
9577
|
-
const kind =
|
|
9610
|
+
const { kind, url } = effectiveCoder({
|
|
9578
9611
|
sessionCodingModel,
|
|
9579
9612
|
choices: probe?.codingChoices,
|
|
9580
9613
|
defaultKind: p?.codingKind,
|
|
9614
|
+
defaultUrl: probe?.codingChoices?.find?.((c) => c?.isDefault)?.url ?? "",
|
|
9581
9615
|
});
|
|
9582
9616
|
const allKeys =
|
|
9583
|
-
kind
|
|
9617
|
+
codingNeedsKey(kind, url) && !sessionCodingKeyId ? await fetchCodingKeys() : null;
|
|
9584
9618
|
const w = unchosenKeyWarning({
|
|
9585
9619
|
codingKind: kind,
|
|
9620
|
+
codingUrl: url,
|
|
9586
9621
|
selectedKeyId: sessionCodingKeyId,
|
|
9587
9622
|
keys: allKeys,
|
|
9588
9623
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiens.nguyen/gu-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.697",
|
|
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",
|