promptimizer-cli 0.1.48 → 0.1.50

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 +167 -29
  2. package/package.json +1 -1
@@ -26,6 +26,133 @@ 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
+
29
156
  const COMMANDS = [
30
157
  ["", "Start interactive session (Gemini-style REPL)"],
31
158
  ["login", "Save a Promptimizer API key"],
@@ -335,7 +462,7 @@ async function complete(flags, config, messages) {
335
462
  });
336
463
  }
337
464
 
338
- /** Stream a completion; writes tokens to stdout as they arrive. Returns { text, result }. */
465
+ /** Stream a completion; buffers tokens then returns { text, result }. */
339
466
  async function completeStream(flags, config, messages) {
340
467
  const gatewayURL = gateway(flags, config);
341
468
  const { apiKey, sessionId } = authFromConfig(flags, config);
@@ -371,34 +498,47 @@ async function completeStream(flags, config, messages) {
371
498
  promptimizer: undefined,
372
499
  choices: [{ message: { role: "assistant", content: "" } }],
373
500
  };
501
+ let spinTimer = null;
502
+ let spinFrame = 0;
503
+ const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
504
+ if (ANSI_OK) {
505
+ spinTimer = setInterval(() => {
506
+ const frame = frames[spinFrame++ % frames.length];
507
+ process.stdout.write(`\r${color(ANSI.dim, ` ${frame} routing…`)}`);
508
+ }, 80);
509
+ }
374
510
 
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);
511
+ try {
512
+ while (true) {
513
+ const { done, value } = await reader.read();
514
+ if (done) break;
515
+ buffer += decoder.decode(value, { stream: true });
516
+ const parts = buffer.split("\n");
517
+ buffer = parts.pop() ?? "";
518
+ for (const line of parts) {
519
+ const trimmed = line.trim();
520
+ if (!trimmed.startsWith("data:")) continue;
521
+ const data = trimmed.slice(5).trim();
522
+ if (!data) continue;
523
+ if (data === "[DONE]") continue;
524
+ try {
525
+ const parsed = JSON.parse(data);
526
+ if (parsed.error?.message) throw new Error(parsed.error.message);
527
+ const delta = parsed.choices?.[0]?.delta?.content;
528
+ if (delta) text += delta;
529
+ if (parsed.promptimizer) result.promptimizer = parsed.promptimizer;
530
+ if (parsed.usage) result.usage = parsed.usage;
531
+ if (parsed.model) result.model = parsed.model;
532
+ } catch (err) {
533
+ if (err instanceof Error && err.message && !err.message.includes("JSON")) throw err;
394
534
  }
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
535
  }
401
536
  }
537
+ } finally {
538
+ if (spinTimer) {
539
+ clearInterval(spinTimer);
540
+ process.stdout.write("\r\x1b[2K");
541
+ }
402
542
  }
403
543
 
404
544
  result.choices = [{ message: { role: "assistant", content: text } }];
@@ -545,8 +685,7 @@ async function cmdChat(flags, positional) {
545
685
  out();
546
686
  out(color(ANSI.magenta, "✦"));
547
687
  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");
688
+ printAnswer(text);
550
689
  out();
551
690
  printMeta(result);
552
691
  out();
@@ -755,8 +894,7 @@ async function interactive(flags) {
755
894
  out(color(ANSI.magenta, "✦"));
756
895
  try {
757
896
  const { text, result } = await completeStream(flags, readConfig(), history);
758
- if (!text) process.stdout.write(color(ANSI.dim, "(empty)"));
759
- process.stdout.write("\n");
897
+ printAnswer(text);
760
898
  history.push({ role: "assistant", content: text });
761
899
  out();
762
900
  printMeta(result);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promptimizer-cli",
3
- "version": "0.1.48",
3
+ "version": "0.1.50",
4
4
  "description": "Interactive Promptimizer CLI — Gemini-style REPL for quality-aware routing.",
5
5
  "type": "module",
6
6
  "bin": {