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.
Files changed (2) hide show
  1. package/bin/promptimizer.mjs +249 -32
  2. package/package.json +1 -1
@@ -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; writes tokens to stdout as they arrive. Returns { text, result }. */
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
- while (true) {
376
- const { done, value } = await reader.read();
377
- if (done) break;
378
- buffer += decoder.decode(value, { stream: true });
379
- const parts = buffer.split("\n");
380
- buffer = parts.pop() ?? "";
381
- for (const line of parts) {
382
- const trimmed = line.trim();
383
- if (!trimmed.startsWith("data:")) continue;
384
- const data = trimmed.slice(5).trim();
385
- if (!data) continue;
386
- if (data === "[DONE]") continue;
387
- try {
388
- const parsed = JSON.parse(data);
389
- if (parsed.error?.message) throw new Error(parsed.error.message);
390
- const delta = parsed.choices?.[0]?.delta?.content;
391
- if (delta) {
392
- text += delta;
393
- process.stdout.write(delta);
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, [{ role: "user", content: prompt }]);
548
- if (!text) process.stdout.write(color(ANSI.dim, "(empty)"));
549
- process.stdout.write("\n");
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
- if (!text) process.stdout.write(color(ANSI.dim, "(empty)"));
759
- process.stdout.write("\n");
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promptimizer-cli",
3
- "version": "0.1.49",
3
+ "version": "0.1.51",
4
4
  "description": "Interactive Promptimizer CLI — Gemini-style REPL for quality-aware routing.",
5
5
  "type": "module",
6
6
  "bin": {