promptimizer-cli 0.1.51 → 0.1.53

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.
@@ -7,7 +7,11 @@ import { dirname, join } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { stdin as input, stdout as output } from "node:process";
9
9
 
10
- const DEFAULT_URL = process.env.PROMPTIMIZER_URL || "https://hackathon-omega-liart.vercel.app/api";
10
+ const DEFAULT_URL =
11
+ process.env.PROMPTIMIZER_URL ||
12
+ (process.env.NEXT_PUBLIC_SITE_URL
13
+ ? `${String(process.env.NEXT_PUBLIC_SITE_URL).replace(/\/$/, "")}/api`
14
+ : "https://hackathon-omega-liart.vercel.app/api");
11
15
  const CONFIG_PATH = join(homedir(), ".promptimizer", "config.json");
12
16
 
13
17
  const ANSI = {
@@ -28,16 +32,39 @@ const color = process.stdout.isTTY
28
32
 
29
33
  const ANSI_OK = Boolean(process.stdout.isTTY);
30
34
 
31
- /** Inline markdown → ANSI (bold, italic, code, links). */
35
+ /** Inline markdown → ANSI (bold, italic, code, links). Strips markers even without a TTY. */
32
36
  function styleInline(text) {
33
37
  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})`)}`);
38
+ // Protect code spans first
39
+ const codes = [];
40
+ s = s.replace(/`([^`]+)`/g, (_, code) => {
41
+ const token = `\u0000C${codes.length}\u0000`;
42
+ codes.push(color(ANSI.cyan, code));
43
+ return token;
44
+ });
45
+ // Bold before italic so ** wins over *
46
+ s = s.replace(/\*\*((?:[^*]|\*(?!\*))+?)\*\*/g, (_, t) => color(ANSI.bold, t));
47
+ s = s.replace(/__((?:[^_]|_(?!_))+?)__/g, (_, t) => color(ANSI.bold, t));
48
+ s = s.replace(/(?<!\*)\*([^*\n]+?)\*(?!\*)/g, (_, t) => color(ANSI.dim, t));
49
+ s = s.replace(/(?<!_)_([^_\n]+?)_(?!_)/g, (_, t) => color(ANSI.dim, t));
50
+ s = s.replace(
51
+ /\[([^\]]+)\]\(([^)]+)\)/g,
52
+ (_, label, url) => `${color(ANSI.blue, label)} ${color(ANSI.dim, `(${url})`)}`,
53
+ );
54
+ // Restore code
55
+ s = s.replace(/\u0000C(\d+)\u0000/g, (_, i) => codes[Number(i)] ?? "");
56
+ return s;
57
+ }
58
+
59
+ /** Soft-normalize messy model markdown before line parsing. */
60
+ function normalizeMarkdownSource(source) {
61
+ let s = String(source).replace(/\r\n/g, "\n");
62
+ // Break glued heading markers: "---### Title" / "text### Title"
63
+ s = s.replace(/([^\n#])(#{1,6}\s)/g, "$1\n$2");
64
+ // Break glued thematic break before heading: "---###"
65
+ s = s.replace(/(-{3,}|_{3,}|\*{3,})(#{1,6}\s)/g, "$1\n$2");
66
+ // Ensure list markers start on their own line when jammed after text
67
+ s = s.replace(/([.:;!?])\s*([-*+]\s+\*\*)/g, "$1\n$2");
41
68
  return s;
42
69
  }
43
70
 
@@ -47,7 +74,7 @@ function styleInline(text) {
47
74
  */
48
75
  function renderMarkdown(source) {
49
76
  if (!source) return "";
50
- const lines = String(source).replace(/\r\n/g, "\n").split("\n");
77
+ const lines = normalizeMarkdownSource(source).split("\n");
51
78
  const outLines = [];
52
79
  let i = 0;
53
80
  let inFence = false;
@@ -56,10 +83,10 @@ function renderMarkdown(source) {
56
83
  while (i < lines.length) {
57
84
  const line = lines[i];
58
85
 
59
- if (line.startsWith("```")) {
86
+ if (line.trimStart().startsWith("```")) {
60
87
  if (!inFence) {
61
88
  inFence = true;
62
- fenceLang = line.slice(3).trim();
89
+ fenceLang = line.trimStart().slice(3).trim();
63
90
  outLines.push(color(ANSI.dim, fenceLang ? `┌─ ${fenceLang}` : "┌─"));
64
91
  } else {
65
92
  inFence = false;
@@ -80,7 +107,7 @@ function renderMarkdown(source) {
80
107
  const level = line.match(/^#+/)[0].length;
81
108
  const title = line.replace(/^#{1,6}\s+/, "");
82
109
  const styled = styleInline(title);
83
- outLines.push(level <= 2 ? color(ANSI.bold, styled) : styled);
110
+ outLines.push(level <= 2 ? color(ANSI.bold, styled) : color(ANSI.bold, styled));
84
111
  i += 1;
85
112
  continue;
86
113
  }
@@ -115,14 +142,13 @@ function renderMarkdown(source) {
115
142
  }
116
143
 
117
144
  if (line.includes("|") && line.trim().startsWith("|")) {
118
- // simple table row — strip pipes, pad lightly
119
145
  const cells = line
120
146
  .split("|")
121
147
  .slice(1, -1)
122
148
  .map((c) => c.trim());
123
149
  if (cells.every((c) => /^:?-+:?$/.test(c))) {
124
150
  i += 1;
125
- continue; // separator
151
+ continue;
126
152
  }
127
153
  outLines.push(` ${cells.map((c) => styleInline(c)).join(color(ANSI.dim, " · "))}`);
128
154
  i += 1;
@@ -139,7 +165,6 @@ function renderMarkdown(source) {
139
165
  i += 1;
140
166
  }
141
167
 
142
- // Trim trailing blank lines
143
168
  while (outLines.length && outLines[outLines.length - 1] === "") outLines.pop();
144
169
  return outLines.join("\n");
145
170
  }
@@ -149,8 +174,7 @@ function printAnswer(text) {
149
174
  out(color(ANSI.dim, "(empty)"));
150
175
  return;
151
176
  }
152
- const rendered = ANSI_OK ? renderMarkdown(text) : text;
153
- process.stdout.write(`${rendered}\n`);
177
+ process.stdout.write(`${renderMarkdown(text)}\n`);
154
178
  }
155
179
 
156
180
  function sleep(ms) {
@@ -175,17 +199,15 @@ async function streamAnswer(text, opts = {}) {
175
199
  out(color(ANSI.dim, "(empty)"));
176
200
  return;
177
201
  }
202
+ const rendered = renderMarkdown(text);
178
203
  if (!ANSI_OK) {
179
- process.stdout.write(`${text}\n`);
204
+ process.stdout.write(`${rendered}\n`);
180
205
  return;
181
206
  }
182
- const rendered = renderMarkdown(text);
183
207
  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));
208
+ const cps = cacheHit ? 16_000 : opts.fast ? 10_000 : 6_000;
209
+ const chunkSize = Math.max(12, Math.floor(cps / 90));
187
210
  let pending = "";
188
- let visible = 0;
189
211
 
190
212
  for (const part of ansiChunks(rendered)) {
191
213
  if (part.startsWith("\x1b")) {
@@ -195,19 +217,16 @@ async function streamAnswer(text, opts = {}) {
195
217
  for (let i = 0; i < part.length; ) {
196
218
  const slice = part.slice(i, i + chunkSize);
197
219
  pending += slice;
198
- visible += slice.length;
199
220
  i += slice.length;
200
221
  if (pending.length >= chunkSize || slice.includes("\n")) {
201
222
  process.stdout.write(pending);
202
223
  pending = "";
203
- await sleep(Math.max(4, Math.round((1000 * chunkSize) / cps)));
224
+ await sleep(Math.max(3, Math.round((1000 * chunkSize) / cps)));
204
225
  }
205
226
  }
206
227
  }
207
228
  if (pending) process.stdout.write(pending);
208
229
  process.stdout.write("\n");
209
- // silence unused when tiny answers finish in one tick
210
- void visible;
211
230
  }
212
231
 
213
232
  function isCacheHit(meta) {
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "promptimizer-cli",
3
- "version": "0.1.51",
3
+ "version": "0.1.53",
4
4
  "description": "Interactive Promptimizer CLI — Gemini-style REPL for quality-aware routing.",
5
5
  "type": "module",
6
6
  "bin": {
7
- "promptimizer": "./bin/promptimizer.mjs"
7
+ "promptimizer": "bin/promptimizer.mjs"
8
8
  },
9
9
  "files": [
10
10
  "bin",