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.
Files changed (2) hide show
  1. package/bin/promptimizer.mjs +117 -23
  2. package/package.json +1 -1
@@ -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 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})`)}`);
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 = String(source).replace(/\r\n/g, "\n").split("\n");
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; // separator
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
- const rendered = ANSI_OK ? renderMarkdown(text) : text;
153
- process.stdout.write(`${rendered}\n`);
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 then returns { text, result }. */
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, [{ role: "user", content: prompt }]);
688
- printAnswer(text);
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
- printAnswer(text);
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promptimizer-cli",
3
- "version": "0.1.50",
3
+ "version": "0.1.52",
4
4
  "description": "Interactive Promptimizer CLI — Gemini-style REPL for quality-aware routing.",
5
5
  "type": "module",
6
6
  "bin": {