llmist 0.4.1 → 0.5.1

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/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  createGadget
4
- } from "./chunk-QVDGTUQN.js";
4
+ } from "./chunk-LKIBXQ5I.js";
5
5
  import {
6
6
  AgentBuilder,
7
7
  BaseGadget,
@@ -22,7 +22,7 @@ import {
22
22
  init_model_shortcuts,
23
23
  init_registry,
24
24
  resolveModel
25
- } from "./chunk-LQE7TKKW.js";
25
+ } from "./chunk-MH4TQ5AD.js";
26
26
 
27
27
  // src/cli/constants.ts
28
28
  var CLI_NAME = "llmist";
@@ -34,7 +34,7 @@ var COMMANDS = {
34
34
  };
35
35
  var LOG_LEVELS = ["silly", "trace", "debug", "info", "warn", "error", "fatal"];
36
36
  var DEFAULT_MODEL = "openai:gpt-5-nano";
37
- var DEFAULT_PARAMETER_FORMAT = "json";
37
+ var DEFAULT_PARAMETER_FORMAT = "toml";
38
38
  var OPTION_FLAGS = {
39
39
  model: "-m, --model <identifier>",
40
40
  systemPrompt: "-s, --system <prompt>",
@@ -55,7 +55,7 @@ var OPTION_DESCRIPTIONS = {
55
55
  maxTokens: "Maximum number of output tokens requested from the model.",
56
56
  maxIterations: "Maximum number of agent loop iterations before exiting.",
57
57
  gadgetModule: "Path or module specifier for a gadget export. Repeat to register multiple gadgets.",
58
- parameterFormat: "Format for gadget parameter schemas: 'json', 'yaml', or 'auto'.",
58
+ parameterFormat: "Format for gadget parameter schemas: 'json', 'yaml', 'toml', or 'auto'.",
59
59
  logLevel: "Log level: silly, trace, debug, info, warn, error, fatal.",
60
60
  logFile: "Path to log file. When set, logs are written to file instead of stderr.",
61
61
  noBuiltins: "Disable built-in gadgets (AskUser, TellUser).",
@@ -69,7 +69,7 @@ import { Command, InvalidArgumentError as InvalidArgumentError3 } from "commande
69
69
  // package.json
70
70
  var package_default = {
71
71
  name: "llmist",
72
- version: "0.4.0",
72
+ version: "0.5.0",
73
73
  description: "Universal TypeScript LLM client with streaming-first agent framework. Works with any model - no structured outputs or native tool calling required. Implements its own flexible grammar for function calling.",
74
74
  type: "module",
75
75
  main: "dist/index.cjs",
@@ -153,7 +153,10 @@ var package_default = {
153
153
  "@google/genai": "^1.27.0",
154
154
  chalk: "^5.6.2",
155
155
  commander: "^12.1.0",
156
+ "js-toml": "^1.0.2",
156
157
  "js-yaml": "^4.1.0",
158
+ marked: "^15.0.12",
159
+ "marked-terminal": "^7.3.0",
157
160
  openai: "^6.0.0",
158
161
  tiktoken: "^1.0.22",
159
162
  tslog: "^4.10.2",
@@ -166,6 +169,7 @@ var package_default = {
166
169
  "@semantic-release/changelog": "^6.0.3",
167
170
  "@semantic-release/git": "^10.0.1",
168
171
  "@types/js-yaml": "^4.0.9",
172
+ "@types/marked-terminal": "^6.1.1",
169
173
  "@types/node": "^20.12.7",
170
174
  "bun-types": "^1.3.2",
171
175
  dotenv: "^17.2.3",
@@ -181,7 +185,7 @@ init_builder();
181
185
  init_registry();
182
186
  init_constants();
183
187
  import { createInterface } from "node:readline/promises";
184
- import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
188
+ import chalk3 from "chalk";
185
189
 
186
190
  // src/cli/builtin-gadgets.ts
187
191
  import { z } from "zod";
@@ -190,8 +194,20 @@ var askUser = createGadget({
190
194
  name: "AskUser",
191
195
  description: "Ask the user a question when you need more information or clarification. The user's response will be provided back to you.",
192
196
  schema: z.object({
193
- question: z.string().describe("The question to ask the user")
197
+ question: z.string().describe("The question to ask the user in plain-text or Markdown")
194
198
  }),
199
+ examples: [
200
+ {
201
+ comment: "Ask for clarification about the task",
202
+ params: { question: "Which file would you like me to modify?" }
203
+ },
204
+ {
205
+ comment: "Ask user to choose between options",
206
+ params: {
207
+ question: "I found multiple matches. Which one should I use?\n- src/utils/helper.ts\n- src/lib/helper.ts"
208
+ }
209
+ }
210
+ ],
195
211
  execute: ({ question }) => {
196
212
  throw new HumanInputException(question);
197
213
  }
@@ -200,10 +216,28 @@ var tellUser = createGadget({
200
216
  name: "TellUser",
201
217
  description: "Tell the user something important. Set done=true when your work is complete and you want to end the conversation.",
202
218
  schema: z.object({
203
- message: z.string().describe("The message to display to the user"),
204
- done: z.boolean().describe("Set to true to end the conversation, false to continue"),
219
+ message: z.string().describe("The message to display to the user in Markdown"),
220
+ done: z.boolean().default(false).describe("Set to true to end the conversation, false to continue"),
205
221
  type: z.enum(["info", "success", "warning", "error"]).default("info").describe("Message type: info, success, warning, or error")
206
222
  }),
223
+ examples: [
224
+ {
225
+ comment: "Report successful completion and end the conversation",
226
+ params: {
227
+ message: "I've completed the refactoring. All tests pass.",
228
+ done: true,
229
+ type: "success"
230
+ }
231
+ },
232
+ {
233
+ comment: "Warn the user about something without ending",
234
+ params: {
235
+ message: "Found 3 files with potential issues. Continuing analysis...",
236
+ done: false,
237
+ type: "warning"
238
+ }
239
+ }
240
+ ],
207
241
  execute: ({ message, done, type }) => {
208
242
  const prefixes = {
209
243
  info: "\u2139\uFE0F ",
@@ -316,6 +350,9 @@ async function loadGadgets(specifiers, cwd, importer = (specifier) => import(spe
316
350
  return gadgets;
317
351
  }
318
352
 
353
+ // src/cli/option-helpers.ts
354
+ import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
355
+
319
356
  // src/cli/utils.ts
320
357
  init_constants();
321
358
  import chalk2 from "chalk";
@@ -323,6 +360,44 @@ import { InvalidArgumentError } from "commander";
323
360
 
324
361
  // src/cli/ui/formatters.ts
325
362
  import chalk from "chalk";
363
+ import { marked } from "marked";
364
+ import { markedTerminal } from "marked-terminal";
365
+ var markedConfigured = false;
366
+ function ensureMarkedConfigured() {
367
+ if (!markedConfigured) {
368
+ chalk.level = process.env.NO_COLOR ? 0 : 3;
369
+ marked.use(
370
+ markedTerminal({
371
+ // Text styling
372
+ strong: chalk.bold,
373
+ em: chalk.italic,
374
+ del: chalk.dim.gray.strikethrough,
375
+ // Code styling
376
+ code: chalk.yellow,
377
+ codespan: chalk.yellow,
378
+ // Headings
379
+ heading: chalk.green.bold,
380
+ firstHeading: chalk.magenta.underline.bold,
381
+ // Links
382
+ link: chalk.blue,
383
+ href: chalk.blue.underline,
384
+ // Block elements
385
+ blockquote: chalk.gray.italic,
386
+ // List formatting - reduce indentation and add bullet styling
387
+ tab: 2,
388
+ // Reduce from default 4 to 2 spaces
389
+ listitem: chalk.reset
390
+ // Keep items readable (no dim)
391
+ })
392
+ );
393
+ markedConfigured = true;
394
+ }
395
+ }
396
+ function renderMarkdown(text) {
397
+ ensureMarkedConfigured();
398
+ const rendered = marked.parse(text);
399
+ return rendered.trimEnd();
400
+ }
326
401
  function formatTokens(tokens) {
327
402
  return tokens >= 1e3 ? `${(tokens / 1e3).toFixed(1)}k` : `${tokens}`;
328
403
  }
@@ -341,7 +416,14 @@ function formatCost(cost) {
341
416
  function renderSummary(metadata) {
342
417
  const parts = [];
343
418
  if (metadata.iterations !== void 0) {
344
- parts.push(chalk.cyan(`#${metadata.iterations}`));
419
+ const iterPart = chalk.cyan(`#${metadata.iterations}`);
420
+ if (metadata.model) {
421
+ parts.push(`${iterPart} ${chalk.magenta(metadata.model)}`);
422
+ } else {
423
+ parts.push(iterPart);
424
+ }
425
+ } else if (metadata.model) {
426
+ parts.push(chalk.magenta(metadata.model));
345
427
  }
346
428
  if (metadata.usage) {
347
429
  const { inputTokens, outputTokens } = metadata.usage;
@@ -362,22 +444,128 @@ function renderSummary(metadata) {
362
444
  }
363
445
  return parts.join(chalk.dim(" | "));
364
446
  }
447
+ function renderOverallSummary(metadata) {
448
+ const parts = [];
449
+ if (metadata.totalTokens !== void 0 && metadata.totalTokens > 0) {
450
+ parts.push(chalk.dim("total:") + chalk.magenta(` ${formatTokens(metadata.totalTokens)}`));
451
+ }
452
+ if (metadata.iterations !== void 0 && metadata.iterations > 0) {
453
+ parts.push(chalk.cyan(`#${metadata.iterations}`));
454
+ }
455
+ if (metadata.elapsedSeconds !== void 0 && metadata.elapsedSeconds > 0) {
456
+ parts.push(chalk.dim(`${metadata.elapsedSeconds}s`));
457
+ }
458
+ if (metadata.cost !== void 0 && metadata.cost > 0) {
459
+ parts.push(chalk.cyan(`$${formatCost(metadata.cost)}`));
460
+ }
461
+ if (parts.length === 0) {
462
+ return null;
463
+ }
464
+ return parts.join(chalk.dim(" | "));
465
+ }
466
+ function formatParametersInline(params) {
467
+ if (!params || Object.keys(params).length === 0) {
468
+ return "";
469
+ }
470
+ return Object.entries(params).map(([key, value]) => {
471
+ let formatted;
472
+ if (typeof value === "string") {
473
+ formatted = value.length > 30 ? `${value.slice(0, 30)}\u2026` : value;
474
+ } else if (typeof value === "boolean" || typeof value === "number") {
475
+ formatted = String(value);
476
+ } else {
477
+ const json = JSON.stringify(value);
478
+ formatted = json.length > 30 ? `${json.slice(0, 30)}\u2026` : json;
479
+ }
480
+ return `${chalk.dim(key)}${chalk.dim("=")}${chalk.cyan(formatted)}`;
481
+ }).join(chalk.dim(", "));
482
+ }
483
+ function formatBytes(bytes) {
484
+ if (bytes < 1024) {
485
+ return `${bytes} bytes`;
486
+ }
487
+ if (bytes < 1024 * 1024) {
488
+ return `${(bytes / 1024).toFixed(1)} KB`;
489
+ }
490
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
491
+ }
365
492
  function formatGadgetSummary(result) {
366
493
  const gadgetLabel = chalk.magenta.bold(result.gadgetName);
367
494
  const timeLabel = chalk.dim(`${Math.round(result.executionTimeMs)}ms`);
495
+ const paramsStr = formatParametersInline(result.parameters);
496
+ const paramsLabel = paramsStr ? `${chalk.dim("(")}${paramsStr}${chalk.dim(")")}` : "";
368
497
  if (result.error) {
369
- return `${chalk.red("\u2717")} ${gadgetLabel} ${chalk.red("error:")} ${result.error} ${timeLabel}`;
498
+ const errorMsg = result.error.length > 50 ? `${result.error.slice(0, 50)}\u2026` : result.error;
499
+ return `${chalk.red("\u2717")} ${gadgetLabel}${paramsLabel} ${chalk.red("error:")} ${errorMsg} ${timeLabel}`;
500
+ }
501
+ let outputLabel;
502
+ if (result.tokenCount !== void 0 && result.tokenCount > 0) {
503
+ outputLabel = chalk.green(`${formatTokens(result.tokenCount)} tokens`);
504
+ } else if (result.result) {
505
+ const outputBytes = Buffer.byteLength(result.result, "utf-8");
506
+ outputLabel = outputBytes > 0 ? chalk.green(formatBytes(outputBytes)) : chalk.dim("no output");
507
+ } else {
508
+ outputLabel = chalk.dim("no output");
370
509
  }
371
- if (result.breaksLoop) {
372
- return `${chalk.yellow("\u23F9")} ${gadgetLabel} ${chalk.yellow("finished:")} ${result.result} ${timeLabel}`;
510
+ const icon = result.breaksLoop ? chalk.yellow("\u23F9") : chalk.green("\u2713");
511
+ const summaryLine = `${icon} ${gadgetLabel}${paramsLabel} ${chalk.dim("\u2192")} ${outputLabel} ${timeLabel}`;
512
+ if (result.gadgetName === "TellUser" && result.parameters?.message) {
513
+ const message = String(result.parameters.message);
514
+ const rendered = renderMarkdown(message);
515
+ return `${summaryLine}
516
+ ${rendered}`;
373
517
  }
374
- const maxLen = 80;
375
- const shouldTruncate = result.gadgetName !== "TellUser";
376
- const resultText = result.result ? shouldTruncate && result.result.length > maxLen ? `${result.result.slice(0, maxLen)}...` : result.result : "";
377
- return `${chalk.green("\u2713")} ${gadgetLabel} ${chalk.dim("\u2192")} ${resultText} ${timeLabel}`;
518
+ return summaryLine;
378
519
  }
379
520
 
380
521
  // src/cli/utils.ts
522
+ var RARE_EMOJI = [
523
+ "\u{1F531}",
524
+ "\u2697\uFE0F",
525
+ "\u{1F9FF}",
526
+ "\u{1F530}",
527
+ "\u269B\uFE0F",
528
+ "\u{1F3FA}",
529
+ "\u{1F9EB}",
530
+ "\u{1F52C}",
531
+ "\u2695\uFE0F",
532
+ "\u{1F5DD}\uFE0F",
533
+ "\u2696\uFE0F",
534
+ "\u{1F52E}",
535
+ "\u{1FAAC}",
536
+ "\u{1F9EC}",
537
+ "\u2699\uFE0F",
538
+ "\u{1F529}",
539
+ "\u{1FA9B}",
540
+ "\u26CF\uFE0F",
541
+ "\u{1FA83}",
542
+ "\u{1F3F9}",
543
+ "\u{1F6E1}\uFE0F",
544
+ "\u2694\uFE0F",
545
+ "\u{1F5E1}\uFE0F",
546
+ "\u{1FA93}",
547
+ "\u{1F5C3}\uFE0F",
548
+ "\u{1F4DC}",
549
+ "\u{1F4EF}",
550
+ "\u{1F3B4}",
551
+ "\u{1F004}",
552
+ "\u{1F3B2}"
553
+ ];
554
+ function generateMarkers() {
555
+ const pick = (count) => {
556
+ const result = [];
557
+ const pool = [...RARE_EMOJI];
558
+ for (let i = 0; i < count && pool.length > 0; i++) {
559
+ const idx = Math.floor(Math.random() * pool.length);
560
+ result.push(pool.splice(idx, 1)[0]);
561
+ }
562
+ return result.join("");
563
+ };
564
+ return {
565
+ startPrefix: pick(5),
566
+ endPrefix: pick(5)
567
+ };
568
+ }
381
569
  function createNumericParser({
382
570
  label,
383
571
  integer = false,
@@ -544,6 +732,13 @@ var StreamProgress = class {
544
732
  if (this.totalStartTime === 0) return 0;
545
733
  return Number(((Date.now() - this.totalStartTime) / 1e3).toFixed(1));
546
734
  }
735
+ /**
736
+ * Get elapsed time in seconds for the current call.
737
+ * @returns Elapsed time in seconds with 1 decimal place
738
+ */
739
+ getCallElapsedSeconds() {
740
+ return Number(((Date.now() - this.callStartTime) / 1e3).toFixed(1));
741
+ }
547
742
  /**
548
743
  * Starts the progress indicator animation after a brief delay.
549
744
  */
@@ -578,7 +773,12 @@ var StreamProgress = class {
578
773
  const elapsed = ((Date.now() - this.callStartTime) / 1e3).toFixed(1);
579
774
  const outTokens = this.callOutputTokensEstimated ? Math.round(this.callOutputChars / FALLBACK_CHARS_PER_TOKEN) : this.callOutputTokens;
580
775
  const parts = [];
581
- parts.push(chalk2.cyan(`#${this.currentIteration}`));
776
+ const iterPart = chalk2.cyan(`#${this.currentIteration}`);
777
+ if (this.model) {
778
+ parts.push(`${iterPart} ${chalk2.magenta(this.model)}`);
779
+ } else {
780
+ parts.push(iterPart);
781
+ }
582
782
  if (this.callInputTokens > 0) {
583
783
  const prefix = this.callInputTokensEstimated ? "~" : "";
584
784
  parts.push(chalk2.dim("\u2191") + chalk2.yellow(` ${prefix}${formatTokens(this.callInputTokens)}`));
@@ -591,7 +791,7 @@ var StreamProgress = class {
591
791
  if (this.totalCost > 0) {
592
792
  parts.push(chalk2.cyan(`$${formatCost(this.totalCost)}`));
593
793
  }
594
- this.target.write(`\r${chalk2.cyan(spinner)} ${parts.join(chalk2.dim(" | "))}`);
794
+ this.target.write(`\r${parts.join(chalk2.dim(" | "))} ${chalk2.cyan(spinner)}`);
595
795
  }
596
796
  renderCumulativeMode(spinner) {
597
797
  const elapsed = ((Date.now() - this.totalStartTime) / 1e3).toFixed(1);
@@ -609,7 +809,7 @@ var StreamProgress = class {
609
809
  parts.push(chalk2.dim("cost:") + chalk2.cyan(` $${formatCost(this.totalCost)}`));
610
810
  }
611
811
  parts.push(chalk2.dim(`${elapsed}s`));
612
- this.target.write(`\r${chalk2.cyan(spinner)} ${parts.join(chalk2.dim(" | "))}`);
812
+ this.target.write(`\r${parts.join(chalk2.dim(" | "))} ${chalk2.cyan(spinner)}`);
613
813
  }
614
814
  /**
615
815
  * Pauses the progress indicator and clears the line.
@@ -719,15 +919,91 @@ async function executeAction(action, env) {
719
919
  }
720
920
  }
721
921
 
722
- // src/cli/agent-command.ts
723
- var PARAMETER_FORMAT_VALUES = ["json", "yaml", "auto"];
922
+ // src/cli/option-helpers.ts
923
+ var PARAMETER_FORMAT_VALUES = ["json", "yaml", "toml", "auto"];
724
924
  function parseParameterFormat(value) {
725
925
  const normalized = value.toLowerCase();
726
926
  if (!PARAMETER_FORMAT_VALUES.includes(normalized)) {
727
- throw new InvalidArgumentError2("Parameter format must be one of 'json', 'yaml', or 'auto'.");
927
+ throw new InvalidArgumentError2(
928
+ `Parameter format must be one of: ${PARAMETER_FORMAT_VALUES.join(", ")}`
929
+ );
728
930
  }
729
931
  return normalized;
730
932
  }
933
+ function addCompleteOptions(cmd, defaults) {
934
+ return cmd.option(OPTION_FLAGS.model, OPTION_DESCRIPTIONS.model, defaults?.model ?? DEFAULT_MODEL).option(OPTION_FLAGS.systemPrompt, OPTION_DESCRIPTIONS.systemPrompt, defaults?.system).option(
935
+ OPTION_FLAGS.temperature,
936
+ OPTION_DESCRIPTIONS.temperature,
937
+ createNumericParser({ label: "Temperature", min: 0, max: 2 }),
938
+ defaults?.temperature
939
+ ).option(
940
+ OPTION_FLAGS.maxTokens,
941
+ OPTION_DESCRIPTIONS.maxTokens,
942
+ createNumericParser({ label: "Max tokens", integer: true, min: 1 }),
943
+ defaults?.["max-tokens"]
944
+ );
945
+ }
946
+ function addAgentOptions(cmd, defaults) {
947
+ const gadgetAccumulator = (value, previous = []) => [
948
+ ...previous,
949
+ value
950
+ ];
951
+ const defaultGadgets = defaults?.gadget ?? [];
952
+ return cmd.option(OPTION_FLAGS.model, OPTION_DESCRIPTIONS.model, defaults?.model ?? DEFAULT_MODEL).option(OPTION_FLAGS.systemPrompt, OPTION_DESCRIPTIONS.systemPrompt, defaults?.system).option(
953
+ OPTION_FLAGS.temperature,
954
+ OPTION_DESCRIPTIONS.temperature,
955
+ createNumericParser({ label: "Temperature", min: 0, max: 2 }),
956
+ defaults?.temperature
957
+ ).option(
958
+ OPTION_FLAGS.maxIterations,
959
+ OPTION_DESCRIPTIONS.maxIterations,
960
+ createNumericParser({ label: "Max iterations", integer: true, min: 1 }),
961
+ defaults?.["max-iterations"]
962
+ ).option(OPTION_FLAGS.gadgetModule, OPTION_DESCRIPTIONS.gadgetModule, gadgetAccumulator, [
963
+ ...defaultGadgets
964
+ ]).option(
965
+ OPTION_FLAGS.parameterFormat,
966
+ OPTION_DESCRIPTIONS.parameterFormat,
967
+ parseParameterFormat,
968
+ defaults?.["parameter-format"] ?? DEFAULT_PARAMETER_FORMAT
969
+ ).option(OPTION_FLAGS.noBuiltins, OPTION_DESCRIPTIONS.noBuiltins, defaults?.builtins !== false).option(
970
+ OPTION_FLAGS.noBuiltinInteraction,
971
+ OPTION_DESCRIPTIONS.noBuiltinInteraction,
972
+ defaults?.["builtin-interaction"] !== false
973
+ );
974
+ }
975
+ function configToCompleteOptions(config) {
976
+ const result = {};
977
+ if (config.model !== void 0) result.model = config.model;
978
+ if (config.system !== void 0) result.system = config.system;
979
+ if (config.temperature !== void 0) result.temperature = config.temperature;
980
+ if (config["max-tokens"] !== void 0) result.maxTokens = config["max-tokens"];
981
+ return result;
982
+ }
983
+ function configToAgentOptions(config) {
984
+ const result = {};
985
+ if (config.model !== void 0) result.model = config.model;
986
+ if (config.system !== void 0) result.system = config.system;
987
+ if (config.temperature !== void 0) result.temperature = config.temperature;
988
+ if (config["max-iterations"] !== void 0) result.maxIterations = config["max-iterations"];
989
+ if (config.gadget !== void 0) result.gadget = config.gadget;
990
+ if (config["parameter-format"] !== void 0) result.parameterFormat = config["parameter-format"];
991
+ if (config.builtins !== void 0) result.builtins = config.builtins;
992
+ if (config["builtin-interaction"] !== void 0)
993
+ result.builtinInteraction = config["builtin-interaction"];
994
+ return result;
995
+ }
996
+
997
+ // src/cli/agent-command.ts
998
+ async function promptApproval(env, prompt) {
999
+ const rl = createInterface({ input: env.stdin, output: env.stderr });
1000
+ try {
1001
+ const answer = await rl.question(prompt);
1002
+ return answer.toLowerCase().startsWith("y");
1003
+ } finally {
1004
+ rl.close();
1005
+ }
1006
+ }
731
1007
  function createHumanInputHandler(env, progress) {
732
1008
  const stdout = env.stdout;
733
1009
  if (!isInteractive(env.stdin) || typeof stdout.isTTY !== "boolean" || !stdout.isTTY) {
@@ -738,7 +1014,7 @@ function createHumanInputHandler(env, progress) {
738
1014
  const rl = createInterface({ input: env.stdin, output: env.stdout });
739
1015
  try {
740
1016
  const questionLine = question.trim() ? `
741
- ${question.trim()}` : "";
1017
+ ${renderMarkdown(question.trim())}` : "";
742
1018
  let isFirst = true;
743
1019
  while (true) {
744
1020
  const statsPrompt = progress.formatPrompt();
@@ -756,7 +1032,7 @@ ${statsPrompt}` : statsPrompt;
756
1032
  }
757
1033
  };
758
1034
  }
759
- async function handleAgentCommand(promptArg, options, env) {
1035
+ async function executeAgent(promptArg, options, env) {
760
1036
  const prompt = await resolvePrompt(promptArg, env);
761
1037
  const client = env.createClient();
762
1038
  const registry = new GadgetRegistry();
@@ -778,7 +1054,6 @@ async function handleAgentCommand(promptArg, options, env) {
778
1054
  const printer = new StreamPrinter(env.stdout);
779
1055
  const stderrTTY = env.stderr.isTTY === true;
780
1056
  const progress = new StreamProgress(env.stderr, stderrTTY, client.modelRegistry);
781
- let finishReason;
782
1057
  let usage;
783
1058
  let iterations = 0;
784
1059
  const countMessagesTokens = async (model, messages) => {
@@ -789,6 +1064,15 @@ async function handleAgentCommand(promptArg, options, env) {
789
1064
  return Math.round(totalChars / FALLBACK_CHARS_PER_TOKEN);
790
1065
  }
791
1066
  };
1067
+ const countGadgetOutputTokens = async (output) => {
1068
+ if (!output) return void 0;
1069
+ try {
1070
+ const messages = [{ role: "assistant", content: output }];
1071
+ return await client.countTokens(options.model, messages);
1072
+ } catch {
1073
+ return void 0;
1074
+ }
1075
+ };
792
1076
  const builder = new AgentBuilder(client).withModel(options.model).withLogger(env.createLogger("llmist:cli:agent")).withHooks({
793
1077
  observers: {
794
1078
  // onLLMCallStart: Start progress indicator for each LLM call
@@ -817,7 +1101,6 @@ async function handleAgentCommand(promptArg, options, env) {
817
1101
  // onLLMCallComplete: Finalize metrics after each LLM call
818
1102
  // This is where you'd typically log metrics or update dashboards
819
1103
  onLLMCallComplete: async (context) => {
820
- finishReason = context.finishReason;
821
1104
  usage = context.usage;
822
1105
  iterations = Math.max(iterations, context.iteration + 1);
823
1106
  if (context.usage) {
@@ -828,7 +1111,76 @@ async function handleAgentCommand(promptArg, options, env) {
828
1111
  progress.setOutputTokens(context.usage.outputTokens, false);
829
1112
  }
830
1113
  }
1114
+ let callCost;
1115
+ if (context.usage && client.modelRegistry) {
1116
+ try {
1117
+ const modelName = options.model.includes(":") ? options.model.split(":")[1] : options.model;
1118
+ const costResult = client.modelRegistry.estimateCost(
1119
+ modelName,
1120
+ context.usage.inputTokens,
1121
+ context.usage.outputTokens
1122
+ );
1123
+ if (costResult) callCost = costResult.totalCost;
1124
+ } catch {
1125
+ }
1126
+ }
1127
+ const callElapsed = progress.getCallElapsedSeconds();
831
1128
  progress.endCall(context.usage);
1129
+ if (stderrTTY) {
1130
+ const summary = renderSummary({
1131
+ iterations: context.iteration + 1,
1132
+ model: options.model,
1133
+ usage: context.usage,
1134
+ elapsedSeconds: callElapsed,
1135
+ cost: callCost,
1136
+ finishReason: context.finishReason
1137
+ });
1138
+ if (summary) {
1139
+ env.stderr.write(`${summary}
1140
+ `);
1141
+ }
1142
+ }
1143
+ }
1144
+ },
1145
+ // SHOWCASE: Controller-based approval gating for dangerous gadgets
1146
+ //
1147
+ // This demonstrates how to add safety layers WITHOUT modifying gadgets.
1148
+ // The RunCommand gadget is simple - it just executes commands. The CLI
1149
+ // adds the approval flow externally via beforeGadgetExecution controller.
1150
+ //
1151
+ // This pattern is composable: you can apply the same gating logic to
1152
+ // any gadget (DeleteFile, SendEmail, etc.) without changing the gadgets.
1153
+ controllers: {
1154
+ beforeGadgetExecution: async (ctx) => {
1155
+ if (ctx.gadgetName !== "RunCommand") {
1156
+ return { action: "proceed" };
1157
+ }
1158
+ const stdinTTY = isInteractive(env.stdin);
1159
+ const stderrTTY2 = env.stderr.isTTY === true;
1160
+ if (!stdinTTY || !stderrTTY2) {
1161
+ return {
1162
+ action: "skip",
1163
+ syntheticResult: "status=denied\n\nRunCommand requires interactive approval. Run in a terminal to approve commands."
1164
+ };
1165
+ }
1166
+ const command = ctx.parameters.command;
1167
+ progress.pause();
1168
+ env.stderr.write(`
1169
+ ${chalk3.yellow("\u{1F512} Execute:")} ${command}
1170
+ `);
1171
+ const approved = await promptApproval(env, " Approve? [y/n] ");
1172
+ if (!approved) {
1173
+ env.stderr.write(` ${chalk3.red("\u2717 Denied")}
1174
+
1175
+ `);
1176
+ return {
1177
+ action: "skip",
1178
+ syntheticResult: "status=denied\n\nCommand denied by user. Ask what they'd like to do instead."
1179
+ };
1180
+ }
1181
+ env.stderr.write(` ${chalk3.green("\u2713 Approved")}
1182
+ `);
1183
+ return { action: "proceed" };
832
1184
  }
833
1185
  }
834
1186
  });
@@ -849,6 +1201,10 @@ async function handleAgentCommand(promptArg, options, env) {
849
1201
  if (gadgets.length > 0) {
850
1202
  builder.withGadgets(...gadgets);
851
1203
  }
1204
+ builder.withParameterFormat(options.parameterFormat);
1205
+ const markers = generateMarkers();
1206
+ builder.withGadgetStartPrefix(markers.startPrefix);
1207
+ builder.withGadgetEndPrefix(markers.endPrefix);
852
1208
  const agent = builder.ask(prompt);
853
1209
  for await (const event of agent.run()) {
854
1210
  if (event.type === "text") {
@@ -857,20 +1213,22 @@ async function handleAgentCommand(promptArg, options, env) {
857
1213
  } else if (event.type === "gadget_result") {
858
1214
  progress.pause();
859
1215
  if (stderrTTY) {
860
- env.stderr.write(`${formatGadgetSummary(event.result)}
1216
+ const tokenCount = await countGadgetOutputTokens(event.result.result);
1217
+ env.stderr.write(`${formatGadgetSummary({ ...event.result, tokenCount })}
861
1218
  `);
862
1219
  }
863
1220
  }
864
1221
  }
865
1222
  progress.complete();
866
1223
  printer.ensureNewline();
867
- if (stderrTTY) {
868
- const summary = renderSummary({
869
- finishReason,
870
- usage,
1224
+ if (stderrTTY && iterations > 1) {
1225
+ env.stderr.write(`${chalk3.dim("\u2500".repeat(40))}
1226
+ `);
1227
+ const summary = renderOverallSummary({
1228
+ totalTokens: usage?.totalTokens,
871
1229
  iterations,
872
- cost: progress.getTotalCost(),
873
- elapsedSeconds: progress.getTotalElapsedSeconds()
1230
+ elapsedSeconds: progress.getTotalElapsedSeconds(),
1231
+ cost: progress.getTotalCost()
874
1232
  });
875
1233
  if (summary) {
876
1234
  env.stderr.write(`${summary}
@@ -878,27 +1236,11 @@ async function handleAgentCommand(promptArg, options, env) {
878
1236
  }
879
1237
  }
880
1238
  }
881
- function registerAgentCommand(program, env) {
882
- program.command(COMMANDS.agent).description("Run the llmist agent loop with optional gadgets.").argument("[prompt]", "Prompt for the agent loop. Falls back to stdin when available.").option(OPTION_FLAGS.model, OPTION_DESCRIPTIONS.model, DEFAULT_MODEL).option(OPTION_FLAGS.systemPrompt, OPTION_DESCRIPTIONS.systemPrompt).option(
883
- OPTION_FLAGS.temperature,
884
- OPTION_DESCRIPTIONS.temperature,
885
- createNumericParser({ label: "Temperature", min: 0, max: 2 })
886
- ).option(
887
- OPTION_FLAGS.maxIterations,
888
- OPTION_DESCRIPTIONS.maxIterations,
889
- createNumericParser({ label: "Max iterations", integer: true, min: 1 })
890
- ).option(
891
- OPTION_FLAGS.gadgetModule,
892
- OPTION_DESCRIPTIONS.gadgetModule,
893
- (value, previous = []) => [...previous, value],
894
- []
895
- ).option(
896
- OPTION_FLAGS.parameterFormat,
897
- OPTION_DESCRIPTIONS.parameterFormat,
898
- parseParameterFormat,
899
- DEFAULT_PARAMETER_FORMAT
900
- ).option(OPTION_FLAGS.noBuiltins, OPTION_DESCRIPTIONS.noBuiltins).option(OPTION_FLAGS.noBuiltinInteraction, OPTION_DESCRIPTIONS.noBuiltinInteraction).action(
901
- (prompt, options) => executeAction(() => handleAgentCommand(prompt, options, env), env)
1239
+ function registerAgentCommand(program, env, config) {
1240
+ const cmd = program.command(COMMANDS.agent).description("Run the llmist agent loop with optional gadgets.").argument("[prompt]", "Prompt for the agent loop. Falls back to stdin when available.");
1241
+ addAgentOptions(cmd, config);
1242
+ cmd.action(
1243
+ (prompt, options) => executeAction(() => executeAgent(prompt, options, env), env)
902
1244
  );
903
1245
  }
904
1246
 
@@ -906,7 +1248,7 @@ function registerAgentCommand(program, env) {
906
1248
  init_messages();
907
1249
  init_model_shortcuts();
908
1250
  init_constants();
909
- async function handleCompleteCommand(promptArg, options, env) {
1251
+ async function executeComplete(promptArg, options, env) {
910
1252
  const prompt = await resolvePrompt(promptArg, env);
911
1253
  const client = env.createClient();
912
1254
  const model = resolveModel(options.model);
@@ -960,25 +1302,307 @@ async function handleCompleteCommand(promptArg, options, env) {
960
1302
  }
961
1303
  }
962
1304
  }
963
- function registerCompleteCommand(program, env) {
964
- program.command(COMMANDS.complete).description("Stream a single completion from a specified model.").argument("[prompt]", "Prompt to send to the LLM. If omitted, stdin is used when available.").option(OPTION_FLAGS.model, OPTION_DESCRIPTIONS.model, DEFAULT_MODEL).option(OPTION_FLAGS.systemPrompt, OPTION_DESCRIPTIONS.systemPrompt).option(
965
- OPTION_FLAGS.temperature,
966
- OPTION_DESCRIPTIONS.temperature,
967
- createNumericParser({ label: "Temperature", min: 0, max: 2 })
968
- ).option(
969
- OPTION_FLAGS.maxTokens,
970
- OPTION_DESCRIPTIONS.maxTokens,
971
- createNumericParser({ label: "Max tokens", integer: true, min: 1 })
972
- ).action(
973
- (prompt, options) => executeAction(
974
- () => handleCompleteCommand(prompt, options, env),
975
- env
976
- )
1305
+ function registerCompleteCommand(program, env, config) {
1306
+ const cmd = program.command(COMMANDS.complete).description("Stream a single completion from a specified model.").argument("[prompt]", "Prompt to send to the LLM. If omitted, stdin is used when available.");
1307
+ addCompleteOptions(cmd, config);
1308
+ cmd.action(
1309
+ (prompt, options) => executeAction(() => executeComplete(prompt, options, env), env)
977
1310
  );
978
1311
  }
979
1312
 
1313
+ // src/cli/config.ts
1314
+ import { existsSync, readFileSync } from "node:fs";
1315
+ import { homedir } from "node:os";
1316
+ import { join } from "node:path";
1317
+ import { load as parseToml } from "js-toml";
1318
+ var GLOBAL_CONFIG_KEYS = /* @__PURE__ */ new Set(["log-level", "log-file"]);
1319
+ var VALID_LOG_LEVELS = ["silly", "trace", "debug", "info", "warn", "error", "fatal"];
1320
+ var COMPLETE_CONFIG_KEYS = /* @__PURE__ */ new Set(["model", "system", "temperature", "max-tokens"]);
1321
+ var AGENT_CONFIG_KEYS = /* @__PURE__ */ new Set([
1322
+ "model",
1323
+ "system",
1324
+ "temperature",
1325
+ "max-iterations",
1326
+ "gadget",
1327
+ "parameter-format",
1328
+ "builtins",
1329
+ "builtin-interaction"
1330
+ ]);
1331
+ var CUSTOM_CONFIG_KEYS = /* @__PURE__ */ new Set([
1332
+ ...COMPLETE_CONFIG_KEYS,
1333
+ ...AGENT_CONFIG_KEYS,
1334
+ "type",
1335
+ "description"
1336
+ ]);
1337
+ var VALID_PARAMETER_FORMATS = ["json", "yaml", "toml", "auto"];
1338
+ function getConfigPath() {
1339
+ return join(homedir(), ".llmist", "cli.toml");
1340
+ }
1341
+ var ConfigError = class extends Error {
1342
+ constructor(message, path2) {
1343
+ super(path2 ? `${path2}: ${message}` : message);
1344
+ this.path = path2;
1345
+ this.name = "ConfigError";
1346
+ }
1347
+ };
1348
+ function validateString(value, key, section) {
1349
+ if (typeof value !== "string") {
1350
+ throw new ConfigError(`[${section}].${key} must be a string`);
1351
+ }
1352
+ return value;
1353
+ }
1354
+ function validateNumber(value, key, section, opts) {
1355
+ if (typeof value !== "number") {
1356
+ throw new ConfigError(`[${section}].${key} must be a number`);
1357
+ }
1358
+ if (opts?.integer && !Number.isInteger(value)) {
1359
+ throw new ConfigError(`[${section}].${key} must be an integer`);
1360
+ }
1361
+ if (opts?.min !== void 0 && value < opts.min) {
1362
+ throw new ConfigError(`[${section}].${key} must be >= ${opts.min}`);
1363
+ }
1364
+ if (opts?.max !== void 0 && value > opts.max) {
1365
+ throw new ConfigError(`[${section}].${key} must be <= ${opts.max}`);
1366
+ }
1367
+ return value;
1368
+ }
1369
+ function validateBoolean(value, key, section) {
1370
+ if (typeof value !== "boolean") {
1371
+ throw new ConfigError(`[${section}].${key} must be a boolean`);
1372
+ }
1373
+ return value;
1374
+ }
1375
+ function validateStringArray(value, key, section) {
1376
+ if (!Array.isArray(value)) {
1377
+ throw new ConfigError(`[${section}].${key} must be an array`);
1378
+ }
1379
+ for (let i = 0; i < value.length; i++) {
1380
+ if (typeof value[i] !== "string") {
1381
+ throw new ConfigError(`[${section}].${key}[${i}] must be a string`);
1382
+ }
1383
+ }
1384
+ return value;
1385
+ }
1386
+ function validateBaseConfig(raw, section) {
1387
+ const result = {};
1388
+ if ("model" in raw) {
1389
+ result.model = validateString(raw.model, "model", section);
1390
+ }
1391
+ if ("system" in raw) {
1392
+ result.system = validateString(raw.system, "system", section);
1393
+ }
1394
+ if ("temperature" in raw) {
1395
+ result.temperature = validateNumber(raw.temperature, "temperature", section, {
1396
+ min: 0,
1397
+ max: 2
1398
+ });
1399
+ }
1400
+ return result;
1401
+ }
1402
+ function validateGlobalConfig(raw, section) {
1403
+ if (typeof raw !== "object" || raw === null) {
1404
+ throw new ConfigError(`[${section}] must be a table`);
1405
+ }
1406
+ const rawObj = raw;
1407
+ for (const key of Object.keys(rawObj)) {
1408
+ if (!GLOBAL_CONFIG_KEYS.has(key)) {
1409
+ throw new ConfigError(`[${section}].${key} is not a valid option`);
1410
+ }
1411
+ }
1412
+ const result = {};
1413
+ if ("log-level" in rawObj) {
1414
+ const level = validateString(rawObj["log-level"], "log-level", section);
1415
+ if (!VALID_LOG_LEVELS.includes(level)) {
1416
+ throw new ConfigError(
1417
+ `[${section}].log-level must be one of: ${VALID_LOG_LEVELS.join(", ")}`
1418
+ );
1419
+ }
1420
+ result["log-level"] = level;
1421
+ }
1422
+ if ("log-file" in rawObj) {
1423
+ result["log-file"] = validateString(rawObj["log-file"], "log-file", section);
1424
+ }
1425
+ return result;
1426
+ }
1427
+ function validateCompleteConfig(raw, section) {
1428
+ if (typeof raw !== "object" || raw === null) {
1429
+ throw new ConfigError(`[${section}] must be a table`);
1430
+ }
1431
+ const rawObj = raw;
1432
+ for (const key of Object.keys(rawObj)) {
1433
+ if (!COMPLETE_CONFIG_KEYS.has(key)) {
1434
+ throw new ConfigError(`[${section}].${key} is not a valid option`);
1435
+ }
1436
+ }
1437
+ const result = { ...validateBaseConfig(rawObj, section) };
1438
+ if ("max-tokens" in rawObj) {
1439
+ result["max-tokens"] = validateNumber(rawObj["max-tokens"], "max-tokens", section, {
1440
+ integer: true,
1441
+ min: 1
1442
+ });
1443
+ }
1444
+ return result;
1445
+ }
1446
+ function validateAgentConfig(raw, section) {
1447
+ if (typeof raw !== "object" || raw === null) {
1448
+ throw new ConfigError(`[${section}] must be a table`);
1449
+ }
1450
+ const rawObj = raw;
1451
+ for (const key of Object.keys(rawObj)) {
1452
+ if (!AGENT_CONFIG_KEYS.has(key)) {
1453
+ throw new ConfigError(`[${section}].${key} is not a valid option`);
1454
+ }
1455
+ }
1456
+ const result = { ...validateBaseConfig(rawObj, section) };
1457
+ if ("max-iterations" in rawObj) {
1458
+ result["max-iterations"] = validateNumber(rawObj["max-iterations"], "max-iterations", section, {
1459
+ integer: true,
1460
+ min: 1
1461
+ });
1462
+ }
1463
+ if ("gadget" in rawObj) {
1464
+ result.gadget = validateStringArray(rawObj.gadget, "gadget", section);
1465
+ }
1466
+ if ("parameter-format" in rawObj) {
1467
+ const format = validateString(rawObj["parameter-format"], "parameter-format", section);
1468
+ if (!VALID_PARAMETER_FORMATS.includes(format)) {
1469
+ throw new ConfigError(
1470
+ `[${section}].parameter-format must be one of: ${VALID_PARAMETER_FORMATS.join(", ")}`
1471
+ );
1472
+ }
1473
+ result["parameter-format"] = format;
1474
+ }
1475
+ if ("builtins" in rawObj) {
1476
+ result.builtins = validateBoolean(rawObj.builtins, "builtins", section);
1477
+ }
1478
+ if ("builtin-interaction" in rawObj) {
1479
+ result["builtin-interaction"] = validateBoolean(
1480
+ rawObj["builtin-interaction"],
1481
+ "builtin-interaction",
1482
+ section
1483
+ );
1484
+ }
1485
+ return result;
1486
+ }
1487
+ function validateCustomConfig(raw, section) {
1488
+ if (typeof raw !== "object" || raw === null) {
1489
+ throw new ConfigError(`[${section}] must be a table`);
1490
+ }
1491
+ const rawObj = raw;
1492
+ for (const key of Object.keys(rawObj)) {
1493
+ if (!CUSTOM_CONFIG_KEYS.has(key)) {
1494
+ throw new ConfigError(`[${section}].${key} is not a valid option`);
1495
+ }
1496
+ }
1497
+ let type = "agent";
1498
+ if ("type" in rawObj) {
1499
+ const typeValue = validateString(rawObj.type, "type", section);
1500
+ if (typeValue !== "agent" && typeValue !== "complete") {
1501
+ throw new ConfigError(`[${section}].type must be "agent" or "complete"`);
1502
+ }
1503
+ type = typeValue;
1504
+ }
1505
+ const result = {
1506
+ ...validateBaseConfig(rawObj, section),
1507
+ type
1508
+ };
1509
+ if ("description" in rawObj) {
1510
+ result.description = validateString(rawObj.description, "description", section);
1511
+ }
1512
+ if ("max-iterations" in rawObj) {
1513
+ result["max-iterations"] = validateNumber(rawObj["max-iterations"], "max-iterations", section, {
1514
+ integer: true,
1515
+ min: 1
1516
+ });
1517
+ }
1518
+ if ("gadget" in rawObj) {
1519
+ result.gadget = validateStringArray(rawObj.gadget, "gadget", section);
1520
+ }
1521
+ if ("parameter-format" in rawObj) {
1522
+ const format = validateString(rawObj["parameter-format"], "parameter-format", section);
1523
+ if (!VALID_PARAMETER_FORMATS.includes(format)) {
1524
+ throw new ConfigError(
1525
+ `[${section}].parameter-format must be one of: ${VALID_PARAMETER_FORMATS.join(", ")}`
1526
+ );
1527
+ }
1528
+ result["parameter-format"] = format;
1529
+ }
1530
+ if ("builtins" in rawObj) {
1531
+ result.builtins = validateBoolean(rawObj.builtins, "builtins", section);
1532
+ }
1533
+ if ("builtin-interaction" in rawObj) {
1534
+ result["builtin-interaction"] = validateBoolean(
1535
+ rawObj["builtin-interaction"],
1536
+ "builtin-interaction",
1537
+ section
1538
+ );
1539
+ }
1540
+ if ("max-tokens" in rawObj) {
1541
+ result["max-tokens"] = validateNumber(rawObj["max-tokens"], "max-tokens", section, {
1542
+ integer: true,
1543
+ min: 1
1544
+ });
1545
+ }
1546
+ return result;
1547
+ }
1548
+ function validateConfig(raw, configPath) {
1549
+ if (typeof raw !== "object" || raw === null) {
1550
+ throw new ConfigError("Config must be a TOML table", configPath);
1551
+ }
1552
+ const rawObj = raw;
1553
+ const result = {};
1554
+ for (const [key, value] of Object.entries(rawObj)) {
1555
+ try {
1556
+ if (key === "global") {
1557
+ result.global = validateGlobalConfig(value, key);
1558
+ } else if (key === "complete") {
1559
+ result.complete = validateCompleteConfig(value, key);
1560
+ } else if (key === "agent") {
1561
+ result.agent = validateAgentConfig(value, key);
1562
+ } else {
1563
+ result[key] = validateCustomConfig(value, key);
1564
+ }
1565
+ } catch (error) {
1566
+ if (error instanceof ConfigError) {
1567
+ throw new ConfigError(error.message, configPath);
1568
+ }
1569
+ throw error;
1570
+ }
1571
+ }
1572
+ return result;
1573
+ }
1574
+ function loadConfig() {
1575
+ const configPath = getConfigPath();
1576
+ if (!existsSync(configPath)) {
1577
+ return {};
1578
+ }
1579
+ let content;
1580
+ try {
1581
+ content = readFileSync(configPath, "utf-8");
1582
+ } catch (error) {
1583
+ throw new ConfigError(
1584
+ `Failed to read config file: ${error instanceof Error ? error.message : "Unknown error"}`,
1585
+ configPath
1586
+ );
1587
+ }
1588
+ let raw;
1589
+ try {
1590
+ raw = parseToml(content);
1591
+ } catch (error) {
1592
+ throw new ConfigError(
1593
+ `Invalid TOML syntax: ${error instanceof Error ? error.message : "Unknown error"}`,
1594
+ configPath
1595
+ );
1596
+ }
1597
+ return validateConfig(raw, configPath);
1598
+ }
1599
+ function getCustomCommandNames(config) {
1600
+ const reserved = /* @__PURE__ */ new Set(["global", "complete", "agent"]);
1601
+ return Object.keys(config).filter((key) => !reserved.has(key));
1602
+ }
1603
+
980
1604
  // src/cli/models-command.ts
981
- import chalk3 from "chalk";
1605
+ import chalk4 from "chalk";
982
1606
  init_model_shortcuts();
983
1607
  async function handleModelsCommand(options, env) {
984
1608
  const client = env.createClient();
@@ -998,13 +1622,13 @@ function renderTable(models, verbose, stream) {
998
1622
  }
999
1623
  grouped.get(provider).push(model);
1000
1624
  }
1001
- stream.write(chalk3.bold.cyan("\nAvailable Models\n"));
1002
- stream.write(chalk3.cyan("=".repeat(80)) + "\n\n");
1625
+ stream.write(chalk4.bold.cyan("\nAvailable Models\n"));
1626
+ stream.write(chalk4.cyan("=".repeat(80)) + "\n\n");
1003
1627
  const providers = Array.from(grouped.keys()).sort();
1004
1628
  for (const provider of providers) {
1005
1629
  const providerModels = grouped.get(provider);
1006
1630
  const providerName = provider.charAt(0).toUpperCase() + provider.slice(1);
1007
- stream.write(chalk3.bold.yellow(`${providerName} Models
1631
+ stream.write(chalk4.bold.yellow(`${providerName} Models
1008
1632
  `));
1009
1633
  if (verbose) {
1010
1634
  renderVerboseTable(providerModels, stream);
@@ -1013,11 +1637,11 @@ function renderTable(models, verbose, stream) {
1013
1637
  }
1014
1638
  stream.write("\n");
1015
1639
  }
1016
- stream.write(chalk3.bold.magenta("Model Shortcuts\n"));
1017
- stream.write(chalk3.dim("\u2500".repeat(80)) + "\n");
1640
+ stream.write(chalk4.bold.magenta("Model Shortcuts\n"));
1641
+ stream.write(chalk4.dim("\u2500".repeat(80)) + "\n");
1018
1642
  const shortcuts = Object.entries(MODEL_ALIASES).sort((a, b) => a[0].localeCompare(b[0]));
1019
1643
  for (const [shortcut, fullName] of shortcuts) {
1020
- stream.write(chalk3.cyan(` ${shortcut.padEnd(15)}`) + chalk3.dim(" \u2192 ") + chalk3.white(fullName) + "\n");
1644
+ stream.write(chalk4.cyan(` ${shortcut.padEnd(15)}`) + chalk4.dim(" \u2192 ") + chalk4.white(fullName) + "\n");
1021
1645
  }
1022
1646
  stream.write("\n");
1023
1647
  }
@@ -1027,45 +1651,45 @@ function renderCompactTable(models, stream) {
1027
1651
  const contextWidth = 13;
1028
1652
  const inputWidth = 10;
1029
1653
  const outputWidth = 10;
1030
- stream.write(chalk3.dim("\u2500".repeat(idWidth + nameWidth + contextWidth + inputWidth + outputWidth + 8)) + "\n");
1654
+ stream.write(chalk4.dim("\u2500".repeat(idWidth + nameWidth + contextWidth + inputWidth + outputWidth + 8)) + "\n");
1031
1655
  stream.write(
1032
- chalk3.bold(
1656
+ chalk4.bold(
1033
1657
  "Model ID".padEnd(idWidth) + " " + "Display Name".padEnd(nameWidth) + " " + "Context".padEnd(contextWidth) + " " + "Input".padEnd(inputWidth) + " " + "Output".padEnd(outputWidth)
1034
1658
  ) + "\n"
1035
1659
  );
1036
- stream.write(chalk3.dim("\u2500".repeat(idWidth + nameWidth + contextWidth + inputWidth + outputWidth + 8)) + "\n");
1660
+ stream.write(chalk4.dim("\u2500".repeat(idWidth + nameWidth + contextWidth + inputWidth + outputWidth + 8)) + "\n");
1037
1661
  for (const model of models) {
1038
1662
  const contextFormatted = formatTokens2(model.contextWindow);
1039
1663
  const inputPrice = `$${model.pricing.input.toFixed(2)}`;
1040
1664
  const outputPrice = `$${model.pricing.output.toFixed(2)}`;
1041
1665
  stream.write(
1042
- chalk3.green(model.modelId.padEnd(idWidth)) + " " + chalk3.white(model.displayName.padEnd(nameWidth)) + " " + chalk3.yellow(contextFormatted.padEnd(contextWidth)) + " " + chalk3.cyan(inputPrice.padEnd(inputWidth)) + " " + chalk3.cyan(outputPrice.padEnd(outputWidth)) + "\n"
1666
+ chalk4.green(model.modelId.padEnd(idWidth)) + " " + chalk4.white(model.displayName.padEnd(nameWidth)) + " " + chalk4.yellow(contextFormatted.padEnd(contextWidth)) + " " + chalk4.cyan(inputPrice.padEnd(inputWidth)) + " " + chalk4.cyan(outputPrice.padEnd(outputWidth)) + "\n"
1043
1667
  );
1044
1668
  }
1045
- stream.write(chalk3.dim("\u2500".repeat(idWidth + nameWidth + contextWidth + inputWidth + outputWidth + 8)) + "\n");
1046
- stream.write(chalk3.dim(` * Prices are per 1M tokens
1669
+ stream.write(chalk4.dim("\u2500".repeat(idWidth + nameWidth + contextWidth + inputWidth + outputWidth + 8)) + "\n");
1670
+ stream.write(chalk4.dim(` * Prices are per 1M tokens
1047
1671
  `));
1048
1672
  }
1049
1673
  function renderVerboseTable(models, stream) {
1050
1674
  for (const model of models) {
1051
- stream.write(chalk3.bold.green(`
1675
+ stream.write(chalk4.bold.green(`
1052
1676
  ${model.modelId}
1053
1677
  `));
1054
- stream.write(chalk3.dim(" " + "\u2500".repeat(60)) + "\n");
1055
- stream.write(` ${chalk3.dim("Name:")} ${chalk3.white(model.displayName)}
1678
+ stream.write(chalk4.dim(" " + "\u2500".repeat(60)) + "\n");
1679
+ stream.write(` ${chalk4.dim("Name:")} ${chalk4.white(model.displayName)}
1056
1680
  `);
1057
- stream.write(` ${chalk3.dim("Context:")} ${chalk3.yellow(formatTokens2(model.contextWindow))}
1681
+ stream.write(` ${chalk4.dim("Context:")} ${chalk4.yellow(formatTokens2(model.contextWindow))}
1058
1682
  `);
1059
- stream.write(` ${chalk3.dim("Max Output:")} ${chalk3.yellow(formatTokens2(model.maxOutputTokens))}
1683
+ stream.write(` ${chalk4.dim("Max Output:")} ${chalk4.yellow(formatTokens2(model.maxOutputTokens))}
1060
1684
  `);
1061
- stream.write(` ${chalk3.dim("Pricing:")} ${chalk3.cyan(`$${model.pricing.input.toFixed(2)} input`)} ${chalk3.dim("/")} ${chalk3.cyan(`$${model.pricing.output.toFixed(2)} output`)} ${chalk3.dim("(per 1M tokens)")}
1685
+ stream.write(` ${chalk4.dim("Pricing:")} ${chalk4.cyan(`$${model.pricing.input.toFixed(2)} input`)} ${chalk4.dim("/")} ${chalk4.cyan(`$${model.pricing.output.toFixed(2)} output`)} ${chalk4.dim("(per 1M tokens)")}
1062
1686
  `);
1063
1687
  if (model.pricing.cachedInput !== void 0) {
1064
- stream.write(` ${chalk3.dim("Cached Input:")} ${chalk3.cyan(`$${model.pricing.cachedInput.toFixed(2)} per 1M tokens`)}
1688
+ stream.write(` ${chalk4.dim("Cached Input:")} ${chalk4.cyan(`$${model.pricing.cachedInput.toFixed(2)} per 1M tokens`)}
1065
1689
  `);
1066
1690
  }
1067
1691
  if (model.knowledgeCutoff) {
1068
- stream.write(` ${chalk3.dim("Knowledge:")} ${model.knowledgeCutoff}
1692
+ stream.write(` ${chalk4.dim("Knowledge:")} ${model.knowledgeCutoff}
1069
1693
  `);
1070
1694
  }
1071
1695
  const features = [];
@@ -1076,20 +1700,20 @@ function renderVerboseTable(models, stream) {
1076
1700
  if (model.features.structuredOutputs) features.push("structured-outputs");
1077
1701
  if (model.features.fineTuning) features.push("fine-tuning");
1078
1702
  if (features.length > 0) {
1079
- stream.write(` ${chalk3.dim("Features:")} ${chalk3.blue(features.join(", "))}
1703
+ stream.write(` ${chalk4.dim("Features:")} ${chalk4.blue(features.join(", "))}
1080
1704
  `);
1081
1705
  }
1082
1706
  if (model.metadata) {
1083
1707
  if (model.metadata.family) {
1084
- stream.write(` ${chalk3.dim("Family:")} ${model.metadata.family}
1708
+ stream.write(` ${chalk4.dim("Family:")} ${model.metadata.family}
1085
1709
  `);
1086
1710
  }
1087
1711
  if (model.metadata.releaseDate) {
1088
- stream.write(` ${chalk3.dim("Released:")} ${model.metadata.releaseDate}
1712
+ stream.write(` ${chalk4.dim("Released:")} ${model.metadata.releaseDate}
1089
1713
  `);
1090
1714
  }
1091
1715
  if (model.metadata.notes) {
1092
- stream.write(` ${chalk3.dim("Notes:")} ${chalk3.italic(model.metadata.notes)}
1716
+ stream.write(` ${chalk4.dim("Notes:")} ${chalk4.italic(model.metadata.notes)}
1093
1717
  `);
1094
1718
  }
1095
1719
  }
@@ -1137,11 +1761,43 @@ function registerModelsCommand(program, env) {
1137
1761
  );
1138
1762
  }
1139
1763
 
1764
+ // src/cli/custom-command.ts
1765
+ function registerCustomCommand(program, name, config, env) {
1766
+ const type = config.type ?? "agent";
1767
+ const description = config.description ?? `Custom ${type} command`;
1768
+ const cmd = program.command(name).description(description).argument("[prompt]", "Prompt for the command. Falls back to stdin when available.");
1769
+ if (type === "complete") {
1770
+ addCompleteOptions(cmd, config);
1771
+ cmd.action(
1772
+ (prompt, cliOptions) => executeAction(async () => {
1773
+ const configDefaults = configToCompleteOptions(config);
1774
+ const options = {
1775
+ ...configDefaults,
1776
+ ...cliOptions
1777
+ };
1778
+ await executeComplete(prompt, options, env);
1779
+ }, env)
1780
+ );
1781
+ } else {
1782
+ addAgentOptions(cmd, config);
1783
+ cmd.action(
1784
+ (prompt, cliOptions) => executeAction(async () => {
1785
+ const configDefaults = configToAgentOptions(config);
1786
+ const options = {
1787
+ ...configDefaults,
1788
+ ...cliOptions
1789
+ };
1790
+ await executeAgent(prompt, options, env);
1791
+ }, env)
1792
+ );
1793
+ }
1794
+ }
1795
+
1140
1796
  // src/cli/environment.ts
1141
1797
  init_client();
1142
1798
  init_logger();
1143
1799
  import readline from "node:readline";
1144
- import chalk4 from "chalk";
1800
+ import chalk5 from "chalk";
1145
1801
  var LOG_LEVEL_MAP = {
1146
1802
  silly: 0,
1147
1803
  trace: 1,
@@ -1185,14 +1841,14 @@ function createPromptFunction(stdin, stdout) {
1185
1841
  output: stdout
1186
1842
  });
1187
1843
  stdout.write("\n");
1188
- stdout.write(`${chalk4.cyan("\u2500".repeat(60))}
1844
+ stdout.write(`${chalk5.cyan("\u2500".repeat(60))}
1189
1845
  `);
1190
- stdout.write(chalk4.cyan.bold("\u{1F916} Agent asks:\n"));
1846
+ stdout.write(chalk5.cyan.bold("\u{1F916} Agent asks:\n"));
1191
1847
  stdout.write(`${question}
1192
1848
  `);
1193
- stdout.write(`${chalk4.cyan("\u2500".repeat(60))}
1849
+ stdout.write(`${chalk5.cyan("\u2500".repeat(60))}
1194
1850
  `);
1195
- rl.question(chalk4.green.bold("You: "), (answer) => {
1851
+ rl.question(chalk5.green.bold("You: "), (answer) => {
1196
1852
  rl.close();
1197
1853
  resolve(answer);
1198
1854
  });
@@ -1227,29 +1883,39 @@ function parseLogLevel(value) {
1227
1883
  }
1228
1884
  return normalized;
1229
1885
  }
1230
- function createProgram(env) {
1886
+ function createProgram(env, config) {
1231
1887
  const program = new Command();
1232
1888
  program.name(CLI_NAME).description(CLI_DESCRIPTION).version(package_default.version).option(OPTION_FLAGS.logLevel, OPTION_DESCRIPTIONS.logLevel, parseLogLevel).option(OPTION_FLAGS.logFile, OPTION_DESCRIPTIONS.logFile).configureOutput({
1233
1889
  writeOut: (str) => env.stdout.write(str),
1234
1890
  writeErr: (str) => env.stderr.write(str)
1235
1891
  });
1236
- registerCompleteCommand(program, env);
1237
- registerAgentCommand(program, env);
1892
+ registerCompleteCommand(program, env, config?.complete);
1893
+ registerAgentCommand(program, env, config?.agent);
1238
1894
  registerModelsCommand(program, env);
1895
+ if (config) {
1896
+ const customNames = getCustomCommandNames(config);
1897
+ for (const name of customNames) {
1898
+ const cmdConfig = config[name];
1899
+ registerCustomCommand(program, name, cmdConfig, env);
1900
+ }
1901
+ }
1239
1902
  return program;
1240
1903
  }
1241
1904
  async function runCLI(overrides = {}) {
1905
+ const opts = "env" in overrides || "config" in overrides ? overrides : { env: overrides };
1906
+ const config = opts.config !== void 0 ? opts.config : loadConfig();
1907
+ const envOverrides = opts.env ?? {};
1242
1908
  const preParser = new Command();
1243
1909
  preParser.option(OPTION_FLAGS.logLevel, OPTION_DESCRIPTIONS.logLevel, parseLogLevel).option(OPTION_FLAGS.logFile, OPTION_DESCRIPTIONS.logFile).allowUnknownOption().allowExcessArguments().helpOption(false);
1244
1910
  preParser.parse(process.argv);
1245
1911
  const globalOpts = preParser.opts();
1246
1912
  const loggerConfig = {
1247
- logLevel: globalOpts.logLevel,
1248
- logFile: globalOpts.logFile
1913
+ logLevel: globalOpts.logLevel ?? config.global?.["log-level"],
1914
+ logFile: globalOpts.logFile ?? config.global?.["log-file"]
1249
1915
  };
1250
1916
  const defaultEnv = createDefaultEnvironment(loggerConfig);
1251
- const env = { ...defaultEnv, ...overrides };
1252
- const program = createProgram(env);
1917
+ const env = { ...defaultEnv, ...envOverrides };
1918
+ const program = createProgram(env, config);
1253
1919
  await program.parseAsync(env.argv);
1254
1920
  }
1255
1921