promptimizer-cli 0.1.49 → 0.1.51
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/bin/promptimizer.mjs +249 -32
- package/package.json +1 -1
package/bin/promptimizer.mjs
CHANGED
|
@@ -26,6 +26,201 @@ const color = process.stdout.isTTY
|
|
|
26
26
|
? (code, text) => `${code}${text}${ANSI.reset}`
|
|
27
27
|
: (_code, text) => text;
|
|
28
28
|
|
|
29
|
+
const ANSI_OK = Boolean(process.stdout.isTTY);
|
|
30
|
+
|
|
31
|
+
/** Inline markdown → ANSI (bold, italic, code, links). */
|
|
32
|
+
function styleInline(text) {
|
|
33
|
+
let s = String(text);
|
|
34
|
+
// code first so we don't style inside backticks
|
|
35
|
+
s = s.replace(/`([^`]+)`/g, (_, code) => color(ANSI.cyan, code));
|
|
36
|
+
s = s.replace(/\*\*([^*]+)\*\*/g, (_, t) => color(ANSI.bold, t));
|
|
37
|
+
s = s.replace(/__([^_]+)__/g, (_, t) => color(ANSI.bold, t));
|
|
38
|
+
s = s.replace(/(?<!\*)\*([^*\n]+)\*(?!\*)/g, (_, t) => color(ANSI.dim, t));
|
|
39
|
+
s = s.replace(/(?<!_)_([^_\n]+)_(?!_)/g, (_, t) => color(ANSI.dim, t));
|
|
40
|
+
s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, url) => `${color(ANSI.blue, label)} ${color(ANSI.dim, `(${url})`)}`);
|
|
41
|
+
return s;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Render markdown for the terminal. Keeps structure readable without extra deps.
|
|
46
|
+
* Headers, lists, fences, quotes, hr, tables (plain), paragraphs.
|
|
47
|
+
*/
|
|
48
|
+
function renderMarkdown(source) {
|
|
49
|
+
if (!source) return "";
|
|
50
|
+
const lines = String(source).replace(/\r\n/g, "\n").split("\n");
|
|
51
|
+
const outLines = [];
|
|
52
|
+
let i = 0;
|
|
53
|
+
let inFence = false;
|
|
54
|
+
let fenceLang = "";
|
|
55
|
+
|
|
56
|
+
while (i < lines.length) {
|
|
57
|
+
const line = lines[i];
|
|
58
|
+
|
|
59
|
+
if (line.startsWith("```")) {
|
|
60
|
+
if (!inFence) {
|
|
61
|
+
inFence = true;
|
|
62
|
+
fenceLang = line.slice(3).trim();
|
|
63
|
+
outLines.push(color(ANSI.dim, fenceLang ? `┌─ ${fenceLang}` : "┌─"));
|
|
64
|
+
} else {
|
|
65
|
+
inFence = false;
|
|
66
|
+
fenceLang = "";
|
|
67
|
+
outLines.push(color(ANSI.dim, "└─"));
|
|
68
|
+
}
|
|
69
|
+
i += 1;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (inFence) {
|
|
74
|
+
outLines.push(` ${color(ANSI.cyan, line)}`);
|
|
75
|
+
i += 1;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (/^#{1,6}\s+/.test(line)) {
|
|
80
|
+
const level = line.match(/^#+/)[0].length;
|
|
81
|
+
const title = line.replace(/^#{1,6}\s+/, "");
|
|
82
|
+
const styled = styleInline(title);
|
|
83
|
+
outLines.push(level <= 2 ? color(ANSI.bold, styled) : styled);
|
|
84
|
+
i += 1;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (/^\s*([-*_] *){3,}\s*$/.test(line)) {
|
|
89
|
+
outLines.push(color(ANSI.dim, " ───"));
|
|
90
|
+
i += 1;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (/^\s*>\s?/.test(line)) {
|
|
95
|
+
const body = line.replace(/^\s*>\s?/, "");
|
|
96
|
+
outLines.push(`${color(ANSI.dim, "│")} ${styleInline(body)}`);
|
|
97
|
+
i += 1;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const ul = line.match(/^(\s*)([-*+])\s+(.*)$/);
|
|
102
|
+
if (ul) {
|
|
103
|
+
const indent = Math.min(Math.floor(ul[1].length / 2), 4);
|
|
104
|
+
outLines.push(`${" ".repeat(indent)}${color(ANSI.magenta, "•")} ${styleInline(ul[3])}`);
|
|
105
|
+
i += 1;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const ol = line.match(/^(\s*)(\d+)[.)]\s+(.*)$/);
|
|
110
|
+
if (ol) {
|
|
111
|
+
const indent = Math.min(Math.floor(ol[1].length / 2), 4);
|
|
112
|
+
outLines.push(`${" ".repeat(indent)}${color(ANSI.magenta, `${ol[2]}.`)} ${styleInline(ol[3])}`);
|
|
113
|
+
i += 1;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (line.includes("|") && line.trim().startsWith("|")) {
|
|
118
|
+
// simple table row — strip pipes, pad lightly
|
|
119
|
+
const cells = line
|
|
120
|
+
.split("|")
|
|
121
|
+
.slice(1, -1)
|
|
122
|
+
.map((c) => c.trim());
|
|
123
|
+
if (cells.every((c) => /^:?-+:?$/.test(c))) {
|
|
124
|
+
i += 1;
|
|
125
|
+
continue; // separator
|
|
126
|
+
}
|
|
127
|
+
outLines.push(` ${cells.map((c) => styleInline(c)).join(color(ANSI.dim, " · "))}`);
|
|
128
|
+
i += 1;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!line.trim()) {
|
|
133
|
+
outLines.push("");
|
|
134
|
+
i += 1;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
outLines.push(styleInline(line));
|
|
139
|
+
i += 1;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Trim trailing blank lines
|
|
143
|
+
while (outLines.length && outLines[outLines.length - 1] === "") outLines.pop();
|
|
144
|
+
return outLines.join("\n");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function printAnswer(text) {
|
|
148
|
+
if (!text) {
|
|
149
|
+
out(color(ANSI.dim, "(empty)"));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const rendered = ANSI_OK ? renderMarkdown(text) : text;
|
|
153
|
+
process.stdout.write(`${rendered}\n`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function sleep(ms) {
|
|
157
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Split so ANSI escapes stay intact while typewriting. */
|
|
161
|
+
function ansiChunks(text) {
|
|
162
|
+
const parts = [];
|
|
163
|
+
const re = /\x1b\[[0-9;]*m|[^\x1b]+/g;
|
|
164
|
+
let match;
|
|
165
|
+
while ((match = re.exec(String(text)))) parts.push(match[0]);
|
|
166
|
+
return parts;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Stream clean markdown to the terminal. Cache hits use a very high CPS so
|
|
171
|
+
* the replay feels snappy instead of dumping raw markdown source.
|
|
172
|
+
*/
|
|
173
|
+
async function streamAnswer(text, opts = {}) {
|
|
174
|
+
if (!text) {
|
|
175
|
+
out(color(ANSI.dim, "(empty)"));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (!ANSI_OK) {
|
|
179
|
+
process.stdout.write(`${text}\n`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const rendered = renderMarkdown(text);
|
|
183
|
+
const cacheHit = Boolean(opts.cacheHit);
|
|
184
|
+
// ~chars per second for the visible body (escapes are free)
|
|
185
|
+
const cps = cacheHit ? 14_000 : opts.fast ? 9_000 : 5_500;
|
|
186
|
+
const chunkSize = Math.max(8, Math.floor(cps / 80));
|
|
187
|
+
let pending = "";
|
|
188
|
+
let visible = 0;
|
|
189
|
+
|
|
190
|
+
for (const part of ansiChunks(rendered)) {
|
|
191
|
+
if (part.startsWith("\x1b")) {
|
|
192
|
+
process.stdout.write(part);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
for (let i = 0; i < part.length; ) {
|
|
196
|
+
const slice = part.slice(i, i + chunkSize);
|
|
197
|
+
pending += slice;
|
|
198
|
+
visible += slice.length;
|
|
199
|
+
i += slice.length;
|
|
200
|
+
if (pending.length >= chunkSize || slice.includes("\n")) {
|
|
201
|
+
process.stdout.write(pending);
|
|
202
|
+
pending = "";
|
|
203
|
+
await sleep(Math.max(4, Math.round((1000 * chunkSize) / cps)));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (pending) process.stdout.write(pending);
|
|
208
|
+
process.stdout.write("\n");
|
|
209
|
+
// silence unused when tiny answers finish in one tick
|
|
210
|
+
void visible;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function isCacheHit(meta) {
|
|
214
|
+
if (!meta || typeof meta !== "object") return false;
|
|
215
|
+
return Boolean(
|
|
216
|
+
meta.exact_cache_hit ||
|
|
217
|
+
meta.prompt_cache_hit ||
|
|
218
|
+
meta.semantic_cache_hit ||
|
|
219
|
+
meta.prefix_cache_hit ||
|
|
220
|
+
meta.cache_hit,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
29
224
|
const COMMANDS = [
|
|
30
225
|
["", "Start interactive session (Gemini-style REPL)"],
|
|
31
226
|
["login", "Save a Promptimizer API key"],
|
|
@@ -335,7 +530,7 @@ async function complete(flags, config, messages) {
|
|
|
335
530
|
});
|
|
336
531
|
}
|
|
337
532
|
|
|
338
|
-
/** Stream a completion;
|
|
533
|
+
/** Stream a completion; buffers tokens (no raw dump), returns { text, result }. */
|
|
339
534
|
async function completeStream(flags, config, messages) {
|
|
340
535
|
const gatewayURL = gateway(flags, config);
|
|
341
536
|
const { apiKey, sessionId } = authFromConfig(flags, config);
|
|
@@ -371,38 +566,52 @@ async function completeStream(flags, config, messages) {
|
|
|
371
566
|
promptimizer: undefined,
|
|
372
567
|
choices: [{ message: { role: "assistant", content: "" } }],
|
|
373
568
|
};
|
|
569
|
+
let spinTimer = null;
|
|
570
|
+
let spinFrame = 0;
|
|
571
|
+
const started = Date.now();
|
|
572
|
+
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
573
|
+
if (ANSI_OK) {
|
|
574
|
+
spinTimer = setInterval(() => {
|
|
575
|
+
const frame = frames[spinFrame++ % frames.length];
|
|
576
|
+
process.stdout.write(`\r${color(ANSI.dim, ` ${frame} routing…`)}`);
|
|
577
|
+
}, 80);
|
|
578
|
+
}
|
|
374
579
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
text += delta;
|
|
393
|
-
|
|
580
|
+
try {
|
|
581
|
+
while (true) {
|
|
582
|
+
const { done, value } = await reader.read();
|
|
583
|
+
if (done) break;
|
|
584
|
+
buffer += decoder.decode(value, { stream: true });
|
|
585
|
+
const parts = buffer.split("\n");
|
|
586
|
+
buffer = parts.pop() ?? "";
|
|
587
|
+
for (const line of parts) {
|
|
588
|
+
const trimmed = line.trim();
|
|
589
|
+
if (!trimmed.startsWith("data:")) continue;
|
|
590
|
+
const data = trimmed.slice(5).trim();
|
|
591
|
+
if (!data) continue;
|
|
592
|
+
if (data === "[DONE]") continue;
|
|
593
|
+
try {
|
|
594
|
+
const parsed = JSON.parse(data);
|
|
595
|
+
if (parsed.error?.message) throw new Error(parsed.error.message);
|
|
596
|
+
const delta = parsed.choices?.[0]?.delta?.content;
|
|
597
|
+
if (delta) text += delta;
|
|
598
|
+
if (parsed.promptimizer) result.promptimizer = parsed.promptimizer;
|
|
599
|
+
if (parsed.usage) result.usage = parsed.usage;
|
|
600
|
+
if (parsed.model) result.model = parsed.model;
|
|
601
|
+
} catch (err) {
|
|
602
|
+
if (err instanceof Error && err.message && !err.message.includes("JSON")) throw err;
|
|
394
603
|
}
|
|
395
|
-
if (parsed.promptimizer) result.promptimizer = parsed.promptimizer;
|
|
396
|
-
if (parsed.usage) result.usage = parsed.usage;
|
|
397
|
-
if (parsed.model) result.model = parsed.model;
|
|
398
|
-
} catch (err) {
|
|
399
|
-
if (err instanceof Error && err.message && !err.message.includes("JSON")) throw err;
|
|
400
604
|
}
|
|
401
605
|
}
|
|
606
|
+
} finally {
|
|
607
|
+
if (spinTimer) {
|
|
608
|
+
clearInterval(spinTimer);
|
|
609
|
+
process.stdout.write("\r\x1b[2K");
|
|
610
|
+
}
|
|
402
611
|
}
|
|
403
612
|
|
|
404
613
|
result.choices = [{ message: { role: "assistant", content: text } }];
|
|
405
|
-
return { text, result };
|
|
614
|
+
return { text, result, elapsedMs: Date.now() - started };
|
|
406
615
|
}
|
|
407
616
|
|
|
408
617
|
async function cmdLogin(flags) {
|
|
@@ -544,9 +753,14 @@ async function cmdChat(flags, positional) {
|
|
|
544
753
|
if (!prompt) die('Usage: promptimizer chat "What is 17 * 24?"');
|
|
545
754
|
out();
|
|
546
755
|
out(color(ANSI.magenta, "✦"));
|
|
547
|
-
const { text, result } = await completeStream(flags, config, [
|
|
548
|
-
|
|
549
|
-
|
|
756
|
+
const { text, result, elapsedMs } = await completeStream(flags, config, [
|
|
757
|
+
{ role: "user", content: prompt },
|
|
758
|
+
]);
|
|
759
|
+
const meta = result.promptimizer ?? {};
|
|
760
|
+
await streamAnswer(text, {
|
|
761
|
+
cacheHit: isCacheHit(meta),
|
|
762
|
+
fast: (elapsedMs ?? 0) < 500,
|
|
763
|
+
});
|
|
550
764
|
out();
|
|
551
765
|
printMeta(result);
|
|
552
766
|
out();
|
|
@@ -754,9 +968,12 @@ async function interactive(flags) {
|
|
|
754
968
|
out();
|
|
755
969
|
out(color(ANSI.magenta, "✦"));
|
|
756
970
|
try {
|
|
757
|
-
const { text, result } = await completeStream(flags, readConfig(), history);
|
|
758
|
-
|
|
759
|
-
|
|
971
|
+
const { text, result, elapsedMs } = await completeStream(flags, readConfig(), history);
|
|
972
|
+
const meta = result.promptimizer ?? {};
|
|
973
|
+
await streamAnswer(text, {
|
|
974
|
+
cacheHit: isCacheHit(meta),
|
|
975
|
+
fast: (elapsedMs ?? 0) < 500,
|
|
976
|
+
});
|
|
760
977
|
history.push({ role: "assistant", content: text });
|
|
761
978
|
out();
|
|
762
979
|
printMeta(result);
|