promptimizer-cli 0.1.17 → 0.1.18

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/README.md CHANGED
@@ -1,16 +1,32 @@
1
1
  # promptimizer-cli
2
2
 
3
+ Gemini-style interactive routing CLI for Promptimizer.
4
+
3
5
  ```bash
4
6
  npm install -g promptimizer-cli
5
- npx promptimizer-cli --help
6
- npx promptimizer-cli login --help
7
+ promptimizer
7
8
  ```
8
9
 
9
- ```bash
10
- promptimizer login --key pmz_live_…
11
- promptimizer connect baseten --key "$BASETEN_API_KEY"
12
- promptimizer chat "What is 17 * 24?"
13
- promptimizer savings
10
+ ```text
11
+ ██████╗ ███╗ ███╗███████╗
12
+
13
+ Promptimizer v0.1.17
14
+ Type a prompt, or /help /models /savings /clear /quit
15
+ › What is 17 * 24?
16
+
17
+ 408
18
+ ↳ thinkingmachines/inkling-small · economy · saved $0.0001
14
19
  ```
15
20
 
21
+ ## Commands
22
+
23
+ | Command | What it does |
24
+ | --- | --- |
25
+ | `promptimizer` | Interactive multi-turn session |
26
+ | `promptimizer login --key pmz_live_…` | Save API key |
27
+ | `promptimizer connect baseten --key $BASETEN_API_KEY` | Attach provider |
28
+ | `promptimizer chat "…"` | One-shot completion |
29
+ | `promptimizer models` | List fleet |
30
+ | `promptimizer savings` | Account ledger |
31
+
16
32
  Defaults to the hosted gateway. Override with `--url` or `PROMPTIMIZER_URL`.
@@ -1,18 +1,37 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import { createInterface } from "node:readline/promises";
3
4
  import { chmodSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
5
  import { homedir } from "node:os";
5
6
  import { dirname, join } from "node:path";
6
7
  import { fileURLToPath } from "node:url";
8
+ import { stdin as input, stdout as output } from "node:process";
7
9
 
8
10
  const DEFAULT_URL = process.env.PROMPTIMIZER_URL || "https://hackathon-omega-liart.vercel.app/api";
9
11
  const CONFIG_PATH = join(homedir(), ".promptimizer", "config.json");
10
12
 
13
+ const ANSI = {
14
+ reset: "\x1b[0m",
15
+ bold: "\x1b[1m",
16
+ dim: "\x1b[2m",
17
+ cyan: "\x1b[36m",
18
+ green: "\x1b[32m",
19
+ yellow: "\x1b[33m",
20
+ magenta: "\x1b[35m",
21
+ gray: "\x1b[90m",
22
+ blue: "\x1b[34m",
23
+ };
24
+
25
+ const color = process.stdout.isTTY
26
+ ? (code, text) => `${code}${text}${ANSI.reset}`
27
+ : (_code, text) => text;
28
+
11
29
  const COMMANDS = [
30
+ ["", "Start interactive session (Gemini-style REPL)"],
12
31
  ["login", "Save a Promptimizer API key"],
13
32
  ["logout", "Remove the saved key"],
14
33
  ["connect", "Attach a model provider"],
15
- ["chat", "Route a completion"],
34
+ ["chat", "Route one completion"],
16
35
  ["models", "List the connected fleet"],
17
36
  ["savings", "Show account savings"],
18
37
  ["providers", "List known provider URLs"],
@@ -24,10 +43,6 @@ const COMMAND_HELP = {
24
43
  " promptimizer login --key <pmz_live_...>",
25
44
  "",
26
45
  "Save a key from /account. Stored in ~/.promptimizer/config.json.",
27
- "",
28
- "Options",
29
- " --key, -k Promptimizer API key",
30
- " --url, -u Gateway URL",
31
46
  ],
32
47
  logout: ["Usage", " promptimizer logout", "", "Deletes ~/.promptimizer/config.json."],
33
48
  connect: [
@@ -35,54 +50,16 @@ const COMMAND_HELP = {
35
50
  " promptimizer connect <provider> --key <vendor-key>",
36
51
  " promptimizer connect custom --base-url <url> --key <vendor-key>",
37
52
  " promptimizer connect simulator",
38
- "",
39
- "Attach a provider to the signed-in account. Known hosts do not need --base-url.",
40
- "",
41
- "Options",
42
- " --key, -k Provider API key (or $BASETEN_API_KEY, $GROQ_API_KEY, …)",
43
- " --base-url Required for custom",
44
- " --pmz Promptimizer key (else the saved login)",
45
- " --url, -u Gateway URL",
46
53
  ],
47
54
  chat: [
48
55
  "Usage",
49
- ' promptimizer chat "<prompt>"',
50
- "",
51
- "Route one completion through the connected provider.",
52
- "",
53
- "Options",
54
- " --prompt Prompt text",
55
- " --pmz Promptimizer key (else the saved login)",
56
- " --url, -u Gateway URL",
57
- ],
58
- models: [
59
- "Usage",
60
- " promptimizer models",
61
- "",
62
- "List chat models on the current session.",
63
- "",
64
- "Options",
65
- " --url, -u Gateway URL",
66
- ],
67
- savings: [
68
- "Usage",
69
- " promptimizer savings",
56
+ ' promptimizer chat "What is 17 * 24?"',
70
57
  "",
71
- "Print routed spend versus the frontier baseline.",
72
- "",
73
- "Options",
74
- " --key, -k Promptimizer API key",
75
- " --url, -u Gateway URL",
76
- ],
77
- providers: [
78
- "Usage",
79
- " promptimizer providers",
80
- "",
81
- "Print known provider ids and base URLs.",
82
- "",
83
- "Options",
84
- " --url, -u Gateway URL",
58
+ "One-shot. Prefer bare `promptimizer` for a multi-turn session.",
85
59
  ],
60
+ models: ["Usage", " promptimizer models"],
61
+ savings: ["Usage", " promptimizer savings"],
62
+ providers: ["Usage", " promptimizer providers"],
86
63
  };
87
64
 
88
65
  function die(message, code = 1) {
@@ -149,23 +126,55 @@ function usd(value) {
149
126
  return Math.abs(n) >= 1 ? `$${n.toFixed(2)}` : `$${n.toFixed(4)}`;
150
127
  }
151
128
 
129
+ function printVersion() {
130
+ const here = dirname(fileURLToPath(import.meta.url));
131
+ const pkg = JSON.parse(readFileSync(join(here, "../package.json"), "utf8"));
132
+ out(pkg.version);
133
+ return pkg.version;
134
+ }
135
+
136
+ function banner(session, version) {
137
+ const label = session?.label || "not connected";
138
+ const models = session?.models?.length ?? 0;
139
+ const baseline = session?.baseline_model || "—";
140
+ out();
141
+ out(color(ANSI.cyan, " ██████╗ ███╗ ███╗███████╗"));
142
+ out(color(ANSI.cyan, " ██╔══██╗████╗ ████║╚══███╔╝"));
143
+ out(color(ANSI.cyan, " ██████╔╝██╔████╔██║ ███╔╝ "));
144
+ out(color(ANSI.cyan, " ██╔═══╝ ██║╚██╔╝██║ ███╔╝ "));
145
+ out(color(ANSI.cyan, " ██║ ██║ ╚═╝ ██║███████╗"));
146
+ out(color(ANSI.cyan, " ╚═╝ ╚═╝ ╚═╝╚══════╝"));
147
+ out();
148
+ out(` ${color(ANSI.bold, "Promptimizer")} ${color(ANSI.dim, `v${version}`)}`);
149
+ out(` ${color(ANSI.dim, "Quality-aware routing · OpenAI-compatible")}`);
150
+ out();
151
+ out(` ${color(ANSI.green, "●")} ${label}${models ? ` · ${models} models` : ""}`);
152
+ out(` ${color(ANSI.dim, `baseline ${baseline}`)}`);
153
+ out();
154
+ out(` ${color(ANSI.dim, "Type a prompt, or /help /models /savings /clear /quit")}`);
155
+ out();
156
+ }
157
+
152
158
  function help() {
153
- const width = Math.max(...COMMANDS.map(([name]) => name.length));
154
- out("Usage: promptimizer [--url <gateway>] <command> [options]");
159
+ out("Usage: promptimizer [--url <gateway>] [command] [options]");
155
160
  out();
156
- out("Route prompts through Promptimizer. Create a key at /account.");
161
+ out(" promptimizer Interactive session (REPL)");
162
+ out(' promptimizer chat "…" One-shot completion');
157
163
  out();
158
164
  out("Commands");
159
- for (const [name, desc] of COMMANDS) out(` ${name.padEnd(width + 2)}${desc}`);
165
+ const width = Math.max(...COMMANDS.map(([name]) => name.length || 1));
166
+ for (const [name, desc] of COMMANDS) {
167
+ const label = name || "(default)";
168
+ out(` ${label.padEnd(width + 4)}${desc}`);
169
+ }
160
170
  out();
161
171
  out("Global options");
162
172
  out(" --url, -u Gateway URL (default: hosted app, or $PROMPTIMIZER_URL)");
163
173
  out(" --help, -h Show help");
164
174
  out(" --version, -v Print version");
165
175
  out();
166
- out("Run `promptimizer <command> --help` for command flags.");
167
- out();
168
176
  out("Examples");
177
+ out(" promptimizer");
169
178
  out(" promptimizer login --key pmz_live_…");
170
179
  out(" promptimizer connect baseten --key $BASETEN_API_KEY");
171
180
  out(' promptimizer chat "What is 17 * 24?"');
@@ -197,7 +206,7 @@ async function request(path, { method = "GET", body, apiKey, sessionId, gatewayU
197
206
  if (!response.ok) {
198
207
  const detail =
199
208
  typeof data === "object" && data && "detail" in data ? String(data.detail) : response.statusText;
200
- die(detail);
209
+ throw Object.assign(new Error(detail), { status: response.status });
201
210
  }
202
211
  return data;
203
212
  }
@@ -213,6 +222,44 @@ function requireKey(flags, config) {
213
222
  return String(apiKey);
214
223
  }
215
224
 
225
+ function authFromConfig(flags, config) {
226
+ const apiKey = flags.pmz || process.env.PROMPTIMIZER_API_KEY || config.apiKey;
227
+ const sessionId = apiKey ? undefined : config.sessionId;
228
+ if (!apiKey && !sessionId) {
229
+ throw new Error("Not signed in. Run promptimizer login --key pmz_live_…");
230
+ }
231
+ return { apiKey, sessionId };
232
+ }
233
+
234
+ async function loadSession(flags, config) {
235
+ const gatewayURL = gateway(flags, config);
236
+ const { apiKey, sessionId } = authFromConfig(flags, config);
237
+ return request("/v1/session", { gatewayURL, apiKey, sessionId });
238
+ }
239
+
240
+ function printMeta(result) {
241
+ const meta = result.promptimizer ?? {};
242
+ const saved = result.usage?.cost?.saved_usd;
243
+ const bits = [meta.model || result.model, meta.tier].filter(Boolean);
244
+ if (saved != null) bits.push(`saved ${usd(saved)}`);
245
+ if (meta.cache_hit) bits.push("cache");
246
+ if (meta.escalated) bits.push("escalated");
247
+ if (meta.latency_ms != null) bits.push(`${Math.round(Number(meta.latency_ms))}ms`);
248
+ out(color(ANSI.dim, ` ↳ ${bits.join(" · ")}`));
249
+ }
250
+
251
+ async function complete(flags, config, messages) {
252
+ const gatewayURL = gateway(flags, config);
253
+ const { apiKey, sessionId } = authFromConfig(flags, config);
254
+ return request("/v1/chat/completions", {
255
+ method: "POST",
256
+ gatewayURL,
257
+ apiKey,
258
+ sessionId,
259
+ body: { messages },
260
+ });
261
+ }
262
+
216
263
  async function cmdLogin(flags) {
217
264
  const apiKey = flags.key || flags.k || process.env.PROMPTIMIZER_API_KEY;
218
265
  if (!apiKey) die("Missing --key. Create one at /account.");
@@ -220,12 +267,12 @@ async function cmdLogin(flags) {
220
267
  const gatewayURL = gateway(flags, config);
221
268
  await request("/v1/session", { apiKey, gatewayURL });
222
269
  writeConfig({ ...config, gatewayURL, apiKey });
223
- out(`Saved ${gatewayURL}`);
270
+ out(`${color(ANSI.green, "✓")} Saved ${gatewayURL}`);
224
271
  }
225
272
 
226
273
  function cmdLogout() {
227
274
  rmSync(CONFIG_PATH, { force: true });
228
- out("Forgot saved key.");
275
+ out(`${color(ANSI.green, "✓")} Forgot saved key.`);
229
276
  }
230
277
 
231
278
  async function cmdProviders(flags) {
@@ -245,7 +292,9 @@ async function cmdConnect(flags, positional) {
245
292
  const gatewayURL = gateway(flags, config);
246
293
  const provider = String(flags.provider || positional[0] || "").trim();
247
294
  const baseURL = flags["base-url"] || flags.baseUrl;
248
- if (!provider && !baseURL) die("Usage: promptimizer connect <provider>\n promptimizer connect custom --base-url https://…");
295
+ if (!provider && !baseURL) {
296
+ die("Usage: promptimizer connect <provider>\n promptimizer connect custom --base-url https://…");
297
+ }
249
298
 
250
299
  const mock = provider === "simulator" || provider === "mock";
251
300
  let vendorKey = flags.key || flags.k;
@@ -279,53 +328,34 @@ async function cmdConnect(flags, positional) {
279
328
  });
280
329
 
281
330
  writeConfig({ ...config, gatewayURL, apiKey, sessionId: session.session_id });
282
- out(`${session.label} ${session.base_url}`);
283
- out(`${session.models.length} models · baseline ${session.baseline_model}`);
331
+ out(`${color(ANSI.green, "✓")} ${session.label} ${session.base_url}`);
332
+ out(` ${session.models.length} models · baseline ${session.baseline_model}`);
284
333
  }
285
334
 
286
335
  async function cmdChat(flags, positional) {
287
336
  const config = readConfig();
288
337
  const prompt = String(flags.prompt || positional.join(" ")).trim();
289
338
  if (!prompt) die('Usage: promptimizer chat "What is 17 * 24?"');
290
- const gatewayURL = gateway(flags, config);
291
- const apiKey = flags.pmz || process.env.PROMPTIMIZER_API_KEY || config.apiKey;
292
- const sessionId = apiKey ? undefined : config.sessionId;
293
- if (!apiKey && !sessionId) die("Run promptimizer login or promptimizer connect first.");
294
-
295
- const result = await request("/v1/chat/completions", {
296
- method: "POST",
297
- gatewayURL,
298
- apiKey,
299
- sessionId,
300
- body: { messages: [{ role: "user", content: prompt }] },
301
- });
339
+ const result = await complete(flags, config, [{ role: "user", content: prompt }]);
302
340
  const text = result.choices?.[0]?.message?.content?.trim() ?? "";
303
- const meta = result.promptimizer ?? {};
304
- const saved = result.usage?.cost?.saved_usd;
305
341
  out();
306
342
  out(text);
307
343
  out();
308
- const bits = [meta.model || result.model, meta.tier].filter(Boolean);
309
- if (saved != null) bits.push(`saved ${usd(saved)}`);
310
- if (meta.cache_hit) bits.push("cache");
311
- if (meta.escalated) bits.push("escalated");
312
- out(bits.join(" · "));
344
+ printMeta(result);
313
345
  out();
314
346
  }
315
347
 
316
348
  async function cmdModels(flags) {
317
349
  const config = readConfig();
318
350
  const gatewayURL = gateway(flags, config);
319
- const apiKey = process.env.PROMPTIMIZER_API_KEY || config.apiKey;
320
- const sessionId = apiKey ? undefined : config.sessionId;
321
- if (!apiKey && !sessionId) die("Run promptimizer login or promptimizer connect first.");
351
+ const { apiKey, sessionId } = authFromConfig(flags, config);
322
352
  const data = await request("/v1/models", { gatewayURL, apiKey, sessionId });
323
353
  const models = data.data ?? [];
324
354
  const width = Math.max(8, ...models.map((model) => String(model.tier).length));
325
355
  out();
326
356
  for (const model of models) {
327
- const mark = model.id === data.baseline_model ? " baseline" : "";
328
- out(` ${String(model.tier).padEnd(width + 2)}${model.id}${mark}`);
357
+ const mark = model.id === data.baseline_model ? color(ANSI.yellow, " baseline") : "";
358
+ out(` ${color(ANSI.dim, String(model.tier).padEnd(width + 2))}${model.id}${mark}`);
329
359
  }
330
360
  out();
331
361
  }
@@ -335,7 +365,7 @@ async function cmdSavings(flags) {
335
365
  const apiKey = requireKey(flags, config);
336
366
  const data = await request("/v1/savings", { gatewayURL: gateway(flags, config), apiKey });
337
367
  out();
338
- out(`${usd(data.saved_usd)} saved`);
368
+ out(`${color(ANSI.bold, usd(data.saved_usd))} saved ${color(ANSI.dim, `(${Number(data.saved_pct || 0).toFixed(1)}%)`)}`);
339
369
  out();
340
370
  out(` routed ${usd(data.actual_usd)}`);
341
371
  out(` baseline ${usd(data.baseline_usd)}`);
@@ -345,10 +375,137 @@ async function cmdSavings(flags) {
345
375
  out();
346
376
  }
347
377
 
348
- function printVersion() {
349
- const here = dirname(fileURLToPath(import.meta.url));
350
- const pkg = JSON.parse(readFileSync(join(here, "../package.json"), "utf8"));
351
- out(pkg.version);
378
+ async function interactive(flags) {
379
+ if (!input.isTTY || !output.isTTY) {
380
+ help();
381
+ return;
382
+ }
383
+
384
+ const config = readConfig();
385
+ const version = (() => {
386
+ try {
387
+ const here = dirname(fileURLToPath(import.meta.url));
388
+ return JSON.parse(readFileSync(join(here, "../package.json"), "utf8")).version;
389
+ } catch {
390
+ return "0.0.0";
391
+ }
392
+ })();
393
+
394
+ let session = null;
395
+ try {
396
+ session = await loadSession(flags, config);
397
+ } catch (error) {
398
+ banner(null, version);
399
+ out(color(ANSI.yellow, ` ${error instanceof Error ? error.message : String(error)}`));
400
+ out(color(ANSI.dim, " Run: promptimizer login --key pmz_live_…"));
401
+ out();
402
+ return;
403
+ }
404
+
405
+ banner(session, version);
406
+
407
+ const rl = createInterface({ input, output, terminal: true });
408
+ const history = [];
409
+
410
+ const slashHelp = () => {
411
+ out();
412
+ out(` ${color(ANSI.bold, "/help")} this list`);
413
+ out(` ${color(ANSI.bold, "/models")} fleet + tiers`);
414
+ out(` ${color(ANSI.bold, "/savings")} account ledger`);
415
+ out(` ${color(ANSI.bold, "/session")} provider status`);
416
+ out(` ${color(ANSI.bold, "/clear")} clear chat history`);
417
+ out(` ${color(ANSI.bold, "/quit")} exit`);
418
+ out();
419
+ };
420
+
421
+ try {
422
+ while (true) {
423
+ let line;
424
+ try {
425
+ line = await rl.question(color(ANSI.cyan, "› "));
426
+ } catch {
427
+ break;
428
+ }
429
+ const trimmed = line.trim();
430
+ if (!trimmed) continue;
431
+
432
+ if (trimmed === "/quit" || trimmed === "/exit" || trimmed === "/q") break;
433
+
434
+ if (trimmed === "/help" || trimmed === "/?") {
435
+ slashHelp();
436
+ continue;
437
+ }
438
+
439
+ if (trimmed === "/clear") {
440
+ history.length = 0;
441
+ out(color(ANSI.dim, " History cleared."));
442
+ out();
443
+ continue;
444
+ }
445
+
446
+ if (trimmed === "/session") {
447
+ try {
448
+ session = await loadSession(flags, readConfig());
449
+ out();
450
+ out(` ${session.label} · ${session.mode}`);
451
+ out(` ${session.base_url}`);
452
+ out(` ${session.models.length} models · baseline ${session.baseline_model}`);
453
+ out();
454
+ } catch (error) {
455
+ out(color(ANSI.yellow, ` ${error instanceof Error ? error.message : String(error)}`));
456
+ }
457
+ continue;
458
+ }
459
+
460
+ if (trimmed === "/models") {
461
+ try {
462
+ await cmdModels(flags);
463
+ } catch (error) {
464
+ out(color(ANSI.yellow, ` ${error instanceof Error ? error.message : String(error)}`));
465
+ }
466
+ continue;
467
+ }
468
+
469
+ if (trimmed === "/savings") {
470
+ try {
471
+ await cmdSavings(flags);
472
+ } catch (error) {
473
+ out(color(ANSI.yellow, ` ${error instanceof Error ? error.message : String(error)}`));
474
+ }
475
+ continue;
476
+ }
477
+
478
+ if (trimmed.startsWith("/")) {
479
+ out(color(ANSI.dim, " Unknown command. Try /help"));
480
+ continue;
481
+ }
482
+
483
+ history.push({ role: "user", content: trimmed });
484
+ process.stdout.write(color(ANSI.dim, " … routing\r"));
485
+ try {
486
+ const result = await complete(flags, readConfig(), history);
487
+ process.stdout.write(" \r");
488
+ const text = result.choices?.[0]?.message?.content?.trim() ?? "";
489
+ history.push({ role: "assistant", content: text });
490
+ out();
491
+ out(color(ANSI.magenta, "✦"));
492
+ out(text);
493
+ out();
494
+ printMeta(result);
495
+ out();
496
+ } catch (error) {
497
+ process.stdout.write(" \r");
498
+ history.pop();
499
+ out(color(ANSI.yellow, ` ${error instanceof Error ? error.message : String(error)}`));
500
+ out();
501
+ }
502
+ }
503
+ } finally {
504
+ rl.close();
505
+ out();
506
+ out(color(ANSI.dim, " bye"));
507
+ out();
508
+ }
352
509
  }
353
510
 
354
511
  async function main() {
@@ -362,23 +519,29 @@ async function main() {
362
519
  else help();
363
520
  return;
364
521
  }
365
- if (positional.length === 0) {
366
- help();
522
+ if (flags.help || flags.h) {
523
+ if (positional[0]) commandHelp(positional[0]);
524
+ else help();
367
525
  return;
368
526
  }
369
527
 
528
+ if (positional.length === 0) {
529
+ return interactive(flags);
530
+ }
531
+
370
532
  const [command, ...rest] = positional;
371
- if (flags.help || flags.h) {
372
- commandHelp(command);
373
- return;
533
+ try {
534
+ if (command === "login") return await cmdLogin(flags);
535
+ if (command === "logout") return cmdLogout();
536
+ if (command === "providers") return await cmdProviders(flags);
537
+ if (command === "connect") return await cmdConnect(flags, rest);
538
+ if (command === "chat") return await cmdChat(flags, rest);
539
+ if (command === "models") return await cmdModels(flags);
540
+ if (command === "savings") return await cmdSavings(flags);
541
+ if (command === "repl" || command === "i") return await interactive(flags);
542
+ } catch (error) {
543
+ die(error instanceof Error ? error.message : String(error));
374
544
  }
375
- if (command === "login") return cmdLogin(flags);
376
- if (command === "logout") return cmdLogout();
377
- if (command === "providers") return cmdProviders(flags);
378
- if (command === "connect") return cmdConnect(flags, rest);
379
- if (command === "chat") return cmdChat(flags, rest);
380
- if (command === "models") return cmdModels(flags);
381
- if (command === "savings") return cmdSavings(flags);
382
545
  die(`Unknown command "${command}". Run promptimizer --help.`);
383
546
  }
384
547
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "promptimizer-cli",
3
- "version": "0.1.17",
4
- "description": "Login, connect a provider, route prompts, and read savings.",
3
+ "version": "0.1.18",
4
+ "description": "Interactive Promptimizer CLI Gemini-style REPL for quality-aware routing.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "promptimizer": "./bin/promptimizer.mjs"