promptimizer-cli 0.1.50 → 0.1.52
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 +117 -23
- package/package.json +1 -1
package/bin/promptimizer.mjs
CHANGED
|
@@ -28,16 +28,39 @@ const color = process.stdout.isTTY
|
|
|
28
28
|
|
|
29
29
|
const ANSI_OK = Boolean(process.stdout.isTTY);
|
|
30
30
|
|
|
31
|
-
/** Inline markdown → ANSI (bold, italic, code, links). */
|
|
31
|
+
/** Inline markdown → ANSI (bold, italic, code, links). Strips markers even without a TTY. */
|
|
32
32
|
function styleInline(text) {
|
|
33
33
|
let s = String(text);
|
|
34
|
-
// code first
|
|
35
|
-
|
|
36
|
-
s = s.replace(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
// Protect code spans first
|
|
35
|
+
const codes = [];
|
|
36
|
+
s = s.replace(/`([^`]+)`/g, (_, code) => {
|
|
37
|
+
const token = `\u0000C${codes.length}\u0000`;
|
|
38
|
+
codes.push(color(ANSI.cyan, code));
|
|
39
|
+
return token;
|
|
40
|
+
});
|
|
41
|
+
// Bold before italic so ** wins over *
|
|
42
|
+
s = s.replace(/\*\*((?:[^*]|\*(?!\*))+?)\*\*/g, (_, t) => color(ANSI.bold, t));
|
|
43
|
+
s = s.replace(/__((?:[^_]|_(?!_))+?)__/g, (_, t) => color(ANSI.bold, t));
|
|
44
|
+
s = s.replace(/(?<!\*)\*([^*\n]+?)\*(?!\*)/g, (_, t) => color(ANSI.dim, t));
|
|
45
|
+
s = s.replace(/(?<!_)_([^_\n]+?)_(?!_)/g, (_, t) => color(ANSI.dim, t));
|
|
46
|
+
s = s.replace(
|
|
47
|
+
/\[([^\]]+)\]\(([^)]+)\)/g,
|
|
48
|
+
(_, label, url) => `${color(ANSI.blue, label)} ${color(ANSI.dim, `(${url})`)}`,
|
|
49
|
+
);
|
|
50
|
+
// Restore code
|
|
51
|
+
s = s.replace(/\u0000C(\d+)\u0000/g, (_, i) => codes[Number(i)] ?? "");
|
|
52
|
+
return s;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Soft-normalize messy model markdown before line parsing. */
|
|
56
|
+
function normalizeMarkdownSource(source) {
|
|
57
|
+
let s = String(source).replace(/\r\n/g, "\n");
|
|
58
|
+
// Break glued heading markers: "---### Title" / "text### Title"
|
|
59
|
+
s = s.replace(/([^\n#])(#{1,6}\s)/g, "$1\n$2");
|
|
60
|
+
// Break glued thematic break before heading: "---###"
|
|
61
|
+
s = s.replace(/(-{3,}|_{3,}|\*{3,})(#{1,6}\s)/g, "$1\n$2");
|
|
62
|
+
// Ensure list markers start on their own line when jammed after text
|
|
63
|
+
s = s.replace(/([.:;!?])\s*([-*+]\s+\*\*)/g, "$1\n$2");
|
|
41
64
|
return s;
|
|
42
65
|
}
|
|
43
66
|
|
|
@@ -47,7 +70,7 @@ function styleInline(text) {
|
|
|
47
70
|
*/
|
|
48
71
|
function renderMarkdown(source) {
|
|
49
72
|
if (!source) return "";
|
|
50
|
-
const lines =
|
|
73
|
+
const lines = normalizeMarkdownSource(source).split("\n");
|
|
51
74
|
const outLines = [];
|
|
52
75
|
let i = 0;
|
|
53
76
|
let inFence = false;
|
|
@@ -56,10 +79,10 @@ function renderMarkdown(source) {
|
|
|
56
79
|
while (i < lines.length) {
|
|
57
80
|
const line = lines[i];
|
|
58
81
|
|
|
59
|
-
if (line.startsWith("```")) {
|
|
82
|
+
if (line.trimStart().startsWith("```")) {
|
|
60
83
|
if (!inFence) {
|
|
61
84
|
inFence = true;
|
|
62
|
-
fenceLang = line.slice(3).trim();
|
|
85
|
+
fenceLang = line.trimStart().slice(3).trim();
|
|
63
86
|
outLines.push(color(ANSI.dim, fenceLang ? `┌─ ${fenceLang}` : "┌─"));
|
|
64
87
|
} else {
|
|
65
88
|
inFence = false;
|
|
@@ -80,7 +103,7 @@ function renderMarkdown(source) {
|
|
|
80
103
|
const level = line.match(/^#+/)[0].length;
|
|
81
104
|
const title = line.replace(/^#{1,6}\s+/, "");
|
|
82
105
|
const styled = styleInline(title);
|
|
83
|
-
outLines.push(level <= 2 ? color(ANSI.bold, styled) : styled);
|
|
106
|
+
outLines.push(level <= 2 ? color(ANSI.bold, styled) : color(ANSI.bold, styled));
|
|
84
107
|
i += 1;
|
|
85
108
|
continue;
|
|
86
109
|
}
|
|
@@ -115,14 +138,13 @@ function renderMarkdown(source) {
|
|
|
115
138
|
}
|
|
116
139
|
|
|
117
140
|
if (line.includes("|") && line.trim().startsWith("|")) {
|
|
118
|
-
// simple table row — strip pipes, pad lightly
|
|
119
141
|
const cells = line
|
|
120
142
|
.split("|")
|
|
121
143
|
.slice(1, -1)
|
|
122
144
|
.map((c) => c.trim());
|
|
123
145
|
if (cells.every((c) => /^:?-+:?$/.test(c))) {
|
|
124
146
|
i += 1;
|
|
125
|
-
continue;
|
|
147
|
+
continue;
|
|
126
148
|
}
|
|
127
149
|
outLines.push(` ${cells.map((c) => styleInline(c)).join(color(ANSI.dim, " · "))}`);
|
|
128
150
|
i += 1;
|
|
@@ -139,7 +161,6 @@ function renderMarkdown(source) {
|
|
|
139
161
|
i += 1;
|
|
140
162
|
}
|
|
141
163
|
|
|
142
|
-
// Trim trailing blank lines
|
|
143
164
|
while (outLines.length && outLines[outLines.length - 1] === "") outLines.pop();
|
|
144
165
|
return outLines.join("\n");
|
|
145
166
|
}
|
|
@@ -149,8 +170,70 @@ function printAnswer(text) {
|
|
|
149
170
|
out(color(ANSI.dim, "(empty)"));
|
|
150
171
|
return;
|
|
151
172
|
}
|
|
152
|
-
|
|
153
|
-
|
|
173
|
+
process.stdout.write(`${renderMarkdown(text)}\n`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function sleep(ms) {
|
|
177
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Split so ANSI escapes stay intact while typewriting. */
|
|
181
|
+
function ansiChunks(text) {
|
|
182
|
+
const parts = [];
|
|
183
|
+
const re = /\x1b\[[0-9;]*m|[^\x1b]+/g;
|
|
184
|
+
let match;
|
|
185
|
+
while ((match = re.exec(String(text)))) parts.push(match[0]);
|
|
186
|
+
return parts;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Stream clean markdown to the terminal. Cache hits use a very high CPS so
|
|
191
|
+
* the replay feels snappy instead of dumping raw markdown source.
|
|
192
|
+
*/
|
|
193
|
+
async function streamAnswer(text, opts = {}) {
|
|
194
|
+
if (!text) {
|
|
195
|
+
out(color(ANSI.dim, "(empty)"));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const rendered = renderMarkdown(text);
|
|
199
|
+
if (!ANSI_OK) {
|
|
200
|
+
process.stdout.write(`${rendered}\n`);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const cacheHit = Boolean(opts.cacheHit);
|
|
204
|
+
const cps = cacheHit ? 16_000 : opts.fast ? 10_000 : 6_000;
|
|
205
|
+
const chunkSize = Math.max(12, Math.floor(cps / 90));
|
|
206
|
+
let pending = "";
|
|
207
|
+
|
|
208
|
+
for (const part of ansiChunks(rendered)) {
|
|
209
|
+
if (part.startsWith("\x1b")) {
|
|
210
|
+
process.stdout.write(part);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
for (let i = 0; i < part.length; ) {
|
|
214
|
+
const slice = part.slice(i, i + chunkSize);
|
|
215
|
+
pending += slice;
|
|
216
|
+
i += slice.length;
|
|
217
|
+
if (pending.length >= chunkSize || slice.includes("\n")) {
|
|
218
|
+
process.stdout.write(pending);
|
|
219
|
+
pending = "";
|
|
220
|
+
await sleep(Math.max(3, Math.round((1000 * chunkSize) / cps)));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (pending) process.stdout.write(pending);
|
|
225
|
+
process.stdout.write("\n");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function isCacheHit(meta) {
|
|
229
|
+
if (!meta || typeof meta !== "object") return false;
|
|
230
|
+
return Boolean(
|
|
231
|
+
meta.exact_cache_hit ||
|
|
232
|
+
meta.prompt_cache_hit ||
|
|
233
|
+
meta.semantic_cache_hit ||
|
|
234
|
+
meta.prefix_cache_hit ||
|
|
235
|
+
meta.cache_hit,
|
|
236
|
+
);
|
|
154
237
|
}
|
|
155
238
|
|
|
156
239
|
const COMMANDS = [
|
|
@@ -462,7 +545,7 @@ async function complete(flags, config, messages) {
|
|
|
462
545
|
});
|
|
463
546
|
}
|
|
464
547
|
|
|
465
|
-
/** Stream a completion; buffers tokens
|
|
548
|
+
/** Stream a completion; buffers tokens (no raw dump), returns { text, result }. */
|
|
466
549
|
async function completeStream(flags, config, messages) {
|
|
467
550
|
const gatewayURL = gateway(flags, config);
|
|
468
551
|
const { apiKey, sessionId } = authFromConfig(flags, config);
|
|
@@ -500,6 +583,7 @@ async function completeStream(flags, config, messages) {
|
|
|
500
583
|
};
|
|
501
584
|
let spinTimer = null;
|
|
502
585
|
let spinFrame = 0;
|
|
586
|
+
const started = Date.now();
|
|
503
587
|
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
504
588
|
if (ANSI_OK) {
|
|
505
589
|
spinTimer = setInterval(() => {
|
|
@@ -542,7 +626,7 @@ async function completeStream(flags, config, messages) {
|
|
|
542
626
|
}
|
|
543
627
|
|
|
544
628
|
result.choices = [{ message: { role: "assistant", content: text } }];
|
|
545
|
-
return { text, result };
|
|
629
|
+
return { text, result, elapsedMs: Date.now() - started };
|
|
546
630
|
}
|
|
547
631
|
|
|
548
632
|
async function cmdLogin(flags) {
|
|
@@ -684,8 +768,14 @@ async function cmdChat(flags, positional) {
|
|
|
684
768
|
if (!prompt) die('Usage: promptimizer chat "What is 17 * 24?"');
|
|
685
769
|
out();
|
|
686
770
|
out(color(ANSI.magenta, "✦"));
|
|
687
|
-
const { text, result } = await completeStream(flags, config, [
|
|
688
|
-
|
|
771
|
+
const { text, result, elapsedMs } = await completeStream(flags, config, [
|
|
772
|
+
{ role: "user", content: prompt },
|
|
773
|
+
]);
|
|
774
|
+
const meta = result.promptimizer ?? {};
|
|
775
|
+
await streamAnswer(text, {
|
|
776
|
+
cacheHit: isCacheHit(meta),
|
|
777
|
+
fast: (elapsedMs ?? 0) < 500,
|
|
778
|
+
});
|
|
689
779
|
out();
|
|
690
780
|
printMeta(result);
|
|
691
781
|
out();
|
|
@@ -893,8 +983,12 @@ async function interactive(flags) {
|
|
|
893
983
|
out();
|
|
894
984
|
out(color(ANSI.magenta, "✦"));
|
|
895
985
|
try {
|
|
896
|
-
const { text, result } = await completeStream(flags, readConfig(), history);
|
|
897
|
-
|
|
986
|
+
const { text, result, elapsedMs } = await completeStream(flags, readConfig(), history);
|
|
987
|
+
const meta = result.promptimizer ?? {};
|
|
988
|
+
await streamAnswer(text, {
|
|
989
|
+
cacheHit: isCacheHit(meta),
|
|
990
|
+
fast: (elapsedMs ?? 0) < 500,
|
|
991
|
+
});
|
|
898
992
|
history.push({ role: "assistant", content: text });
|
|
899
993
|
out();
|
|
900
994
|
printMeta(result);
|