llmist 0.7.0 → 1.0.0

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
@@ -22,7 +22,7 @@ import {
22
22
  init_model_shortcuts,
23
23
  init_registry,
24
24
  resolveModel
25
- } from "./chunk-ZFHFBEQ5.js";
25
+ } from "./chunk-T24KLXY4.js";
26
26
 
27
27
  // src/cli/constants.ts
28
28
  var CLI_NAME = "llmist";
@@ -34,7 +34,6 @@ 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 = "toml";
38
37
  var OPTION_FLAGS = {
39
38
  model: "-m, --model <identifier>",
40
39
  systemPrompt: "-s, --system <prompt>",
@@ -42,12 +41,14 @@ var OPTION_FLAGS = {
42
41
  maxTokens: "--max-tokens <count>",
43
42
  maxIterations: "-i, --max-iterations <count>",
44
43
  gadgetModule: "-g, --gadget <module>",
45
- parameterFormat: "--parameter-format <format>",
46
44
  logLevel: "--log-level <level>",
47
45
  logFile: "--log-file <path>",
48
46
  logReset: "--log-reset",
47
+ logLlmRequests: "--log-llm-requests [dir]",
48
+ logLlmResponses: "--log-llm-responses [dir]",
49
49
  noBuiltins: "--no-builtins",
50
- noBuiltinInteraction: "--no-builtin-interaction"
50
+ noBuiltinInteraction: "--no-builtin-interaction",
51
+ quiet: "-q, --quiet"
51
52
  };
52
53
  var OPTION_DESCRIPTIONS = {
53
54
  model: "Model identifier, e.g. openai:gpt-5-nano or anthropic:claude-sonnet-4-5.",
@@ -56,22 +57,24 @@ var OPTION_DESCRIPTIONS = {
56
57
  maxTokens: "Maximum number of output tokens requested from the model.",
57
58
  maxIterations: "Maximum number of agent loop iterations before exiting.",
58
59
  gadgetModule: "Path or module specifier for a gadget export. Repeat to register multiple gadgets.",
59
- parameterFormat: "Format for gadget parameter schemas: 'json', 'yaml', 'toml', or 'auto'.",
60
60
  logLevel: "Log level: silly, trace, debug, info, warn, error, fatal.",
61
61
  logFile: "Path to log file. When set, logs are written to file instead of stderr.",
62
62
  logReset: "Reset (truncate) the log file at session start instead of appending.",
63
+ logLlmRequests: "Save raw LLM requests as plain text. Optional dir, defaults to ~/.llmist/logs/requests/",
64
+ logLlmResponses: "Save raw LLM responses as plain text. Optional dir, defaults to ~/.llmist/logs/responses/",
63
65
  noBuiltins: "Disable built-in gadgets (AskUser, TellUser).",
64
- noBuiltinInteraction: "Disable interactive gadgets (AskUser) while keeping TellUser."
66
+ noBuiltinInteraction: "Disable interactive gadgets (AskUser) while keeping TellUser.",
67
+ quiet: "Suppress all output except content (text and TellUser messages)."
65
68
  };
66
69
  var SUMMARY_PREFIX = "[llmist]";
67
70
 
68
71
  // src/cli/program.ts
69
- import { Command, InvalidArgumentError as InvalidArgumentError3 } from "commander";
72
+ import { Command, InvalidArgumentError as InvalidArgumentError2 } from "commander";
70
73
 
71
74
  // package.json
72
75
  var package_default = {
73
76
  name: "llmist",
74
- version: "0.6.2",
77
+ version: "0.8.0",
75
78
  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.",
76
79
  type: "module",
77
80
  main: "dist/index.cjs",
@@ -155,6 +158,7 @@ var package_default = {
155
158
  "@google/genai": "^1.27.0",
156
159
  chalk: "^5.6.2",
157
160
  commander: "^12.1.0",
161
+ eta: "^4.4.1",
158
162
  "js-toml": "^1.0.2",
159
163
  "js-yaml": "^4.1.0",
160
164
  marked: "^15.0.12",
@@ -274,12 +278,19 @@ import fs from "node:fs";
274
278
  import path from "node:path";
275
279
  import { pathToFileURL } from "node:url";
276
280
  var PATH_PREFIXES = [".", "/", "~"];
281
+ function isGadgetLike(value) {
282
+ if (typeof value !== "object" || value === null) {
283
+ return false;
284
+ }
285
+ const obj = value;
286
+ return typeof obj.execute === "function" && typeof obj.description === "string" && ("parameterSchema" in obj || "schema" in obj);
287
+ }
277
288
  function isGadgetConstructor(value) {
278
289
  if (typeof value !== "function") {
279
290
  return false;
280
291
  }
281
292
  const prototype = value.prototype;
282
- return Boolean(prototype) && prototype instanceof BaseGadget;
293
+ return Boolean(prototype) && (prototype instanceof BaseGadget || isGadgetLike(prototype));
283
294
  }
284
295
  function expandHomePath(input) {
285
296
  if (!input.startsWith("~")) {
@@ -316,7 +327,7 @@ function extractGadgetsFromModule(moduleExports) {
316
327
  return;
317
328
  }
318
329
  visited.add(value);
319
- if (value instanceof BaseGadget) {
330
+ if (value instanceof BaseGadget || isGadgetLike(value)) {
320
331
  results.push(value);
321
332
  return;
322
333
  }
@@ -365,8 +376,33 @@ async function loadGadgets(specifiers, cwd, importer = (specifier) => import(spe
365
376
  return gadgets;
366
377
  }
367
378
 
368
- // src/cli/option-helpers.ts
369
- import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
379
+ // src/cli/llm-logging.ts
380
+ import { mkdir, writeFile } from "node:fs/promises";
381
+ import { homedir } from "node:os";
382
+ import { join } from "node:path";
383
+ var DEFAULT_LLM_LOG_DIR = join(homedir(), ".llmist", "logs");
384
+ function resolveLogDir(option, subdir) {
385
+ if (option === true) {
386
+ return join(DEFAULT_LLM_LOG_DIR, subdir);
387
+ }
388
+ if (typeof option === "string") {
389
+ return option;
390
+ }
391
+ return void 0;
392
+ }
393
+ function formatLlmRequest(messages) {
394
+ const lines = [];
395
+ for (const msg of messages) {
396
+ lines.push(`=== ${msg.role.toUpperCase()} ===`);
397
+ lines.push(msg.content ?? "");
398
+ lines.push("");
399
+ }
400
+ return lines.join("\n");
401
+ }
402
+ async function writeLogFile(dir, filename, content) {
403
+ await mkdir(dir, { recursive: true });
404
+ await writeFile(join(dir, filename), content, "utf-8");
405
+ }
370
406
 
371
407
  // src/cli/utils.ts
372
408
  init_constants();
@@ -410,9 +446,29 @@ function ensureMarkedConfigured() {
410
446
  }
411
447
  function renderMarkdown(text) {
412
448
  ensureMarkedConfigured();
413
- const rendered = marked.parse(text);
449
+ let rendered = marked.parse(text);
450
+ rendered = rendered.replace(/\*\*(.+?)\*\*/g, (_, content) => chalk.bold(content)).replace(/(?<!\*)\*(\S[^*]*)\*(?!\*)/g, (_, content) => chalk.italic(content));
414
451
  return rendered.trimEnd();
415
452
  }
453
+ function createRainbowSeparator() {
454
+ const colors = [chalk.red, chalk.yellow, chalk.green, chalk.cyan, chalk.blue, chalk.magenta];
455
+ const char = "\u2500";
456
+ const width = process.stdout.columns || 80;
457
+ let result = "";
458
+ for (let i = 0; i < width; i++) {
459
+ result += colors[i % colors.length](char);
460
+ }
461
+ return result;
462
+ }
463
+ function renderMarkdownWithSeparators(text) {
464
+ const rendered = renderMarkdown(text);
465
+ const separator = createRainbowSeparator();
466
+ return `
467
+ ${separator}
468
+ ${rendered}
469
+ ${separator}
470
+ `;
471
+ }
416
472
  function formatTokens(tokens) {
417
473
  return tokens >= 1e3 ? `${(tokens / 1e3).toFixed(1)}k` : `${tokens}`;
418
474
  }
@@ -441,8 +497,14 @@ function renderSummary(metadata) {
441
497
  parts.push(chalk.magenta(metadata.model));
442
498
  }
443
499
  if (metadata.usage) {
444
- const { inputTokens, outputTokens } = metadata.usage;
500
+ const { inputTokens, outputTokens, cachedInputTokens, cacheCreationInputTokens } = metadata.usage;
445
501
  parts.push(chalk.dim("\u2191") + chalk.yellow(` ${formatTokens(inputTokens)}`));
502
+ if (cachedInputTokens && cachedInputTokens > 0) {
503
+ parts.push(chalk.dim("\u27F3") + chalk.blue(` ${formatTokens(cachedInputTokens)}`));
504
+ }
505
+ if (cacheCreationInputTokens && cacheCreationInputTokens > 0) {
506
+ parts.push(chalk.dim("\u270E") + chalk.magenta(` ${formatTokens(cacheCreationInputTokens)}`));
507
+ }
446
508
  parts.push(chalk.dim("\u2193") + chalk.green(` ${formatTokens(outputTokens)}`));
447
509
  }
448
510
  if (metadata.elapsedSeconds !== void 0 && metadata.elapsedSeconds > 0) {
@@ -526,7 +588,7 @@ function formatGadgetSummary(result) {
526
588
  const summaryLine = `${icon} ${gadgetLabel}${paramsLabel} ${chalk.dim("\u2192")} ${outputLabel} ${timeLabel}`;
527
589
  if (result.gadgetName === "TellUser" && result.parameters?.message) {
528
590
  const message = String(result.parameters.message);
529
- const rendered = renderMarkdown(message);
591
+ const rendered = renderMarkdownWithSeparators(message);
530
592
  return `${summaryLine}
531
593
  ${rendered}`;
532
594
  }
@@ -611,6 +673,9 @@ var StreamProgress = class {
611
673
  callOutputTokensEstimated = true;
612
674
  callOutputChars = 0;
613
675
  isStreaming = false;
676
+ // Cache token tracking for live cost estimation during streaming
677
+ callCachedInputTokens = 0;
678
+ callCacheCreationInputTokens = 0;
614
679
  // Cumulative stats (cumulative mode)
615
680
  totalStartTime = Date.now();
616
681
  totalTokens = 0;
@@ -636,11 +701,13 @@ var StreamProgress = class {
636
701
  this.callOutputTokensEstimated = true;
637
702
  this.callOutputChars = 0;
638
703
  this.isStreaming = false;
704
+ this.callCachedInputTokens = 0;
705
+ this.callCacheCreationInputTokens = 0;
639
706
  this.start();
640
707
  }
641
708
  /**
642
709
  * Ends the current LLM call. Updates cumulative stats and switches to cumulative mode.
643
- * @param usage - Final token usage from the call
710
+ * @param usage - Final token usage from the call (including cached tokens if available)
644
711
  */
645
712
  endCall(usage) {
646
713
  this.iterations++;
@@ -652,7 +719,9 @@ var StreamProgress = class {
652
719
  const cost = this.modelRegistry.estimateCost(
653
720
  modelName,
654
721
  usage.inputTokens,
655
- usage.outputTokens
722
+ usage.outputTokens,
723
+ usage.cachedInputTokens ?? 0,
724
+ usage.cacheCreationInputTokens ?? 0
656
725
  );
657
726
  if (cost) {
658
727
  this.totalCost += cost.totalCost;
@@ -692,6 +761,16 @@ var StreamProgress = class {
692
761
  this.callOutputTokens = tokens;
693
762
  this.callOutputTokensEstimated = estimated;
694
763
  }
764
+ /**
765
+ * Sets cached token counts for the current call (from stream metadata).
766
+ * Used for live cost estimation during streaming.
767
+ * @param cachedInputTokens - Number of tokens read from cache (cheaper)
768
+ * @param cacheCreationInputTokens - Number of tokens written to cache (more expensive)
769
+ */
770
+ setCachedTokens(cachedInputTokens, cacheCreationInputTokens) {
771
+ this.callCachedInputTokens = cachedInputTokens;
772
+ this.callCacheCreationInputTokens = cacheCreationInputTokens;
773
+ }
695
774
  /**
696
775
  * Get total elapsed time in seconds since the first call started.
697
776
  * @returns Elapsed time in seconds with 1 decimal place
@@ -756,11 +835,32 @@ var StreamProgress = class {
756
835
  parts.push(chalk2.dim("\u2193") + chalk2.green(` ${prefix}${formatTokens(outTokens)}`));
757
836
  }
758
837
  parts.push(chalk2.dim(`${elapsed}s`));
759
- if (this.totalCost > 0) {
760
- parts.push(chalk2.cyan(`$${formatCost(this.totalCost)}`));
838
+ const callCost = this.calculateCurrentCallCost(outTokens);
839
+ if (callCost > 0) {
840
+ parts.push(chalk2.cyan(`$${formatCost(callCost)}`));
761
841
  }
762
842
  this.target.write(`\r${parts.join(chalk2.dim(" | "))} ${chalk2.cyan(spinner)}`);
763
843
  }
844
+ /**
845
+ * Calculates live cost estimate for the current streaming call.
846
+ * Uses current input/output tokens and cached token counts.
847
+ */
848
+ calculateCurrentCallCost(outputTokens) {
849
+ if (!this.modelRegistry || !this.model) return 0;
850
+ try {
851
+ const modelName = this.model.includes(":") ? this.model.split(":")[1] : this.model;
852
+ const cost = this.modelRegistry.estimateCost(
853
+ modelName,
854
+ this.callInputTokens,
855
+ outputTokens,
856
+ this.callCachedInputTokens,
857
+ this.callCacheCreationInputTokens
858
+ );
859
+ return cost?.totalCost ?? 0;
860
+ } catch {
861
+ return 0;
862
+ }
863
+ }
764
864
  renderCumulativeMode(spinner) {
765
865
  const elapsed = ((Date.now() - this.totalStartTime) / 1e3).toFixed(1);
766
866
  const parts = [];
@@ -795,7 +895,7 @@ var StreamProgress = class {
795
895
  }
796
896
  this.isRunning = false;
797
897
  if (this.hasRendered) {
798
- this.target.write("\r\x1B[K");
898
+ this.target.write("\r\x1B[K\x1B[0G");
799
899
  this.hasRendered = false;
800
900
  }
801
901
  }
@@ -888,16 +988,6 @@ async function executeAction(action, env) {
888
988
  }
889
989
 
890
990
  // src/cli/option-helpers.ts
891
- var PARAMETER_FORMAT_VALUES = ["json", "yaml", "toml", "auto"];
892
- function parseParameterFormat(value) {
893
- const normalized = value.toLowerCase();
894
- if (!PARAMETER_FORMAT_VALUES.includes(normalized)) {
895
- throw new InvalidArgumentError2(
896
- `Parameter format must be one of: ${PARAMETER_FORMAT_VALUES.join(", ")}`
897
- );
898
- }
899
- return normalized;
900
- }
901
991
  function addCompleteOptions(cmd, defaults) {
902
992
  return cmd.option(OPTION_FLAGS.model, OPTION_DESCRIPTIONS.model, defaults?.model ?? DEFAULT_MODEL).option(OPTION_FLAGS.systemPrompt, OPTION_DESCRIPTIONS.systemPrompt, defaults?.system).option(
903
993
  OPTION_FLAGS.temperature,
@@ -909,7 +999,7 @@ function addCompleteOptions(cmd, defaults) {
909
999
  OPTION_DESCRIPTIONS.maxTokens,
910
1000
  createNumericParser({ label: "Max tokens", integer: true, min: 1 }),
911
1001
  defaults?.["max-tokens"]
912
- );
1002
+ ).option(OPTION_FLAGS.quiet, OPTION_DESCRIPTIONS.quiet, defaults?.quiet).option(OPTION_FLAGS.logLlmRequests, OPTION_DESCRIPTIONS.logLlmRequests, defaults?.["log-llm-requests"]).option(OPTION_FLAGS.logLlmResponses, OPTION_DESCRIPTIONS.logLlmResponses, defaults?.["log-llm-responses"]);
913
1003
  }
914
1004
  function addAgentOptions(cmd, defaults) {
915
1005
  const gadgetAccumulator = (value, previous = []) => [
@@ -929,16 +1019,11 @@ function addAgentOptions(cmd, defaults) {
929
1019
  defaults?.["max-iterations"]
930
1020
  ).option(OPTION_FLAGS.gadgetModule, OPTION_DESCRIPTIONS.gadgetModule, gadgetAccumulator, [
931
1021
  ...defaultGadgets
932
- ]).option(
933
- OPTION_FLAGS.parameterFormat,
934
- OPTION_DESCRIPTIONS.parameterFormat,
935
- parseParameterFormat,
936
- defaults?.["parameter-format"] ?? DEFAULT_PARAMETER_FORMAT
937
- ).option(OPTION_FLAGS.noBuiltins, OPTION_DESCRIPTIONS.noBuiltins, defaults?.builtins !== false).option(
1022
+ ]).option(OPTION_FLAGS.noBuiltins, OPTION_DESCRIPTIONS.noBuiltins, defaults?.builtins !== false).option(
938
1023
  OPTION_FLAGS.noBuiltinInteraction,
939
1024
  OPTION_DESCRIPTIONS.noBuiltinInteraction,
940
1025
  defaults?.["builtin-interaction"] !== false
941
- );
1026
+ ).option(OPTION_FLAGS.quiet, OPTION_DESCRIPTIONS.quiet, defaults?.quiet).option(OPTION_FLAGS.logLlmRequests, OPTION_DESCRIPTIONS.logLlmRequests, defaults?.["log-llm-requests"]).option(OPTION_FLAGS.logLlmResponses, OPTION_DESCRIPTIONS.logLlmResponses, defaults?.["log-llm-responses"]);
942
1027
  }
943
1028
  function configToCompleteOptions(config) {
944
1029
  const result = {};
@@ -946,6 +1031,9 @@ function configToCompleteOptions(config) {
946
1031
  if (config.system !== void 0) result.system = config.system;
947
1032
  if (config.temperature !== void 0) result.temperature = config.temperature;
948
1033
  if (config["max-tokens"] !== void 0) result.maxTokens = config["max-tokens"];
1034
+ if (config.quiet !== void 0) result.quiet = config.quiet;
1035
+ if (config["log-llm-requests"] !== void 0) result.logLlmRequests = config["log-llm-requests"];
1036
+ if (config["log-llm-responses"] !== void 0) result.logLlmResponses = config["log-llm-responses"];
949
1037
  return result;
950
1038
  }
951
1039
  function configToAgentOptions(config) {
@@ -955,7 +1043,6 @@ function configToAgentOptions(config) {
955
1043
  if (config.temperature !== void 0) result.temperature = config.temperature;
956
1044
  if (config["max-iterations"] !== void 0) result.maxIterations = config["max-iterations"];
957
1045
  if (config.gadget !== void 0) result.gadget = config.gadget;
958
- if (config["parameter-format"] !== void 0) result.parameterFormat = config["parameter-format"];
959
1046
  if (config.builtins !== void 0) result.builtins = config.builtins;
960
1047
  if (config["builtin-interaction"] !== void 0)
961
1048
  result.builtinInteraction = config["builtin-interaction"];
@@ -963,6 +1050,11 @@ function configToAgentOptions(config) {
963
1050
  result.gadgetStartPrefix = config["gadget-start-prefix"];
964
1051
  if (config["gadget-end-prefix"] !== void 0)
965
1052
  result.gadgetEndPrefix = config["gadget-end-prefix"];
1053
+ if (config["gadget-arg-prefix"] !== void 0)
1054
+ result.gadgetArgPrefix = config["gadget-arg-prefix"];
1055
+ if (config.quiet !== void 0) result.quiet = config.quiet;
1056
+ if (config["log-llm-requests"] !== void 0) result.logLlmRequests = config["log-llm-requests"];
1057
+ if (config["log-llm-responses"] !== void 0) result.logLlmResponses = config["log-llm-responses"];
966
1058
  return result;
967
1059
  }
968
1060
 
@@ -986,7 +1078,7 @@ function createHumanInputHandler(env, progress) {
986
1078
  const rl = createInterface({ input: env.stdin, output: env.stdout });
987
1079
  try {
988
1080
  const questionLine = question.trim() ? `
989
- ${renderMarkdown(question.trim())}` : "";
1081
+ ${renderMarkdownWithSeparators(question.trim())}` : "";
990
1082
  let isFirst = true;
991
1083
  while (true) {
992
1084
  const statsPrompt = progress.formatPrompt();
@@ -1008,9 +1100,10 @@ async function executeAgent(promptArg, options, env) {
1008
1100
  const prompt = await resolvePrompt(promptArg, env);
1009
1101
  const client = env.createClient();
1010
1102
  const registry = new GadgetRegistry();
1103
+ const stdinIsInteractive = isInteractive(env.stdin);
1011
1104
  if (options.builtins !== false) {
1012
1105
  for (const gadget of builtinGadgets) {
1013
- if (options.builtinInteraction === false && gadget.name === "AskUser") {
1106
+ if (gadget.name === "AskUser" && (options.builtinInteraction === false || !stdinIsInteractive)) {
1014
1107
  continue;
1015
1108
  }
1016
1109
  registry.registerByClass(gadget);
@@ -1028,6 +1121,9 @@ async function executeAgent(promptArg, options, env) {
1028
1121
  const progress = new StreamProgress(env.stderr, stderrTTY, client.modelRegistry);
1029
1122
  let usage;
1030
1123
  let iterations = 0;
1124
+ const llmRequestsDir = resolveLogDir(options.logLlmRequests, "requests");
1125
+ const llmResponsesDir = resolveLogDir(options.logLlmResponses, "responses");
1126
+ let llmCallCounter = 0;
1031
1127
  const countMessagesTokens = async (model, messages) => {
1032
1128
  try {
1033
1129
  return await client.countTokens(model, messages);
@@ -1050,12 +1146,18 @@ async function executeAgent(promptArg, options, env) {
1050
1146
  // onLLMCallStart: Start progress indicator for each LLM call
1051
1147
  // This showcases how to react to agent lifecycle events
1052
1148
  onLLMCallStart: async (context) => {
1149
+ llmCallCounter++;
1053
1150
  const inputTokens = await countMessagesTokens(
1054
1151
  context.options.model,
1055
1152
  context.options.messages
1056
1153
  );
1057
1154
  progress.startCall(context.options.model, inputTokens);
1058
1155
  progress.setInputTokens(inputTokens, false);
1156
+ if (llmRequestsDir) {
1157
+ const filename = `${Date.now()}_call_${llmCallCounter}.request.txt`;
1158
+ const content = formatLlmRequest(context.options.messages);
1159
+ await writeLogFile(llmRequestsDir, filename, content);
1160
+ }
1059
1161
  },
1060
1162
  // onStreamChunk: Real-time updates as LLM generates tokens
1061
1163
  // This enables responsive UIs that show progress during generation
@@ -1068,6 +1170,10 @@ async function executeAgent(promptArg, options, env) {
1068
1170
  if (context.usage.outputTokens) {
1069
1171
  progress.setOutputTokens(context.usage.outputTokens, false);
1070
1172
  }
1173
+ progress.setCachedTokens(
1174
+ context.usage.cachedInputTokens ?? 0,
1175
+ context.usage.cacheCreationInputTokens ?? 0
1176
+ );
1071
1177
  }
1072
1178
  },
1073
1179
  // onLLMCallComplete: Finalize metrics after each LLM call
@@ -1086,11 +1192,13 @@ async function executeAgent(promptArg, options, env) {
1086
1192
  let callCost;
1087
1193
  if (context.usage && client.modelRegistry) {
1088
1194
  try {
1089
- const modelName = options.model.includes(":") ? options.model.split(":")[1] : options.model;
1195
+ const modelName = context.options.model.includes(":") ? context.options.model.split(":")[1] : context.options.model;
1090
1196
  const costResult = client.modelRegistry.estimateCost(
1091
1197
  modelName,
1092
1198
  context.usage.inputTokens,
1093
- context.usage.outputTokens
1199
+ context.usage.outputTokens,
1200
+ context.usage.cachedInputTokens ?? 0,
1201
+ context.usage.cacheCreationInputTokens ?? 0
1094
1202
  );
1095
1203
  if (costResult) callCost = costResult.totalCost;
1096
1204
  } catch {
@@ -1098,7 +1206,7 @@ async function executeAgent(promptArg, options, env) {
1098
1206
  }
1099
1207
  const callElapsed = progress.getCallElapsedSeconds();
1100
1208
  progress.endCall(context.usage);
1101
- if (stderrTTY) {
1209
+ if (!options.quiet) {
1102
1210
  const summary = renderSummary({
1103
1211
  iterations: context.iteration + 1,
1104
1212
  model: options.model,
@@ -1112,6 +1220,10 @@ async function executeAgent(promptArg, options, env) {
1112
1220
  `);
1113
1221
  }
1114
1222
  }
1223
+ if (llmResponsesDir) {
1224
+ const filename = `${Date.now()}_call_${llmCallCounter}.response.txt`;
1225
+ await writeLogFile(llmResponsesDir, filename, context.rawResponse);
1226
+ }
1115
1227
  }
1116
1228
  },
1117
1229
  // SHOWCASE: Controller-based approval gating for dangerous gadgets
@@ -1176,13 +1288,15 @@ Command rejected by user with message: "${response}"`
1176
1288
  if (gadgets.length > 0) {
1177
1289
  builder.withGadgets(...gadgets);
1178
1290
  }
1179
- builder.withParameterFormat(options.parameterFormat);
1180
1291
  if (options.gadgetStartPrefix) {
1181
1292
  builder.withGadgetStartPrefix(options.gadgetStartPrefix);
1182
1293
  }
1183
1294
  if (options.gadgetEndPrefix) {
1184
1295
  builder.withGadgetEndPrefix(options.gadgetEndPrefix);
1185
1296
  }
1297
+ if (options.gadgetArgPrefix) {
1298
+ builder.withGadgetArgPrefix(options.gadgetArgPrefix);
1299
+ }
1186
1300
  builder.withSyntheticGadgetCall(
1187
1301
  "TellUser",
1188
1302
  {
@@ -1199,22 +1313,38 @@ Command rejected by user with message: "${response}"`
1199
1313
  resultMapping: (text) => `\u2139\uFE0F ${text}`
1200
1314
  });
1201
1315
  const agent = builder.ask(prompt);
1316
+ let textBuffer = "";
1317
+ const flushTextBuffer = () => {
1318
+ if (textBuffer) {
1319
+ const output = options.quiet ? textBuffer : renderMarkdownWithSeparators(textBuffer);
1320
+ printer.write(output);
1321
+ textBuffer = "";
1322
+ }
1323
+ };
1202
1324
  for await (const event of agent.run()) {
1203
1325
  if (event.type === "text") {
1204
1326
  progress.pause();
1205
- printer.write(event.content);
1327
+ textBuffer += event.content;
1206
1328
  } else if (event.type === "gadget_result") {
1329
+ flushTextBuffer();
1207
1330
  progress.pause();
1208
- if (stderrTTY) {
1331
+ if (options.quiet) {
1332
+ if (event.result.gadgetName === "TellUser" && event.result.parameters?.message) {
1333
+ const message = String(event.result.parameters.message);
1334
+ env.stdout.write(`${message}
1335
+ `);
1336
+ }
1337
+ } else {
1209
1338
  const tokenCount = await countGadgetOutputTokens(event.result.result);
1210
1339
  env.stderr.write(`${formatGadgetSummary({ ...event.result, tokenCount })}
1211
1340
  `);
1212
1341
  }
1213
1342
  }
1214
1343
  }
1344
+ flushTextBuffer();
1215
1345
  progress.complete();
1216
1346
  printer.ensureNewline();
1217
- if (stderrTTY && iterations > 1) {
1347
+ if (!options.quiet && iterations > 1) {
1218
1348
  env.stderr.write(`${chalk3.dim("\u2500".repeat(40))}
1219
1349
  `);
1220
1350
  const summary = renderOverallSummary({
@@ -1250,9 +1380,18 @@ async function executeComplete(promptArg, options, env) {
1250
1380
  builder.addSystem(options.system);
1251
1381
  }
1252
1382
  builder.addUser(prompt);
1383
+ const messages = builder.build();
1384
+ const llmRequestsDir = resolveLogDir(options.logLlmRequests, "requests");
1385
+ const llmResponsesDir = resolveLogDir(options.logLlmResponses, "responses");
1386
+ const timestamp = Date.now();
1387
+ if (llmRequestsDir) {
1388
+ const filename = `${timestamp}_complete.request.txt`;
1389
+ const content = formatLlmRequest(messages);
1390
+ await writeLogFile(llmRequestsDir, filename, content);
1391
+ }
1253
1392
  const stream = client.stream({
1254
1393
  model,
1255
- messages: builder.build(),
1394
+ messages,
1256
1395
  temperature: options.temperature,
1257
1396
  maxTokens: options.maxTokens
1258
1397
  });
@@ -1263,7 +1402,7 @@ async function executeComplete(promptArg, options, env) {
1263
1402
  progress.startCall(model, estimatedInputTokens);
1264
1403
  let finishReason;
1265
1404
  let usage;
1266
- let totalChars = 0;
1405
+ let accumulatedResponse = "";
1267
1406
  for await (const chunk of stream) {
1268
1407
  if (chunk.usage) {
1269
1408
  usage = chunk.usage;
@@ -1276,8 +1415,8 @@ async function executeComplete(promptArg, options, env) {
1276
1415
  }
1277
1416
  if (chunk.text) {
1278
1417
  progress.pause();
1279
- totalChars += chunk.text.length;
1280
- progress.update(totalChars);
1418
+ accumulatedResponse += chunk.text;
1419
+ progress.update(accumulatedResponse.length);
1281
1420
  printer.write(chunk.text);
1282
1421
  }
1283
1422
  if (chunk.finishReason !== void 0) {
@@ -1287,7 +1426,11 @@ async function executeComplete(promptArg, options, env) {
1287
1426
  progress.endCall(usage);
1288
1427
  progress.complete();
1289
1428
  printer.ensureNewline();
1290
- if (stderrTTY) {
1429
+ if (llmResponsesDir) {
1430
+ const filename = `${timestamp}_complete.response.txt`;
1431
+ await writeLogFile(llmResponsesDir, filename, accumulatedResponse);
1432
+ }
1433
+ if (stderrTTY && !options.quiet) {
1291
1434
  const summary = renderSummary({ finishReason, usage, cost: progress.getTotalCost() });
1292
1435
  if (summary) {
1293
1436
  env.stderr.write(`${summary}
@@ -1305,36 +1448,136 @@ function registerCompleteCommand(program, env, config) {
1305
1448
 
1306
1449
  // src/cli/config.ts
1307
1450
  import { existsSync, readFileSync } from "node:fs";
1308
- import { homedir } from "node:os";
1309
- import { join } from "node:path";
1451
+ import { homedir as homedir2 } from "node:os";
1452
+ import { join as join2 } from "node:path";
1310
1453
  import { load as parseToml } from "js-toml";
1454
+
1455
+ // src/cli/templates.ts
1456
+ import { Eta } from "eta";
1457
+ var TemplateError = class extends Error {
1458
+ constructor(message, promptName, configPath) {
1459
+ super(promptName ? `[prompts.${promptName}]: ${message}` : message);
1460
+ this.promptName = promptName;
1461
+ this.configPath = configPath;
1462
+ this.name = "TemplateError";
1463
+ }
1464
+ };
1465
+ function createTemplateEngine(prompts, configPath) {
1466
+ const eta = new Eta({
1467
+ views: "/",
1468
+ // Required but we use named templates
1469
+ autoEscape: false,
1470
+ // Don't escape - these are prompts, not HTML
1471
+ autoTrim: false
1472
+ // Preserve whitespace in prompts
1473
+ });
1474
+ for (const [name, template] of Object.entries(prompts)) {
1475
+ try {
1476
+ eta.loadTemplate(`@${name}`, template);
1477
+ } catch (error) {
1478
+ throw new TemplateError(
1479
+ error instanceof Error ? error.message : String(error),
1480
+ name,
1481
+ configPath
1482
+ );
1483
+ }
1484
+ }
1485
+ return eta;
1486
+ }
1487
+ function resolveTemplate(eta, template, context = {}, configPath) {
1488
+ try {
1489
+ const fullContext = {
1490
+ ...context,
1491
+ env: process.env
1492
+ };
1493
+ return eta.renderString(template, fullContext);
1494
+ } catch (error) {
1495
+ throw new TemplateError(
1496
+ error instanceof Error ? error.message : String(error),
1497
+ void 0,
1498
+ configPath
1499
+ );
1500
+ }
1501
+ }
1502
+ function validatePrompts(prompts, configPath) {
1503
+ const eta = createTemplateEngine(prompts, configPath);
1504
+ for (const [name, template] of Object.entries(prompts)) {
1505
+ try {
1506
+ eta.renderString(template, { env: {} });
1507
+ } catch (error) {
1508
+ throw new TemplateError(
1509
+ error instanceof Error ? error.message : String(error),
1510
+ name,
1511
+ configPath
1512
+ );
1513
+ }
1514
+ }
1515
+ }
1516
+ function validateEnvVars(template, promptName, configPath) {
1517
+ const envVarPattern = /<%=\s*it\.env\.(\w+)\s*%>/g;
1518
+ const matches = template.matchAll(envVarPattern);
1519
+ for (const match of matches) {
1520
+ const varName = match[1];
1521
+ if (process.env[varName] === void 0) {
1522
+ throw new TemplateError(
1523
+ `Environment variable '${varName}' is not set`,
1524
+ promptName,
1525
+ configPath
1526
+ );
1527
+ }
1528
+ }
1529
+ }
1530
+ function hasTemplateSyntax(str) {
1531
+ return str.includes("<%");
1532
+ }
1533
+
1534
+ // src/cli/config.ts
1311
1535
  var GLOBAL_CONFIG_KEYS = /* @__PURE__ */ new Set(["log-level", "log-file", "log-reset"]);
1312
1536
  var VALID_LOG_LEVELS = ["silly", "trace", "debug", "info", "warn", "error", "fatal"];
1313
- var COMPLETE_CONFIG_KEYS = /* @__PURE__ */ new Set(["model", "system", "temperature", "max-tokens"]);
1537
+ var COMPLETE_CONFIG_KEYS = /* @__PURE__ */ new Set([
1538
+ "model",
1539
+ "system",
1540
+ "temperature",
1541
+ "max-tokens",
1542
+ "quiet",
1543
+ "inherits",
1544
+ "log-level",
1545
+ "log-file",
1546
+ "log-reset",
1547
+ "log-llm-requests",
1548
+ "log-llm-responses",
1549
+ "type"
1550
+ // Allowed for inheritance compatibility, ignored for built-in commands
1551
+ ]);
1314
1552
  var AGENT_CONFIG_KEYS = /* @__PURE__ */ new Set([
1315
1553
  "model",
1316
1554
  "system",
1317
1555
  "temperature",
1318
1556
  "max-iterations",
1319
1557
  "gadget",
1320
- "parameter-format",
1321
1558
  "builtins",
1322
1559
  "builtin-interaction",
1323
1560
  "gadget-start-prefix",
1324
- "gadget-end-prefix"
1561
+ "gadget-end-prefix",
1562
+ "gadget-arg-prefix",
1563
+ "quiet",
1564
+ "inherits",
1565
+ "log-level",
1566
+ "log-file",
1567
+ "log-reset",
1568
+ "log-llm-requests",
1569
+ "log-llm-responses",
1570
+ "type"
1571
+ // Allowed for inheritance compatibility, ignored for built-in commands
1325
1572
  ]);
1326
1573
  var CUSTOM_CONFIG_KEYS = /* @__PURE__ */ new Set([
1327
1574
  ...COMPLETE_CONFIG_KEYS,
1328
1575
  ...AGENT_CONFIG_KEYS,
1329
1576
  "type",
1330
- "description",
1331
- "log-level",
1332
- "log-file",
1333
- "log-reset"
1577
+ "description"
1334
1578
  ]);
1335
- var VALID_PARAMETER_FORMATS = ["json", "yaml", "toml", "auto"];
1336
1579
  function getConfigPath() {
1337
- return join(homedir(), ".llmist", "cli.toml");
1580
+ return join2(homedir2(), ".llmist", "cli.toml");
1338
1581
  }
1339
1582
  var ConfigError = class extends Error {
1340
1583
  constructor(message, path2) {
@@ -1381,6 +1624,39 @@ function validateStringArray(value, key, section) {
1381
1624
  }
1382
1625
  return value;
1383
1626
  }
1627
+ function validateInherits(value, section) {
1628
+ if (typeof value === "string") {
1629
+ return value;
1630
+ }
1631
+ if (Array.isArray(value)) {
1632
+ for (let i = 0; i < value.length; i++) {
1633
+ if (typeof value[i] !== "string") {
1634
+ throw new ConfigError(`[${section}].inherits[${i}] must be a string`);
1635
+ }
1636
+ }
1637
+ return value;
1638
+ }
1639
+ throw new ConfigError(`[${section}].inherits must be a string or array of strings`);
1640
+ }
1641
+ function validateLoggingConfig(raw, section) {
1642
+ const result = {};
1643
+ if ("log-level" in raw) {
1644
+ const level = validateString(raw["log-level"], "log-level", section);
1645
+ if (!VALID_LOG_LEVELS.includes(level)) {
1646
+ throw new ConfigError(
1647
+ `[${section}].log-level must be one of: ${VALID_LOG_LEVELS.join(", ")}`
1648
+ );
1649
+ }
1650
+ result["log-level"] = level;
1651
+ }
1652
+ if ("log-file" in raw) {
1653
+ result["log-file"] = validateString(raw["log-file"], "log-file", section);
1654
+ }
1655
+ if ("log-reset" in raw) {
1656
+ result["log-reset"] = validateBoolean(raw["log-reset"], "log-reset", section);
1657
+ }
1658
+ return result;
1659
+ }
1384
1660
  function validateBaseConfig(raw, section) {
1385
1661
  const result = {};
1386
1662
  if ("model" in raw) {
@@ -1395,6 +1671,9 @@ function validateBaseConfig(raw, section) {
1395
1671
  max: 2
1396
1672
  });
1397
1673
  }
1674
+ if ("inherits" in raw) {
1675
+ result.inherits = validateInherits(raw.inherits, section);
1676
+ }
1398
1677
  return result;
1399
1678
  }
1400
1679
  function validateGlobalConfig(raw, section) {
@@ -1407,23 +1686,7 @@ function validateGlobalConfig(raw, section) {
1407
1686
  throw new ConfigError(`[${section}].${key} is not a valid option`);
1408
1687
  }
1409
1688
  }
1410
- const result = {};
1411
- if ("log-level" in rawObj) {
1412
- const level = validateString(rawObj["log-level"], "log-level", section);
1413
- if (!VALID_LOG_LEVELS.includes(level)) {
1414
- throw new ConfigError(
1415
- `[${section}].log-level must be one of: ${VALID_LOG_LEVELS.join(", ")}`
1416
- );
1417
- }
1418
- result["log-level"] = level;
1419
- }
1420
- if ("log-file" in rawObj) {
1421
- result["log-file"] = validateString(rawObj["log-file"], "log-file", section);
1422
- }
1423
- if ("log-reset" in rawObj) {
1424
- result["log-reset"] = validateBoolean(rawObj["log-reset"], "log-reset", section);
1425
- }
1426
- return result;
1689
+ return validateLoggingConfig(rawObj, section);
1427
1690
  }
1428
1691
  function validateCompleteConfig(raw, section) {
1429
1692
  if (typeof raw !== "object" || raw === null) {
@@ -1435,13 +1698,33 @@ function validateCompleteConfig(raw, section) {
1435
1698
  throw new ConfigError(`[${section}].${key} is not a valid option`);
1436
1699
  }
1437
1700
  }
1438
- const result = { ...validateBaseConfig(rawObj, section) };
1701
+ const result = {
1702
+ ...validateBaseConfig(rawObj, section),
1703
+ ...validateLoggingConfig(rawObj, section)
1704
+ };
1439
1705
  if ("max-tokens" in rawObj) {
1440
1706
  result["max-tokens"] = validateNumber(rawObj["max-tokens"], "max-tokens", section, {
1441
1707
  integer: true,
1442
1708
  min: 1
1443
1709
  });
1444
1710
  }
1711
+ if ("quiet" in rawObj) {
1712
+ result.quiet = validateBoolean(rawObj.quiet, "quiet", section);
1713
+ }
1714
+ if ("log-llm-requests" in rawObj) {
1715
+ result["log-llm-requests"] = validateStringOrBoolean(
1716
+ rawObj["log-llm-requests"],
1717
+ "log-llm-requests",
1718
+ section
1719
+ );
1720
+ }
1721
+ if ("log-llm-responses" in rawObj) {
1722
+ result["log-llm-responses"] = validateStringOrBoolean(
1723
+ rawObj["log-llm-responses"],
1724
+ "log-llm-responses",
1725
+ section
1726
+ );
1727
+ }
1445
1728
  return result;
1446
1729
  }
1447
1730
  function validateAgentConfig(raw, section) {
@@ -1454,7 +1737,10 @@ function validateAgentConfig(raw, section) {
1454
1737
  throw new ConfigError(`[${section}].${key} is not a valid option`);
1455
1738
  }
1456
1739
  }
1457
- const result = { ...validateBaseConfig(rawObj, section) };
1740
+ const result = {
1741
+ ...validateBaseConfig(rawObj, section),
1742
+ ...validateLoggingConfig(rawObj, section)
1743
+ };
1458
1744
  if ("max-iterations" in rawObj) {
1459
1745
  result["max-iterations"] = validateNumber(rawObj["max-iterations"], "max-iterations", section, {
1460
1746
  integer: true,
@@ -1464,15 +1750,6 @@ function validateAgentConfig(raw, section) {
1464
1750
  if ("gadget" in rawObj) {
1465
1751
  result.gadget = validateStringArray(rawObj.gadget, "gadget", section);
1466
1752
  }
1467
- if ("parameter-format" in rawObj) {
1468
- const format = validateString(rawObj["parameter-format"], "parameter-format", section);
1469
- if (!VALID_PARAMETER_FORMATS.includes(format)) {
1470
- throw new ConfigError(
1471
- `[${section}].parameter-format must be one of: ${VALID_PARAMETER_FORMATS.join(", ")}`
1472
- );
1473
- }
1474
- result["parameter-format"] = format;
1475
- }
1476
1753
  if ("builtins" in rawObj) {
1477
1754
  result.builtins = validateBoolean(rawObj.builtins, "builtins", section);
1478
1755
  }
@@ -1497,8 +1774,38 @@ function validateAgentConfig(raw, section) {
1497
1774
  section
1498
1775
  );
1499
1776
  }
1777
+ if ("gadget-arg-prefix" in rawObj) {
1778
+ result["gadget-arg-prefix"] = validateString(
1779
+ rawObj["gadget-arg-prefix"],
1780
+ "gadget-arg-prefix",
1781
+ section
1782
+ );
1783
+ }
1784
+ if ("quiet" in rawObj) {
1785
+ result.quiet = validateBoolean(rawObj.quiet, "quiet", section);
1786
+ }
1787
+ if ("log-llm-requests" in rawObj) {
1788
+ result["log-llm-requests"] = validateStringOrBoolean(
1789
+ rawObj["log-llm-requests"],
1790
+ "log-llm-requests",
1791
+ section
1792
+ );
1793
+ }
1794
+ if ("log-llm-responses" in rawObj) {
1795
+ result["log-llm-responses"] = validateStringOrBoolean(
1796
+ rawObj["log-llm-responses"],
1797
+ "log-llm-responses",
1798
+ section
1799
+ );
1800
+ }
1500
1801
  return result;
1501
1802
  }
1803
+ function validateStringOrBoolean(value, field, section) {
1804
+ if (typeof value === "string" || typeof value === "boolean") {
1805
+ return value;
1806
+ }
1807
+ throw new ConfigError(`[${section}].${field} must be a string or boolean`);
1808
+ }
1502
1809
  function validateCustomConfig(raw, section) {
1503
1810
  if (typeof raw !== "object" || raw === null) {
1504
1811
  throw new ConfigError(`[${section}] must be a table`);
@@ -1533,15 +1840,6 @@ function validateCustomConfig(raw, section) {
1533
1840
  if ("gadget" in rawObj) {
1534
1841
  result.gadget = validateStringArray(rawObj.gadget, "gadget", section);
1535
1842
  }
1536
- if ("parameter-format" in rawObj) {
1537
- const format = validateString(rawObj["parameter-format"], "parameter-format", section);
1538
- if (!VALID_PARAMETER_FORMATS.includes(format)) {
1539
- throw new ConfigError(
1540
- `[${section}].parameter-format must be one of: ${VALID_PARAMETER_FORMATS.join(", ")}`
1541
- );
1542
- }
1543
- result["parameter-format"] = format;
1544
- }
1545
1843
  if ("builtins" in rawObj) {
1546
1844
  result.builtins = validateBoolean(rawObj.builtins, "builtins", section);
1547
1845
  }
@@ -1566,26 +1864,35 @@ function validateCustomConfig(raw, section) {
1566
1864
  section
1567
1865
  );
1568
1866
  }
1867
+ if ("gadget-arg-prefix" in rawObj) {
1868
+ result["gadget-arg-prefix"] = validateString(
1869
+ rawObj["gadget-arg-prefix"],
1870
+ "gadget-arg-prefix",
1871
+ section
1872
+ );
1873
+ }
1569
1874
  if ("max-tokens" in rawObj) {
1570
1875
  result["max-tokens"] = validateNumber(rawObj["max-tokens"], "max-tokens", section, {
1571
1876
  integer: true,
1572
1877
  min: 1
1573
1878
  });
1574
1879
  }
1575
- if ("log-level" in rawObj) {
1576
- const level = validateString(rawObj["log-level"], "log-level", section);
1577
- if (!VALID_LOG_LEVELS.includes(level)) {
1578
- throw new ConfigError(
1579
- `[${section}].log-level must be one of: ${VALID_LOG_LEVELS.join(", ")}`
1580
- );
1581
- }
1582
- result["log-level"] = level;
1880
+ if ("quiet" in rawObj) {
1881
+ result.quiet = validateBoolean(rawObj.quiet, "quiet", section);
1583
1882
  }
1584
- if ("log-file" in rawObj) {
1585
- result["log-file"] = validateString(rawObj["log-file"], "log-file", section);
1883
+ Object.assign(result, validateLoggingConfig(rawObj, section));
1884
+ return result;
1885
+ }
1886
+ function validatePromptsConfig(raw, section) {
1887
+ if (typeof raw !== "object" || raw === null) {
1888
+ throw new ConfigError(`[${section}] must be a table`);
1586
1889
  }
1587
- if ("log-reset" in rawObj) {
1588
- result["log-reset"] = validateBoolean(rawObj["log-reset"], "log-reset", section);
1890
+ const result = {};
1891
+ for (const [key, value] of Object.entries(raw)) {
1892
+ if (typeof value !== "string") {
1893
+ throw new ConfigError(`[${section}].${key} must be a string`);
1894
+ }
1895
+ result[key] = value;
1589
1896
  }
1590
1897
  return result;
1591
1898
  }
@@ -1603,6 +1910,8 @@ function validateConfig(raw, configPath) {
1603
1910
  result.complete = validateCompleteConfig(value, key);
1604
1911
  } else if (key === "agent") {
1605
1912
  result.agent = validateAgentConfig(value, key);
1913
+ } else if (key === "prompts") {
1914
+ result.prompts = validatePromptsConfig(value, key);
1606
1915
  } else {
1607
1916
  result[key] = validateCustomConfig(value, key);
1608
1917
  }
@@ -1638,12 +1947,119 @@ function loadConfig() {
1638
1947
  configPath
1639
1948
  );
1640
1949
  }
1641
- return validateConfig(raw, configPath);
1950
+ const validated = validateConfig(raw, configPath);
1951
+ const inherited = resolveInheritance(validated, configPath);
1952
+ return resolveTemplatesInConfig(inherited, configPath);
1642
1953
  }
1643
1954
  function getCustomCommandNames(config) {
1644
- const reserved = /* @__PURE__ */ new Set(["global", "complete", "agent"]);
1955
+ const reserved = /* @__PURE__ */ new Set(["global", "complete", "agent", "prompts"]);
1645
1956
  return Object.keys(config).filter((key) => !reserved.has(key));
1646
1957
  }
1958
+ function resolveTemplatesInConfig(config, configPath) {
1959
+ const prompts = config.prompts ?? {};
1960
+ const hasPrompts = Object.keys(prompts).length > 0;
1961
+ let hasTemplates = false;
1962
+ for (const [sectionName, section] of Object.entries(config)) {
1963
+ if (sectionName === "global" || sectionName === "prompts") continue;
1964
+ if (!section || typeof section !== "object") continue;
1965
+ const sectionObj = section;
1966
+ if (typeof sectionObj.system === "string" && hasTemplateSyntax(sectionObj.system)) {
1967
+ hasTemplates = true;
1968
+ break;
1969
+ }
1970
+ }
1971
+ for (const template of Object.values(prompts)) {
1972
+ if (hasTemplateSyntax(template)) {
1973
+ hasTemplates = true;
1974
+ break;
1975
+ }
1976
+ }
1977
+ if (!hasPrompts && !hasTemplates) {
1978
+ return config;
1979
+ }
1980
+ try {
1981
+ validatePrompts(prompts, configPath);
1982
+ } catch (error) {
1983
+ if (error instanceof TemplateError) {
1984
+ throw new ConfigError(error.message, configPath);
1985
+ }
1986
+ throw error;
1987
+ }
1988
+ for (const [name, template] of Object.entries(prompts)) {
1989
+ try {
1990
+ validateEnvVars(template, name, configPath);
1991
+ } catch (error) {
1992
+ if (error instanceof TemplateError) {
1993
+ throw new ConfigError(error.message, configPath);
1994
+ }
1995
+ throw error;
1996
+ }
1997
+ }
1998
+ const eta = createTemplateEngine(prompts, configPath);
1999
+ const result = { ...config };
2000
+ for (const [sectionName, section] of Object.entries(config)) {
2001
+ if (sectionName === "global" || sectionName === "prompts") continue;
2002
+ if (!section || typeof section !== "object") continue;
2003
+ const sectionObj = section;
2004
+ if (typeof sectionObj.system === "string" && hasTemplateSyntax(sectionObj.system)) {
2005
+ try {
2006
+ validateEnvVars(sectionObj.system, void 0, configPath);
2007
+ } catch (error) {
2008
+ if (error instanceof TemplateError) {
2009
+ throw new ConfigError(`[${sectionName}].system: ${error.message}`, configPath);
2010
+ }
2011
+ throw error;
2012
+ }
2013
+ try {
2014
+ const resolved = resolveTemplate(eta, sectionObj.system, {}, configPath);
2015
+ result[sectionName] = {
2016
+ ...sectionObj,
2017
+ system: resolved
2018
+ };
2019
+ } catch (error) {
2020
+ if (error instanceof TemplateError) {
2021
+ throw new ConfigError(`[${sectionName}].system: ${error.message}`, configPath);
2022
+ }
2023
+ throw error;
2024
+ }
2025
+ }
2026
+ }
2027
+ return result;
2028
+ }
2029
+ function resolveInheritance(config, configPath) {
2030
+ const resolved = {};
2031
+ const resolving = /* @__PURE__ */ new Set();
2032
+ function resolveSection(name) {
2033
+ if (name in resolved) {
2034
+ return resolved[name];
2035
+ }
2036
+ if (resolving.has(name)) {
2037
+ throw new ConfigError(`Circular inheritance detected: ${name}`, configPath);
2038
+ }
2039
+ const section = config[name];
2040
+ if (section === void 0 || typeof section !== "object") {
2041
+ throw new ConfigError(`Cannot inherit from unknown section: ${name}`, configPath);
2042
+ }
2043
+ resolving.add(name);
2044
+ const sectionObj = section;
2045
+ const inheritsRaw = sectionObj.inherits;
2046
+ const inheritsList = inheritsRaw ? Array.isArray(inheritsRaw) ? inheritsRaw : [inheritsRaw] : [];
2047
+ let merged = {};
2048
+ for (const parent of inheritsList) {
2049
+ const parentResolved = resolveSection(parent);
2050
+ merged = { ...merged, ...parentResolved };
2051
+ }
2052
+ const { inherits: _inherits, ...ownValues } = sectionObj;
2053
+ merged = { ...merged, ...ownValues };
2054
+ resolving.delete(name);
2055
+ resolved[name] = merged;
2056
+ return merged;
2057
+ }
2058
+ for (const name of Object.keys(config)) {
2059
+ resolveSection(name);
2060
+ }
2061
+ return resolved;
2062
+ }
1647
2063
 
1648
2064
  // src/cli/models-command.ts
1649
2065
  import chalk4 from "chalk";
@@ -1940,7 +2356,7 @@ function registerCustomCommand(program, name, config, env) {
1940
2356
  function parseLogLevel(value) {
1941
2357
  const normalized = value.toLowerCase();
1942
2358
  if (!LOG_LEVELS.includes(normalized)) {
1943
- throw new InvalidArgumentError3(`Log level must be one of: ${LOG_LEVELS.join(", ")}`);
2359
+ throw new InvalidArgumentError2(`Log level must be one of: ${LOG_LEVELS.join(", ")}`);
1944
2360
  }
1945
2361
  return normalized;
1946
2362
  }