llmist 0.7.0 → 0.8.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-62M4TDAK.js";
26
26
 
27
27
  // src/cli/constants.ts
28
28
  var CLI_NAME = "llmist";
@@ -47,7 +47,8 @@ var OPTION_FLAGS = {
47
47
  logFile: "--log-file <path>",
48
48
  logReset: "--log-reset",
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.",
@@ -61,7 +62,8 @@ var OPTION_DESCRIPTIONS = {
61
62
  logFile: "Path to log file. When set, logs are written to file instead of stderr.",
62
63
  logReset: "Reset (truncate) the log file at session start instead of appending.",
63
64
  noBuiltins: "Disable built-in gadgets (AskUser, TellUser).",
64
- noBuiltinInteraction: "Disable interactive gadgets (AskUser) while keeping TellUser."
65
+ noBuiltinInteraction: "Disable interactive gadgets (AskUser) while keeping TellUser.",
66
+ quiet: "Suppress all output except content (text and TellUser messages)."
65
67
  };
66
68
  var SUMMARY_PREFIX = "[llmist]";
67
69
 
@@ -71,7 +73,7 @@ import { Command, InvalidArgumentError as InvalidArgumentError3 } from "commande
71
73
  // package.json
72
74
  var package_default = {
73
75
  name: "llmist",
74
- version: "0.6.2",
76
+ version: "0.7.0",
75
77
  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
78
  type: "module",
77
79
  main: "dist/index.cjs",
@@ -155,6 +157,7 @@ var package_default = {
155
157
  "@google/genai": "^1.27.0",
156
158
  chalk: "^5.6.2",
157
159
  commander: "^12.1.0",
160
+ eta: "^4.4.1",
158
161
  "js-toml": "^1.0.2",
159
162
  "js-yaml": "^4.1.0",
160
163
  marked: "^15.0.12",
@@ -274,12 +277,19 @@ import fs from "node:fs";
274
277
  import path from "node:path";
275
278
  import { pathToFileURL } from "node:url";
276
279
  var PATH_PREFIXES = [".", "/", "~"];
280
+ function isGadgetLike(value) {
281
+ if (typeof value !== "object" || value === null) {
282
+ return false;
283
+ }
284
+ const obj = value;
285
+ return typeof obj.execute === "function" && typeof obj.description === "string" && ("parameterSchema" in obj || "schema" in obj);
286
+ }
277
287
  function isGadgetConstructor(value) {
278
288
  if (typeof value !== "function") {
279
289
  return false;
280
290
  }
281
291
  const prototype = value.prototype;
282
- return Boolean(prototype) && prototype instanceof BaseGadget;
292
+ return Boolean(prototype) && (prototype instanceof BaseGadget || isGadgetLike(prototype));
283
293
  }
284
294
  function expandHomePath(input) {
285
295
  if (!input.startsWith("~")) {
@@ -316,7 +326,7 @@ function extractGadgetsFromModule(moduleExports) {
316
326
  return;
317
327
  }
318
328
  visited.add(value);
319
- if (value instanceof BaseGadget) {
329
+ if (value instanceof BaseGadget || isGadgetLike(value)) {
320
330
  results.push(value);
321
331
  return;
322
332
  }
@@ -441,8 +451,14 @@ function renderSummary(metadata) {
441
451
  parts.push(chalk.magenta(metadata.model));
442
452
  }
443
453
  if (metadata.usage) {
444
- const { inputTokens, outputTokens } = metadata.usage;
454
+ const { inputTokens, outputTokens, cachedInputTokens, cacheCreationInputTokens } = metadata.usage;
445
455
  parts.push(chalk.dim("\u2191") + chalk.yellow(` ${formatTokens(inputTokens)}`));
456
+ if (cachedInputTokens && cachedInputTokens > 0) {
457
+ parts.push(chalk.dim("\u27F3") + chalk.blue(` ${formatTokens(cachedInputTokens)}`));
458
+ }
459
+ if (cacheCreationInputTokens && cacheCreationInputTokens > 0) {
460
+ parts.push(chalk.dim("\u270E") + chalk.magenta(` ${formatTokens(cacheCreationInputTokens)}`));
461
+ }
446
462
  parts.push(chalk.dim("\u2193") + chalk.green(` ${formatTokens(outputTokens)}`));
447
463
  }
448
464
  if (metadata.elapsedSeconds !== void 0 && metadata.elapsedSeconds > 0) {
@@ -611,6 +627,9 @@ var StreamProgress = class {
611
627
  callOutputTokensEstimated = true;
612
628
  callOutputChars = 0;
613
629
  isStreaming = false;
630
+ // Cache token tracking for live cost estimation during streaming
631
+ callCachedInputTokens = 0;
632
+ callCacheCreationInputTokens = 0;
614
633
  // Cumulative stats (cumulative mode)
615
634
  totalStartTime = Date.now();
616
635
  totalTokens = 0;
@@ -636,11 +655,13 @@ var StreamProgress = class {
636
655
  this.callOutputTokensEstimated = true;
637
656
  this.callOutputChars = 0;
638
657
  this.isStreaming = false;
658
+ this.callCachedInputTokens = 0;
659
+ this.callCacheCreationInputTokens = 0;
639
660
  this.start();
640
661
  }
641
662
  /**
642
663
  * Ends the current LLM call. Updates cumulative stats and switches to cumulative mode.
643
- * @param usage - Final token usage from the call
664
+ * @param usage - Final token usage from the call (including cached tokens if available)
644
665
  */
645
666
  endCall(usage) {
646
667
  this.iterations++;
@@ -652,7 +673,9 @@ var StreamProgress = class {
652
673
  const cost = this.modelRegistry.estimateCost(
653
674
  modelName,
654
675
  usage.inputTokens,
655
- usage.outputTokens
676
+ usage.outputTokens,
677
+ usage.cachedInputTokens ?? 0,
678
+ usage.cacheCreationInputTokens ?? 0
656
679
  );
657
680
  if (cost) {
658
681
  this.totalCost += cost.totalCost;
@@ -692,6 +715,16 @@ var StreamProgress = class {
692
715
  this.callOutputTokens = tokens;
693
716
  this.callOutputTokensEstimated = estimated;
694
717
  }
718
+ /**
719
+ * Sets cached token counts for the current call (from stream metadata).
720
+ * Used for live cost estimation during streaming.
721
+ * @param cachedInputTokens - Number of tokens read from cache (cheaper)
722
+ * @param cacheCreationInputTokens - Number of tokens written to cache (more expensive)
723
+ */
724
+ setCachedTokens(cachedInputTokens, cacheCreationInputTokens) {
725
+ this.callCachedInputTokens = cachedInputTokens;
726
+ this.callCacheCreationInputTokens = cacheCreationInputTokens;
727
+ }
695
728
  /**
696
729
  * Get total elapsed time in seconds since the first call started.
697
730
  * @returns Elapsed time in seconds with 1 decimal place
@@ -756,11 +789,32 @@ var StreamProgress = class {
756
789
  parts.push(chalk2.dim("\u2193") + chalk2.green(` ${prefix}${formatTokens(outTokens)}`));
757
790
  }
758
791
  parts.push(chalk2.dim(`${elapsed}s`));
759
- if (this.totalCost > 0) {
760
- parts.push(chalk2.cyan(`$${formatCost(this.totalCost)}`));
792
+ const callCost = this.calculateCurrentCallCost(outTokens);
793
+ if (callCost > 0) {
794
+ parts.push(chalk2.cyan(`$${formatCost(callCost)}`));
761
795
  }
762
796
  this.target.write(`\r${parts.join(chalk2.dim(" | "))} ${chalk2.cyan(spinner)}`);
763
797
  }
798
+ /**
799
+ * Calculates live cost estimate for the current streaming call.
800
+ * Uses current input/output tokens and cached token counts.
801
+ */
802
+ calculateCurrentCallCost(outputTokens) {
803
+ if (!this.modelRegistry || !this.model) return 0;
804
+ try {
805
+ const modelName = this.model.includes(":") ? this.model.split(":")[1] : this.model;
806
+ const cost = this.modelRegistry.estimateCost(
807
+ modelName,
808
+ this.callInputTokens,
809
+ outputTokens,
810
+ this.callCachedInputTokens,
811
+ this.callCacheCreationInputTokens
812
+ );
813
+ return cost?.totalCost ?? 0;
814
+ } catch {
815
+ return 0;
816
+ }
817
+ }
764
818
  renderCumulativeMode(spinner) {
765
819
  const elapsed = ((Date.now() - this.totalStartTime) / 1e3).toFixed(1);
766
820
  const parts = [];
@@ -909,7 +963,7 @@ function addCompleteOptions(cmd, defaults) {
909
963
  OPTION_DESCRIPTIONS.maxTokens,
910
964
  createNumericParser({ label: "Max tokens", integer: true, min: 1 }),
911
965
  defaults?.["max-tokens"]
912
- );
966
+ ).option(OPTION_FLAGS.quiet, OPTION_DESCRIPTIONS.quiet, defaults?.quiet);
913
967
  }
914
968
  function addAgentOptions(cmd, defaults) {
915
969
  const gadgetAccumulator = (value, previous = []) => [
@@ -938,7 +992,7 @@ function addAgentOptions(cmd, defaults) {
938
992
  OPTION_FLAGS.noBuiltinInteraction,
939
993
  OPTION_DESCRIPTIONS.noBuiltinInteraction,
940
994
  defaults?.["builtin-interaction"] !== false
941
- );
995
+ ).option(OPTION_FLAGS.quiet, OPTION_DESCRIPTIONS.quiet, defaults?.quiet);
942
996
  }
943
997
  function configToCompleteOptions(config) {
944
998
  const result = {};
@@ -946,6 +1000,7 @@ function configToCompleteOptions(config) {
946
1000
  if (config.system !== void 0) result.system = config.system;
947
1001
  if (config.temperature !== void 0) result.temperature = config.temperature;
948
1002
  if (config["max-tokens"] !== void 0) result.maxTokens = config["max-tokens"];
1003
+ if (config.quiet !== void 0) result.quiet = config.quiet;
949
1004
  return result;
950
1005
  }
951
1006
  function configToAgentOptions(config) {
@@ -963,6 +1018,7 @@ function configToAgentOptions(config) {
963
1018
  result.gadgetStartPrefix = config["gadget-start-prefix"];
964
1019
  if (config["gadget-end-prefix"] !== void 0)
965
1020
  result.gadgetEndPrefix = config["gadget-end-prefix"];
1021
+ if (config.quiet !== void 0) result.quiet = config.quiet;
966
1022
  return result;
967
1023
  }
968
1024
 
@@ -1008,9 +1064,10 @@ async function executeAgent(promptArg, options, env) {
1008
1064
  const prompt = await resolvePrompt(promptArg, env);
1009
1065
  const client = env.createClient();
1010
1066
  const registry = new GadgetRegistry();
1067
+ const stdinIsInteractive = isInteractive(env.stdin);
1011
1068
  if (options.builtins !== false) {
1012
1069
  for (const gadget of builtinGadgets) {
1013
- if (options.builtinInteraction === false && gadget.name === "AskUser") {
1070
+ if (gadget.name === "AskUser" && (options.builtinInteraction === false || !stdinIsInteractive)) {
1014
1071
  continue;
1015
1072
  }
1016
1073
  registry.registerByClass(gadget);
@@ -1068,6 +1125,10 @@ async function executeAgent(promptArg, options, env) {
1068
1125
  if (context.usage.outputTokens) {
1069
1126
  progress.setOutputTokens(context.usage.outputTokens, false);
1070
1127
  }
1128
+ progress.setCachedTokens(
1129
+ context.usage.cachedInputTokens ?? 0,
1130
+ context.usage.cacheCreationInputTokens ?? 0
1131
+ );
1071
1132
  }
1072
1133
  },
1073
1134
  // onLLMCallComplete: Finalize metrics after each LLM call
@@ -1086,11 +1147,13 @@ async function executeAgent(promptArg, options, env) {
1086
1147
  let callCost;
1087
1148
  if (context.usage && client.modelRegistry) {
1088
1149
  try {
1089
- const modelName = options.model.includes(":") ? options.model.split(":")[1] : options.model;
1150
+ const modelName = context.options.model.includes(":") ? context.options.model.split(":")[1] : context.options.model;
1090
1151
  const costResult = client.modelRegistry.estimateCost(
1091
1152
  modelName,
1092
1153
  context.usage.inputTokens,
1093
- context.usage.outputTokens
1154
+ context.usage.outputTokens,
1155
+ context.usage.cachedInputTokens ?? 0,
1156
+ context.usage.cacheCreationInputTokens ?? 0
1094
1157
  );
1095
1158
  if (costResult) callCost = costResult.totalCost;
1096
1159
  } catch {
@@ -1098,7 +1161,7 @@ async function executeAgent(promptArg, options, env) {
1098
1161
  }
1099
1162
  const callElapsed = progress.getCallElapsedSeconds();
1100
1163
  progress.endCall(context.usage);
1101
- if (stderrTTY) {
1164
+ if (!options.quiet) {
1102
1165
  const summary = renderSummary({
1103
1166
  iterations: context.iteration + 1,
1104
1167
  model: options.model,
@@ -1205,7 +1268,14 @@ Command rejected by user with message: "${response}"`
1205
1268
  printer.write(event.content);
1206
1269
  } else if (event.type === "gadget_result") {
1207
1270
  progress.pause();
1208
- if (stderrTTY) {
1271
+ if (options.quiet) {
1272
+ if (event.result.gadgetName === "TellUser" && event.result.parameters?.message) {
1273
+ const message = String(event.result.parameters.message);
1274
+ const rendered = renderMarkdown(message);
1275
+ env.stdout.write(`${rendered}
1276
+ `);
1277
+ }
1278
+ } else {
1209
1279
  const tokenCount = await countGadgetOutputTokens(event.result.result);
1210
1280
  env.stderr.write(`${formatGadgetSummary({ ...event.result, tokenCount })}
1211
1281
  `);
@@ -1214,7 +1284,7 @@ Command rejected by user with message: "${response}"`
1214
1284
  }
1215
1285
  progress.complete();
1216
1286
  printer.ensureNewline();
1217
- if (stderrTTY && iterations > 1) {
1287
+ if (!options.quiet && iterations > 1) {
1218
1288
  env.stderr.write(`${chalk3.dim("\u2500".repeat(40))}
1219
1289
  `);
1220
1290
  const summary = renderOverallSummary({
@@ -1287,7 +1357,7 @@ async function executeComplete(promptArg, options, env) {
1287
1357
  progress.endCall(usage);
1288
1358
  progress.complete();
1289
1359
  printer.ensureNewline();
1290
- if (stderrTTY) {
1360
+ if (stderrTTY && !options.quiet) {
1291
1361
  const summary = renderSummary({ finishReason, usage, cost: progress.getTotalCost() });
1292
1362
  if (summary) {
1293
1363
  env.stderr.write(`${summary}
@@ -1308,9 +1378,102 @@ import { existsSync, readFileSync } from "node:fs";
1308
1378
  import { homedir } from "node:os";
1309
1379
  import { join } from "node:path";
1310
1380
  import { load as parseToml } from "js-toml";
1381
+
1382
+ // src/cli/templates.ts
1383
+ import { Eta } from "eta";
1384
+ var TemplateError = class extends Error {
1385
+ constructor(message, promptName, configPath) {
1386
+ super(promptName ? `[prompts.${promptName}]: ${message}` : message);
1387
+ this.promptName = promptName;
1388
+ this.configPath = configPath;
1389
+ this.name = "TemplateError";
1390
+ }
1391
+ };
1392
+ function createTemplateEngine(prompts, configPath) {
1393
+ const eta = new Eta({
1394
+ views: "/",
1395
+ // Required but we use named templates
1396
+ autoEscape: false,
1397
+ // Don't escape - these are prompts, not HTML
1398
+ autoTrim: false
1399
+ // Preserve whitespace in prompts
1400
+ });
1401
+ for (const [name, template] of Object.entries(prompts)) {
1402
+ try {
1403
+ eta.loadTemplate(`@${name}`, template);
1404
+ } catch (error) {
1405
+ throw new TemplateError(
1406
+ error instanceof Error ? error.message : String(error),
1407
+ name,
1408
+ configPath
1409
+ );
1410
+ }
1411
+ }
1412
+ return eta;
1413
+ }
1414
+ function resolveTemplate(eta, template, context = {}, configPath) {
1415
+ try {
1416
+ const fullContext = {
1417
+ ...context,
1418
+ env: process.env
1419
+ };
1420
+ return eta.renderString(template, fullContext);
1421
+ } catch (error) {
1422
+ throw new TemplateError(
1423
+ error instanceof Error ? error.message : String(error),
1424
+ void 0,
1425
+ configPath
1426
+ );
1427
+ }
1428
+ }
1429
+ function validatePrompts(prompts, configPath) {
1430
+ const eta = createTemplateEngine(prompts, configPath);
1431
+ for (const [name, template] of Object.entries(prompts)) {
1432
+ try {
1433
+ eta.renderString(template, { env: {} });
1434
+ } catch (error) {
1435
+ throw new TemplateError(
1436
+ error instanceof Error ? error.message : String(error),
1437
+ name,
1438
+ configPath
1439
+ );
1440
+ }
1441
+ }
1442
+ }
1443
+ function validateEnvVars(template, promptName, configPath) {
1444
+ const envVarPattern = /<%=\s*it\.env\.(\w+)\s*%>/g;
1445
+ const matches = template.matchAll(envVarPattern);
1446
+ for (const match of matches) {
1447
+ const varName = match[1];
1448
+ if (process.env[varName] === void 0) {
1449
+ throw new TemplateError(
1450
+ `Environment variable '${varName}' is not set`,
1451
+ promptName,
1452
+ configPath
1453
+ );
1454
+ }
1455
+ }
1456
+ }
1457
+ function hasTemplateSyntax(str) {
1458
+ return str.includes("<%");
1459
+ }
1460
+
1461
+ // src/cli/config.ts
1311
1462
  var GLOBAL_CONFIG_KEYS = /* @__PURE__ */ new Set(["log-level", "log-file", "log-reset"]);
1312
1463
  var VALID_LOG_LEVELS = ["silly", "trace", "debug", "info", "warn", "error", "fatal"];
1313
- var COMPLETE_CONFIG_KEYS = /* @__PURE__ */ new Set(["model", "system", "temperature", "max-tokens"]);
1464
+ var COMPLETE_CONFIG_KEYS = /* @__PURE__ */ new Set([
1465
+ "model",
1466
+ "system",
1467
+ "temperature",
1468
+ "max-tokens",
1469
+ "quiet",
1470
+ "inherits",
1471
+ "log-level",
1472
+ "log-file",
1473
+ "log-reset",
1474
+ "type"
1475
+ // Allowed for inheritance compatibility, ignored for built-in commands
1476
+ ]);
1314
1477
  var AGENT_CONFIG_KEYS = /* @__PURE__ */ new Set([
1315
1478
  "model",
1316
1479
  "system",
@@ -1321,16 +1484,20 @@ var AGENT_CONFIG_KEYS = /* @__PURE__ */ new Set([
1321
1484
  "builtins",
1322
1485
  "builtin-interaction",
1323
1486
  "gadget-start-prefix",
1324
- "gadget-end-prefix"
1487
+ "gadget-end-prefix",
1488
+ "quiet",
1489
+ "inherits",
1490
+ "log-level",
1491
+ "log-file",
1492
+ "log-reset",
1493
+ "type"
1494
+ // Allowed for inheritance compatibility, ignored for built-in commands
1325
1495
  ]);
1326
1496
  var CUSTOM_CONFIG_KEYS = /* @__PURE__ */ new Set([
1327
1497
  ...COMPLETE_CONFIG_KEYS,
1328
1498
  ...AGENT_CONFIG_KEYS,
1329
1499
  "type",
1330
- "description",
1331
- "log-level",
1332
- "log-file",
1333
- "log-reset"
1500
+ "description"
1334
1501
  ]);
1335
1502
  var VALID_PARAMETER_FORMATS = ["json", "yaml", "toml", "auto"];
1336
1503
  function getConfigPath() {
@@ -1381,6 +1548,39 @@ function validateStringArray(value, key, section) {
1381
1548
  }
1382
1549
  return value;
1383
1550
  }
1551
+ function validateInherits(value, section) {
1552
+ if (typeof value === "string") {
1553
+ return value;
1554
+ }
1555
+ if (Array.isArray(value)) {
1556
+ for (let i = 0; i < value.length; i++) {
1557
+ if (typeof value[i] !== "string") {
1558
+ throw new ConfigError(`[${section}].inherits[${i}] must be a string`);
1559
+ }
1560
+ }
1561
+ return value;
1562
+ }
1563
+ throw new ConfigError(`[${section}].inherits must be a string or array of strings`);
1564
+ }
1565
+ function validateLoggingConfig(raw, section) {
1566
+ const result = {};
1567
+ if ("log-level" in raw) {
1568
+ const level = validateString(raw["log-level"], "log-level", section);
1569
+ if (!VALID_LOG_LEVELS.includes(level)) {
1570
+ throw new ConfigError(
1571
+ `[${section}].log-level must be one of: ${VALID_LOG_LEVELS.join(", ")}`
1572
+ );
1573
+ }
1574
+ result["log-level"] = level;
1575
+ }
1576
+ if ("log-file" in raw) {
1577
+ result["log-file"] = validateString(raw["log-file"], "log-file", section);
1578
+ }
1579
+ if ("log-reset" in raw) {
1580
+ result["log-reset"] = validateBoolean(raw["log-reset"], "log-reset", section);
1581
+ }
1582
+ return result;
1583
+ }
1384
1584
  function validateBaseConfig(raw, section) {
1385
1585
  const result = {};
1386
1586
  if ("model" in raw) {
@@ -1395,6 +1595,9 @@ function validateBaseConfig(raw, section) {
1395
1595
  max: 2
1396
1596
  });
1397
1597
  }
1598
+ if ("inherits" in raw) {
1599
+ result.inherits = validateInherits(raw.inherits, section);
1600
+ }
1398
1601
  return result;
1399
1602
  }
1400
1603
  function validateGlobalConfig(raw, section) {
@@ -1407,23 +1610,7 @@ function validateGlobalConfig(raw, section) {
1407
1610
  throw new ConfigError(`[${section}].${key} is not a valid option`);
1408
1611
  }
1409
1612
  }
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;
1613
+ return validateLoggingConfig(rawObj, section);
1427
1614
  }
1428
1615
  function validateCompleteConfig(raw, section) {
1429
1616
  if (typeof raw !== "object" || raw === null) {
@@ -1435,13 +1622,19 @@ function validateCompleteConfig(raw, section) {
1435
1622
  throw new ConfigError(`[${section}].${key} is not a valid option`);
1436
1623
  }
1437
1624
  }
1438
- const result = { ...validateBaseConfig(rawObj, section) };
1625
+ const result = {
1626
+ ...validateBaseConfig(rawObj, section),
1627
+ ...validateLoggingConfig(rawObj, section)
1628
+ };
1439
1629
  if ("max-tokens" in rawObj) {
1440
1630
  result["max-tokens"] = validateNumber(rawObj["max-tokens"], "max-tokens", section, {
1441
1631
  integer: true,
1442
1632
  min: 1
1443
1633
  });
1444
1634
  }
1635
+ if ("quiet" in rawObj) {
1636
+ result.quiet = validateBoolean(rawObj.quiet, "quiet", section);
1637
+ }
1445
1638
  return result;
1446
1639
  }
1447
1640
  function validateAgentConfig(raw, section) {
@@ -1454,7 +1647,10 @@ function validateAgentConfig(raw, section) {
1454
1647
  throw new ConfigError(`[${section}].${key} is not a valid option`);
1455
1648
  }
1456
1649
  }
1457
- const result = { ...validateBaseConfig(rawObj, section) };
1650
+ const result = {
1651
+ ...validateBaseConfig(rawObj, section),
1652
+ ...validateLoggingConfig(rawObj, section)
1653
+ };
1458
1654
  if ("max-iterations" in rawObj) {
1459
1655
  result["max-iterations"] = validateNumber(rawObj["max-iterations"], "max-iterations", section, {
1460
1656
  integer: true,
@@ -1497,6 +1693,9 @@ function validateAgentConfig(raw, section) {
1497
1693
  section
1498
1694
  );
1499
1695
  }
1696
+ if ("quiet" in rawObj) {
1697
+ result.quiet = validateBoolean(rawObj.quiet, "quiet", section);
1698
+ }
1500
1699
  return result;
1501
1700
  }
1502
1701
  function validateCustomConfig(raw, section) {
@@ -1572,20 +1771,22 @@ function validateCustomConfig(raw, section) {
1572
1771
  min: 1
1573
1772
  });
1574
1773
  }
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;
1774
+ if ("quiet" in rawObj) {
1775
+ result.quiet = validateBoolean(rawObj.quiet, "quiet", section);
1583
1776
  }
1584
- if ("log-file" in rawObj) {
1585
- result["log-file"] = validateString(rawObj["log-file"], "log-file", section);
1777
+ Object.assign(result, validateLoggingConfig(rawObj, section));
1778
+ return result;
1779
+ }
1780
+ function validatePromptsConfig(raw, section) {
1781
+ if (typeof raw !== "object" || raw === null) {
1782
+ throw new ConfigError(`[${section}] must be a table`);
1586
1783
  }
1587
- if ("log-reset" in rawObj) {
1588
- result["log-reset"] = validateBoolean(rawObj["log-reset"], "log-reset", section);
1784
+ const result = {};
1785
+ for (const [key, value] of Object.entries(raw)) {
1786
+ if (typeof value !== "string") {
1787
+ throw new ConfigError(`[${section}].${key} must be a string`);
1788
+ }
1789
+ result[key] = value;
1589
1790
  }
1590
1791
  return result;
1591
1792
  }
@@ -1603,6 +1804,8 @@ function validateConfig(raw, configPath) {
1603
1804
  result.complete = validateCompleteConfig(value, key);
1604
1805
  } else if (key === "agent") {
1605
1806
  result.agent = validateAgentConfig(value, key);
1807
+ } else if (key === "prompts") {
1808
+ result.prompts = validatePromptsConfig(value, key);
1606
1809
  } else {
1607
1810
  result[key] = validateCustomConfig(value, key);
1608
1811
  }
@@ -1638,12 +1841,119 @@ function loadConfig() {
1638
1841
  configPath
1639
1842
  );
1640
1843
  }
1641
- return validateConfig(raw, configPath);
1844
+ const validated = validateConfig(raw, configPath);
1845
+ const inherited = resolveInheritance(validated, configPath);
1846
+ return resolveTemplatesInConfig(inherited, configPath);
1642
1847
  }
1643
1848
  function getCustomCommandNames(config) {
1644
- const reserved = /* @__PURE__ */ new Set(["global", "complete", "agent"]);
1849
+ const reserved = /* @__PURE__ */ new Set(["global", "complete", "agent", "prompts"]);
1645
1850
  return Object.keys(config).filter((key) => !reserved.has(key));
1646
1851
  }
1852
+ function resolveTemplatesInConfig(config, configPath) {
1853
+ const prompts = config.prompts ?? {};
1854
+ const hasPrompts = Object.keys(prompts).length > 0;
1855
+ let hasTemplates = false;
1856
+ for (const [sectionName, section] of Object.entries(config)) {
1857
+ if (sectionName === "global" || sectionName === "prompts") continue;
1858
+ if (!section || typeof section !== "object") continue;
1859
+ const sectionObj = section;
1860
+ if (typeof sectionObj.system === "string" && hasTemplateSyntax(sectionObj.system)) {
1861
+ hasTemplates = true;
1862
+ break;
1863
+ }
1864
+ }
1865
+ for (const template of Object.values(prompts)) {
1866
+ if (hasTemplateSyntax(template)) {
1867
+ hasTemplates = true;
1868
+ break;
1869
+ }
1870
+ }
1871
+ if (!hasPrompts && !hasTemplates) {
1872
+ return config;
1873
+ }
1874
+ try {
1875
+ validatePrompts(prompts, configPath);
1876
+ } catch (error) {
1877
+ if (error instanceof TemplateError) {
1878
+ throw new ConfigError(error.message, configPath);
1879
+ }
1880
+ throw error;
1881
+ }
1882
+ for (const [name, template] of Object.entries(prompts)) {
1883
+ try {
1884
+ validateEnvVars(template, name, configPath);
1885
+ } catch (error) {
1886
+ if (error instanceof TemplateError) {
1887
+ throw new ConfigError(error.message, configPath);
1888
+ }
1889
+ throw error;
1890
+ }
1891
+ }
1892
+ const eta = createTemplateEngine(prompts, configPath);
1893
+ const result = { ...config };
1894
+ for (const [sectionName, section] of Object.entries(config)) {
1895
+ if (sectionName === "global" || sectionName === "prompts") continue;
1896
+ if (!section || typeof section !== "object") continue;
1897
+ const sectionObj = section;
1898
+ if (typeof sectionObj.system === "string" && hasTemplateSyntax(sectionObj.system)) {
1899
+ try {
1900
+ validateEnvVars(sectionObj.system, void 0, configPath);
1901
+ } catch (error) {
1902
+ if (error instanceof TemplateError) {
1903
+ throw new ConfigError(`[${sectionName}].system: ${error.message}`, configPath);
1904
+ }
1905
+ throw error;
1906
+ }
1907
+ try {
1908
+ const resolved = resolveTemplate(eta, sectionObj.system, {}, configPath);
1909
+ result[sectionName] = {
1910
+ ...sectionObj,
1911
+ system: resolved
1912
+ };
1913
+ } catch (error) {
1914
+ if (error instanceof TemplateError) {
1915
+ throw new ConfigError(`[${sectionName}].system: ${error.message}`, configPath);
1916
+ }
1917
+ throw error;
1918
+ }
1919
+ }
1920
+ }
1921
+ return result;
1922
+ }
1923
+ function resolveInheritance(config, configPath) {
1924
+ const resolved = {};
1925
+ const resolving = /* @__PURE__ */ new Set();
1926
+ function resolveSection(name) {
1927
+ if (name in resolved) {
1928
+ return resolved[name];
1929
+ }
1930
+ if (resolving.has(name)) {
1931
+ throw new ConfigError(`Circular inheritance detected: ${name}`, configPath);
1932
+ }
1933
+ const section = config[name];
1934
+ if (section === void 0 || typeof section !== "object") {
1935
+ throw new ConfigError(`Cannot inherit from unknown section: ${name}`, configPath);
1936
+ }
1937
+ resolving.add(name);
1938
+ const sectionObj = section;
1939
+ const inheritsRaw = sectionObj.inherits;
1940
+ const inheritsList = inheritsRaw ? Array.isArray(inheritsRaw) ? inheritsRaw : [inheritsRaw] : [];
1941
+ let merged = {};
1942
+ for (const parent of inheritsList) {
1943
+ const parentResolved = resolveSection(parent);
1944
+ merged = { ...merged, ...parentResolved };
1945
+ }
1946
+ const { inherits: _inherits, ...ownValues } = sectionObj;
1947
+ merged = { ...merged, ...ownValues };
1948
+ resolving.delete(name);
1949
+ resolved[name] = merged;
1950
+ return merged;
1951
+ }
1952
+ for (const name of Object.keys(config)) {
1953
+ resolveSection(name);
1954
+ }
1955
+ return resolved;
1956
+ }
1647
1957
 
1648
1958
  // src/cli/models-command.ts
1649
1959
  import chalk4 from "chalk";