@tiens.nguyen/gu-cli 1.0.778 → 1.0.779
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/gu-repl.mjs +142 -47
- package/model-setup-ui.mjs +1 -1
- package/package.json +1 -1
- package/preset-editor.mjs +35 -0
- package/terminal-layout.mjs +3 -1
package/gu-repl.mjs
CHANGED
|
@@ -1579,12 +1579,11 @@ let imageNoticeTimer = null;
|
|
|
1579
1579
|
let agentMissing = false; // no agent model chosen → agent falls back to the coding model
|
|
1580
1580
|
let embedMissing = false; // no embed model chosen → RAG is off until /embed-model picks one
|
|
1581
1581
|
// One footer row; the two halves are independent so a model that IS set is never named.
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
};
|
|
1582
|
+
// ONE SHORT ROW EACH (#317): joined, they ran past 80 columns and were cut mid-word.
|
|
1583
|
+
const missingNoticeRows = () => [
|
|
1584
|
+
...(agentMissing ? ["⚠ agent model not set — using the coding model · /agent-model"] : []),
|
|
1585
|
+
...(embedMissing ? ["⚠ no embed model — RAG is off · /embed-model"] : []),
|
|
1586
|
+
];
|
|
1588
1587
|
|
|
1589
1588
|
// #307: the calm note after a quick change of mind — dim, not the ⚠ orange of a warning, and in the
|
|
1590
1589
|
// bar rather than the transcript (which the whole point is to leave untouched).
|
|
@@ -2533,7 +2532,7 @@ const footerRows = () =>
|
|
|
2533
2532
|
? dim(clipVisible(stopNotice, (process.stdout.columns || 80) - visibleLen(GUTTER) - 1))
|
|
2534
2533
|
: "",
|
|
2535
2534
|
// Task #14: the skipped-setup warning — persistent (not a timed notice), red, its own row.
|
|
2536
|
-
missing:
|
|
2535
|
+
missing: missingNoticeRows().map((r) => red(r)),
|
|
2537
2536
|
});
|
|
2538
2537
|
// An ask()'s hint lines (#305), drawn in yellow under its input while it waits. Plain text.
|
|
2539
2538
|
let askHint = [];
|
|
@@ -7060,6 +7059,83 @@ async function runUndoCommand(line) {
|
|
|
7060
7059
|
if (runId === lastStoppedRun) lastStoppedRun = null;
|
|
7061
7060
|
}
|
|
7062
7061
|
|
|
7062
|
+
/**
|
|
7063
|
+
* #317: a preset the person ALREADY HAS, brought in by pasting — its JSON, a whole preset file, or
|
|
7064
|
+
* the `/save-preset <name> {…}` command copied from another computer (#312). A multi-line paste
|
|
7065
|
+
* arrives as "[Pasted text #N +M lines]" and is expanded before it is read, so the key is never
|
|
7066
|
+
* echoed. A mistake is named; Enter then opens it IN THE EDITOR with the pasted text (not a blank
|
|
7067
|
+
* template), and Enter on nothing opens the editor on the current setup. Esc cancels.
|
|
7068
|
+
* Saved 0600 like /save-preset. → the file, or "" when cancelled.
|
|
7069
|
+
*/
|
|
7070
|
+
async function presetFromPaste(pr, home) {
|
|
7071
|
+
const ed = await import("./preset-editor.mjs");
|
|
7072
|
+
console.log(GUTTER + "Paste a preset you already have — its JSON, a preset file, or a /save-preset command.");
|
|
7073
|
+
console.log(GUTTER + dim("Enter on an empty line opens your editor instead · Esc cancels."));
|
|
7074
|
+
let parsed = null;
|
|
7075
|
+
let lastText = "";
|
|
7076
|
+
try {
|
|
7077
|
+
while (!parsed) {
|
|
7078
|
+
const text = String((await ask("preset: ")) ?? "");
|
|
7079
|
+
if (!text.trim()) {
|
|
7080
|
+
const seed = lastText || ed.editorText("preset", ed.presetFromSettings((await fetchWorkerSettings()) ?? {}));
|
|
7081
|
+
parsed = await fixPresetInEditor(ed, seed);
|
|
7082
|
+
if (!parsed) return "";
|
|
7083
|
+
break;
|
|
7084
|
+
}
|
|
7085
|
+
const p = ed.parsePastedPreset(text);
|
|
7086
|
+
if (p.ok) {
|
|
7087
|
+
parsed = p;
|
|
7088
|
+
break;
|
|
7089
|
+
}
|
|
7090
|
+
lastText = text;
|
|
7091
|
+
console.log(GUTTER + yellow(`Not a preset yet — ${p.problem}`));
|
|
7092
|
+
console.log(GUTTER + dim("Paste it again, or press Enter to fix it in your editor."));
|
|
7093
|
+
}
|
|
7094
|
+
const name = await askPresetName(pr, parsed.name || "preset");
|
|
7095
|
+
if (!name) return "";
|
|
7096
|
+
const file = pr.presetFilePath(name, home);
|
|
7097
|
+
if (existsSync(file) && !(await askYesNo(`A preset called "${name}" exists — replace it?`, false))) return "";
|
|
7098
|
+
writeFileSync(file, JSON.stringify(pr.buildPreset({ name, preset: parsed.preset, host: hostname() }), null, 2) + "\n", { mode: 0o600 });
|
|
7099
|
+
console.log(GUTTER + `Preset "${name}" saved to ${file}`);
|
|
7100
|
+
return file;
|
|
7101
|
+
} catch (e) {
|
|
7102
|
+
if (wasPromptCancelled(e)) return "";
|
|
7103
|
+
throw e;
|
|
7104
|
+
}
|
|
7105
|
+
}
|
|
7106
|
+
|
|
7107
|
+
/** The editor loop of #312, seeded with `text`: → a parsed preset, or null when given up. */
|
|
7108
|
+
async function fixPresetInEditor(ed, text) {
|
|
7109
|
+
const editor = ed.pickEditor();
|
|
7110
|
+
if (!editor || !process.stdin.isTTY) {
|
|
7111
|
+
console.log(GUTTER + (editor ? "an editor needs a terminal." : "no editor found (set $EDITOR, e.g. export EDITOR=nano)."));
|
|
7112
|
+
return null;
|
|
7113
|
+
}
|
|
7114
|
+
let t = text;
|
|
7115
|
+
for (;;) {
|
|
7116
|
+
const r = withTerminalHandedOver(() => ed.editText(t, { editor, fileName: "preset.json" }));
|
|
7117
|
+
if (!r.ok) {
|
|
7118
|
+
console.log(GUTTER + `Not saved — ${r.why}.`);
|
|
7119
|
+
return null;
|
|
7120
|
+
}
|
|
7121
|
+
const p = ed.parsePastedPreset(r.text);
|
|
7122
|
+
if (p.ok) return p;
|
|
7123
|
+
console.log(GUTTER + yellow(`Not saved yet — ${p.problem}`));
|
|
7124
|
+
if (!(await askYesNo("Edit again?", true))) return null;
|
|
7125
|
+
t = r.text;
|
|
7126
|
+
}
|
|
7127
|
+
}
|
|
7128
|
+
|
|
7129
|
+
/** A preset name, defaulting to `dflt` (Enter). "" when cancelled. */
|
|
7130
|
+
async function askPresetName(pr, dflt) {
|
|
7131
|
+
for (;;) {
|
|
7132
|
+
const n = String((await ask(`preset name${dflt ? ` [${dflt}]` : ""}: `)) ?? "").trim() || dflt;
|
|
7133
|
+
if (!n) return "";
|
|
7134
|
+
if (pr.isPresetName(n)) return n;
|
|
7135
|
+
console.log(GUTTER + yellow(`"${n}" is not a preset name — one word, no slashes.`));
|
|
7136
|
+
}
|
|
7137
|
+
}
|
|
7138
|
+
|
|
7063
7139
|
async function runLoadPresetsCommand(line) {
|
|
7064
7140
|
const pr = await import("./presets-transfer.mjs");
|
|
7065
7141
|
const home = homedir();
|
|
@@ -7082,53 +7158,72 @@ async function runLoadPresetsCommand(line) {
|
|
|
7082
7158
|
// picker that stayed open could not be clicked until it closed.
|
|
7083
7159
|
const ed = await import("./preset-editor.mjs");
|
|
7084
7160
|
const found = pr.presetFilesIn(listJsonFiles(home));
|
|
7161
|
+
// NO DEAD END (#317: "I need a place to input a preset I have already"): with nothing saved,
|
|
7162
|
+
// the command asks for one right here; with presets saved, the picker's LAST row does (last, so
|
|
7163
|
+
// Enter still applies the newest preset, as it always has).
|
|
7085
7164
|
if (found.length === 0) {
|
|
7086
|
-
console.log(GUTTER +
|
|
7087
|
-
|
|
7088
|
-
|
|
7089
|
-
|
|
7090
|
-
console.log(dim(` Presets in ${home} (${ed.PICKER_HINT}):`));
|
|
7091
|
-
const pick = await pickFromList(found, 0, { keys: ed.PICKER_KEYS });
|
|
7092
|
-
if (!pick || pick.index < 0) {
|
|
7093
|
-
console.log(GUTTER + "load cancelled.");
|
|
7094
|
-
console.log("");
|
|
7095
|
-
return;
|
|
7096
|
-
}
|
|
7097
|
-
file = pr.presetFilePath(found[pick.index], home);
|
|
7098
|
-
if (pick.action === "edit") {
|
|
7099
|
-
await runSavePresetCommand(`/save-preset ${found[pick.index]}`);
|
|
7100
|
-
return;
|
|
7101
|
-
}
|
|
7102
|
-
if (pick.action !== "select") {
|
|
7103
|
-
const shown = pr.validatePresetFile(readFileSync(file, "utf8"));
|
|
7104
|
-
if (!shown.ok) {
|
|
7105
|
-
console.log(GUTTER + `Can't show it — ${shown.problem}`);
|
|
7165
|
+
console.log(GUTTER + "No presets saved yet.");
|
|
7166
|
+
file = await presetFromPaste(pr, home);
|
|
7167
|
+
if (!file) {
|
|
7168
|
+
console.log(GUTTER + "load cancelled.");
|
|
7106
7169
|
console.log("");
|
|
7107
7170
|
return;
|
|
7108
7171
|
}
|
|
7109
|
-
|
|
7110
|
-
|
|
7111
|
-
|
|
7112
|
-
|
|
7113
|
-
|
|
7114
|
-
|
|
7115
|
-
console.log(
|
|
7116
|
-
|
|
7117
|
-
|
|
7118
|
-
|
|
7119
|
-
|
|
7172
|
+
} else {
|
|
7173
|
+
console.log(dim(` Presets in ${home} (${ed.PICKER_HINT}):`));
|
|
7174
|
+
const PASTE_ROW = "+ Paste a preset I already have…";
|
|
7175
|
+
const pick = await pickFromList([...found, PASTE_ROW], 0, { keys: ed.PICKER_KEYS });
|
|
7176
|
+
if (!pick || pick.index < 0) {
|
|
7177
|
+
console.log(GUTTER + "load cancelled.");
|
|
7178
|
+
console.log("");
|
|
7179
|
+
return;
|
|
7180
|
+
}
|
|
7181
|
+
if (pick.index === found.length) {
|
|
7182
|
+
file = await presetFromPaste(pr, home);
|
|
7183
|
+
if (!file) {
|
|
7184
|
+
console.log(GUTTER + "load cancelled.");
|
|
7185
|
+
console.log("");
|
|
7186
|
+
return;
|
|
7187
|
+
}
|
|
7188
|
+
pick.action = "select"; // a pasted preset goes on to be applied, whatever key opened the row
|
|
7189
|
+
} else {
|
|
7190
|
+
file = pr.presetFilePath(found[pick.index], home);
|
|
7191
|
+
}
|
|
7192
|
+
if (pick.action === "edit") {
|
|
7193
|
+
await runSavePresetCommand(`/save-preset ${found[pick.index]}`);
|
|
7194
|
+
return;
|
|
7195
|
+
}
|
|
7196
|
+
if (pick.action !== "select") {
|
|
7197
|
+
const shown = pr.validatePresetFile(readFileSync(file, "utf8"));
|
|
7198
|
+
if (!shown.ok) {
|
|
7199
|
+
console.log(GUTTER + `Can't show it — ${shown.problem}`);
|
|
7200
|
+
console.log("");
|
|
7201
|
+
return;
|
|
7202
|
+
}
|
|
7203
|
+
const pname = shown.name || found[pick.index];
|
|
7204
|
+
const command = ed.presetCopyCommand(pname, shown.preset);
|
|
7205
|
+
if (pick.action === "view") {
|
|
7206
|
+
const block = ed.presetBlock(pname, shown.preset, { width: process.stdout.columns || 80, gutter: GUTTER });
|
|
7207
|
+
const [top, ...restRows] = block.rows;
|
|
7208
|
+
const cut = block.button.from;
|
|
7209
|
+
console.log(dim(top.slice(0, cut)) + accent(top.slice(cut, block.button.to)) + dim(top.slice(block.button.to)));
|
|
7210
|
+
for (const r of restRows) console.log(r);
|
|
7211
|
+
registerCopyBlock(block.rows.length, command, 0, { button: block.button, note: "✓ copied — includes the API key" });
|
|
7212
|
+
console.log(GUTTER + dim("click ⧉ copy for a /save-preset command to paste anywhere — it includes the real key."));
|
|
7213
|
+
console.log(GUTTER + dim(`/load-presets ${pname} applies it.`));
|
|
7214
|
+
console.log("");
|
|
7215
|
+
return;
|
|
7216
|
+
}
|
|
7217
|
+
const res = await writeClipboard(pick.action === "json" ? ed.presetCopyJson(shown.preset) : command);
|
|
7218
|
+
console.log(
|
|
7219
|
+
GUTTER +
|
|
7220
|
+
(res.ok
|
|
7221
|
+
? accent(`✓ copied ${pick.action === "json" ? "the JSON" : "the /save-preset command"} for "${pname}" — includes the API key.`)
|
|
7222
|
+
: yellow(`couldn't copy (${res.why}) — the preset is in ${file}.`))
|
|
7223
|
+
);
|
|
7120
7224
|
console.log("");
|
|
7121
7225
|
return;
|
|
7122
7226
|
}
|
|
7123
|
-
const res = await writeClipboard(pick.action === "json" ? ed.presetCopyJson(shown.preset) : command);
|
|
7124
|
-
console.log(
|
|
7125
|
-
GUTTER +
|
|
7126
|
-
(res.ok
|
|
7127
|
-
? accent(`✓ copied ${pick.action === "json" ? "the JSON" : "the /save-preset command"} for "${pname}" — includes the API key.`)
|
|
7128
|
-
: yellow(`couldn't copy (${res.why}) — the preset is in ${file}.`))
|
|
7129
|
-
);
|
|
7130
|
-
console.log("");
|
|
7131
|
-
return;
|
|
7132
7227
|
}
|
|
7133
7228
|
}
|
|
7134
7229
|
const checked = pr.validatePresetFile(readFileSync(file, "utf8"));
|
package/model-setup-ui.mjs
CHANGED
|
@@ -98,7 +98,7 @@ export function modelSetupMenu({ presets = 0 } = {}) {
|
|
|
98
98
|
" 1 Guided — pick the servers and models (Enter)",
|
|
99
99
|
presets > 0
|
|
100
100
|
? ` 2 Load a saved preset (${presets} saved)`
|
|
101
|
-
: " 2 Load a
|
|
101
|
+
: " 2 Load a preset (none saved yet — paste one you already have)",
|
|
102
102
|
" 3 Skip for now — set them later with /model",
|
|
103
103
|
];
|
|
104
104
|
}
|
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.779",
|
|
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",
|
package/preset-editor.mjs
CHANGED
|
@@ -185,3 +185,38 @@ export function presetBlock(name, preset, { width = 80, gutter = " " } = {}) {
|
|
|
185
185
|
/** The keys the /load-presets picker takes, in the order its hint line names them. */
|
|
186
186
|
export const PICKER_KEYS = { v: "view", c: "copy", j: "json", e: "edit" };
|
|
187
187
|
export const PICKER_HINT = "↑↓ · Enter apply · v view · c copy command · j copy JSON · e edit · Esc back";
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* A PRESET SOMEONE ALREADY HAS, pasted into /load-presets (#317: "I need a place to input a preset
|
|
191
|
+
* I have already, which is in JSON format"). Three shapes are accepted, because those are the three
|
|
192
|
+
* things a person could be holding:
|
|
193
|
+
*
|
|
194
|
+
* · the JSON itself { "apiUrl": …, "apiKey": …, "agentModel": …, … }
|
|
195
|
+
* · a whole preset FILE { "gu": "preset", "name": "home", "version": 1, … }
|
|
196
|
+
* · the command copied with c /save-preset home {"apiUrl":…} (#312, from another computer)
|
|
197
|
+
*
|
|
198
|
+
* → { ok: true, preset, name } (name = the one the text carries, or "") | { ok: false, problem }.
|
|
199
|
+
* JSON problems name the line and column (parseEdited), and `//` lines are allowed, as in the editor.
|
|
200
|
+
*/
|
|
201
|
+
export function parsePastedPreset(text) {
|
|
202
|
+
let src = String(text ?? "").trim();
|
|
203
|
+
if (!src) return { ok: false, empty: true, problem: "nothing was pasted." };
|
|
204
|
+
let name = "";
|
|
205
|
+
const cmd = /^\/save-preset\s+(\S+)\s+([\s\S]+)$/.exec(src);
|
|
206
|
+
if (cmd) {
|
|
207
|
+
name = cmd[1];
|
|
208
|
+
src = cmd[2].trim();
|
|
209
|
+
} else if (/^\/save-preset\b/.test(src)) {
|
|
210
|
+
return { ok: false, problem: "that /save-preset command has no JSON after its name." };
|
|
211
|
+
}
|
|
212
|
+
const parsed = parseEdited(src);
|
|
213
|
+
if (!parsed.ok) return parsed;
|
|
214
|
+
// A whole preset file carries its name inside; the command's own name wins when both exist.
|
|
215
|
+
try {
|
|
216
|
+
const doc = JSON.parse(src.replace(/^[ \t]*\/\/.*$/gm, ""));
|
|
217
|
+
if (!name && doc && typeof doc.name === "string" && doc.gu === "preset") name = doc.name.trim();
|
|
218
|
+
} catch {
|
|
219
|
+
/* parseEdited already accepted it — a name is optional */
|
|
220
|
+
}
|
|
221
|
+
return { ok: true, preset: parsed.preset, name };
|
|
222
|
+
}
|
package/terminal-layout.mjs
CHANGED
|
@@ -522,7 +522,9 @@ export function buildFooterRows({
|
|
|
522
522
|
// Task #14: a skipped model setup stays VISIBLE. When private/client-expert setup was
|
|
523
523
|
// declined the agent model and/or the embed model is unset — say so at the bottom, every
|
|
524
524
|
// frame, until one is chosen. Its own row, under the passing notice, so it persists.
|
|
525
|
-
|
|
525
|
+
// One row EACH (#317): the two warnings joined on one row ran past 80 columns and were cut
|
|
526
|
+
// mid-word ("…(/agent-model to choose on"). A string still works — one row, as before.
|
|
527
|
+
...(Array.isArray(missing) ? missing : missing ? [missing] : []).filter(Boolean).map((m) => clipVisible(gutter + m, w - 1)),
|
|
526
528
|
];
|
|
527
529
|
}
|
|
528
530
|
|