llmist 0.6.2 → 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-DVK6ZQOV.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.1",
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",
@@ -219,7 +222,7 @@ var tellUser = createGadget({
219
222
  name: "TellUser",
220
223
  description: "Tell the user something important. Set done=true when your work is complete and you want to end the conversation.",
221
224
  schema: z.object({
222
- message: z.string().describe("The message to display to the user in Markdown"),
225
+ message: z.string().optional().describe("The message to display to the user in Markdown"),
223
226
  done: z.boolean().default(false).describe("Set to true to end the conversation, false to continue"),
224
227
  type: z.enum(["info", "success", "warning", "error"]).default("info").describe("Message type: info, success, warning, or error")
225
228
  }),
@@ -239,9 +242,20 @@ var tellUser = createGadget({
239
242
  done: false,
240
243
  type: "warning"
241
244
  }
245
+ },
246
+ {
247
+ comment: "Share detailed analysis with bullet points (use heredoc for multiline)",
248
+ params: {
249
+ message: "Here's what I found in the codebase:\n\n1. **Main entry point**: `src/index.ts` exports all public APIs\n2. **Core logic**: Located in `src/core/` with 5 modules\n3. **Tests**: Good coverage in `src/__tests__/`\n\nI'll continue exploring the core modules.",
250
+ done: false,
251
+ type: "info"
252
+ }
242
253
  }
243
254
  ],
244
255
  execute: ({ message, done, type }) => {
256
+ if (!message || message.trim() === "") {
257
+ return "\u26A0\uFE0F TellUser was called without a message. Please provide content in the 'message' field.";
258
+ }
245
259
  const prefixes = {
246
260
  info: "\u2139\uFE0F ",
247
261
  success: "\u2705 ",
@@ -263,12 +277,19 @@ import fs from "node:fs";
263
277
  import path from "node:path";
264
278
  import { pathToFileURL } from "node:url";
265
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
+ }
266
287
  function isGadgetConstructor(value) {
267
288
  if (typeof value !== "function") {
268
289
  return false;
269
290
  }
270
291
  const prototype = value.prototype;
271
- return Boolean(prototype) && prototype instanceof BaseGadget;
292
+ return Boolean(prototype) && (prototype instanceof BaseGadget || isGadgetLike(prototype));
272
293
  }
273
294
  function expandHomePath(input) {
274
295
  if (!input.startsWith("~")) {
@@ -305,7 +326,7 @@ function extractGadgetsFromModule(moduleExports) {
305
326
  return;
306
327
  }
307
328
  visited.add(value);
308
- if (value instanceof BaseGadget) {
329
+ if (value instanceof BaseGadget || isGadgetLike(value)) {
309
330
  results.push(value);
310
331
  return;
311
332
  }
@@ -430,8 +451,14 @@ function renderSummary(metadata) {
430
451
  parts.push(chalk.magenta(metadata.model));
431
452
  }
432
453
  if (metadata.usage) {
433
- const { inputTokens, outputTokens } = metadata.usage;
454
+ const { inputTokens, outputTokens, cachedInputTokens, cacheCreationInputTokens } = metadata.usage;
434
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
+ }
435
462
  parts.push(chalk.dim("\u2193") + chalk.green(` ${formatTokens(outputTokens)}`));
436
463
  }
437
464
  if (metadata.elapsedSeconds !== void 0 && metadata.elapsedSeconds > 0) {
@@ -523,53 +550,6 @@ ${rendered}`;
523
550
  }
524
551
 
525
552
  // src/cli/utils.ts
526
- var RARE_EMOJI = [
527
- "\u{1F531}",
528
- "\u2697\uFE0F",
529
- "\u{1F9FF}",
530
- "\u{1F530}",
531
- "\u269B\uFE0F",
532
- "\u{1F3FA}",
533
- "\u{1F9EB}",
534
- "\u{1F52C}",
535
- "\u2695\uFE0F",
536
- "\u{1F5DD}\uFE0F",
537
- "\u2696\uFE0F",
538
- "\u{1F52E}",
539
- "\u{1FAAC}",
540
- "\u{1F9EC}",
541
- "\u2699\uFE0F",
542
- "\u{1F529}",
543
- "\u{1FA9B}",
544
- "\u26CF\uFE0F",
545
- "\u{1FA83}",
546
- "\u{1F3F9}",
547
- "\u{1F6E1}\uFE0F",
548
- "\u2694\uFE0F",
549
- "\u{1F5E1}\uFE0F",
550
- "\u{1FA93}",
551
- "\u{1F5C3}\uFE0F",
552
- "\u{1F4DC}",
553
- "\u{1F4EF}",
554
- "\u{1F3B4}",
555
- "\u{1F004}",
556
- "\u{1F3B2}"
557
- ];
558
- function generateMarkers() {
559
- const pick = (count) => {
560
- const result = [];
561
- const pool = [...RARE_EMOJI];
562
- for (let i = 0; i < count && pool.length > 0; i++) {
563
- const idx = Math.floor(Math.random() * pool.length);
564
- result.push(pool.splice(idx, 1)[0]);
565
- }
566
- return result.join("");
567
- };
568
- return {
569
- startPrefix: pick(5),
570
- endPrefix: pick(5)
571
- };
572
- }
573
553
  function createNumericParser({
574
554
  label,
575
555
  integer = false,
@@ -647,6 +627,9 @@ var StreamProgress = class {
647
627
  callOutputTokensEstimated = true;
648
628
  callOutputChars = 0;
649
629
  isStreaming = false;
630
+ // Cache token tracking for live cost estimation during streaming
631
+ callCachedInputTokens = 0;
632
+ callCacheCreationInputTokens = 0;
650
633
  // Cumulative stats (cumulative mode)
651
634
  totalStartTime = Date.now();
652
635
  totalTokens = 0;
@@ -672,11 +655,13 @@ var StreamProgress = class {
672
655
  this.callOutputTokensEstimated = true;
673
656
  this.callOutputChars = 0;
674
657
  this.isStreaming = false;
658
+ this.callCachedInputTokens = 0;
659
+ this.callCacheCreationInputTokens = 0;
675
660
  this.start();
676
661
  }
677
662
  /**
678
663
  * Ends the current LLM call. Updates cumulative stats and switches to cumulative mode.
679
- * @param usage - Final token usage from the call
664
+ * @param usage - Final token usage from the call (including cached tokens if available)
680
665
  */
681
666
  endCall(usage) {
682
667
  this.iterations++;
@@ -688,7 +673,9 @@ var StreamProgress = class {
688
673
  const cost = this.modelRegistry.estimateCost(
689
674
  modelName,
690
675
  usage.inputTokens,
691
- usage.outputTokens
676
+ usage.outputTokens,
677
+ usage.cachedInputTokens ?? 0,
678
+ usage.cacheCreationInputTokens ?? 0
692
679
  );
693
680
  if (cost) {
694
681
  this.totalCost += cost.totalCost;
@@ -728,6 +715,16 @@ var StreamProgress = class {
728
715
  this.callOutputTokens = tokens;
729
716
  this.callOutputTokensEstimated = estimated;
730
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
+ }
731
728
  /**
732
729
  * Get total elapsed time in seconds since the first call started.
733
730
  * @returns Elapsed time in seconds with 1 decimal place
@@ -792,11 +789,32 @@ var StreamProgress = class {
792
789
  parts.push(chalk2.dim("\u2193") + chalk2.green(` ${prefix}${formatTokens(outTokens)}`));
793
790
  }
794
791
  parts.push(chalk2.dim(`${elapsed}s`));
795
- if (this.totalCost > 0) {
796
- parts.push(chalk2.cyan(`$${formatCost(this.totalCost)}`));
792
+ const callCost = this.calculateCurrentCallCost(outTokens);
793
+ if (callCost > 0) {
794
+ parts.push(chalk2.cyan(`$${formatCost(callCost)}`));
797
795
  }
798
796
  this.target.write(`\r${parts.join(chalk2.dim(" | "))} ${chalk2.cyan(spinner)}`);
799
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
+ }
800
818
  renderCumulativeMode(spinner) {
801
819
  const elapsed = ((Date.now() - this.totalStartTime) / 1e3).toFixed(1);
802
820
  const parts = [];
@@ -945,7 +963,7 @@ function addCompleteOptions(cmd, defaults) {
945
963
  OPTION_DESCRIPTIONS.maxTokens,
946
964
  createNumericParser({ label: "Max tokens", integer: true, min: 1 }),
947
965
  defaults?.["max-tokens"]
948
- );
966
+ ).option(OPTION_FLAGS.quiet, OPTION_DESCRIPTIONS.quiet, defaults?.quiet);
949
967
  }
950
968
  function addAgentOptions(cmd, defaults) {
951
969
  const gadgetAccumulator = (value, previous = []) => [
@@ -974,7 +992,7 @@ function addAgentOptions(cmd, defaults) {
974
992
  OPTION_FLAGS.noBuiltinInteraction,
975
993
  OPTION_DESCRIPTIONS.noBuiltinInteraction,
976
994
  defaults?.["builtin-interaction"] !== false
977
- );
995
+ ).option(OPTION_FLAGS.quiet, OPTION_DESCRIPTIONS.quiet, defaults?.quiet);
978
996
  }
979
997
  function configToCompleteOptions(config) {
980
998
  const result = {};
@@ -982,6 +1000,7 @@ function configToCompleteOptions(config) {
982
1000
  if (config.system !== void 0) result.system = config.system;
983
1001
  if (config.temperature !== void 0) result.temperature = config.temperature;
984
1002
  if (config["max-tokens"] !== void 0) result.maxTokens = config["max-tokens"];
1003
+ if (config.quiet !== void 0) result.quiet = config.quiet;
985
1004
  return result;
986
1005
  }
987
1006
  function configToAgentOptions(config) {
@@ -995,6 +1014,11 @@ function configToAgentOptions(config) {
995
1014
  if (config.builtins !== void 0) result.builtins = config.builtins;
996
1015
  if (config["builtin-interaction"] !== void 0)
997
1016
  result.builtinInteraction = config["builtin-interaction"];
1017
+ if (config["gadget-start-prefix"] !== void 0)
1018
+ result.gadgetStartPrefix = config["gadget-start-prefix"];
1019
+ if (config["gadget-end-prefix"] !== void 0)
1020
+ result.gadgetEndPrefix = config["gadget-end-prefix"];
1021
+ if (config.quiet !== void 0) result.quiet = config.quiet;
998
1022
  return result;
999
1023
  }
1000
1024
 
@@ -1040,9 +1064,10 @@ async function executeAgent(promptArg, options, env) {
1040
1064
  const prompt = await resolvePrompt(promptArg, env);
1041
1065
  const client = env.createClient();
1042
1066
  const registry = new GadgetRegistry();
1067
+ const stdinIsInteractive = isInteractive(env.stdin);
1043
1068
  if (options.builtins !== false) {
1044
1069
  for (const gadget of builtinGadgets) {
1045
- if (options.builtinInteraction === false && gadget.name === "AskUser") {
1070
+ if (gadget.name === "AskUser" && (options.builtinInteraction === false || !stdinIsInteractive)) {
1046
1071
  continue;
1047
1072
  }
1048
1073
  registry.registerByClass(gadget);
@@ -1100,6 +1125,10 @@ async function executeAgent(promptArg, options, env) {
1100
1125
  if (context.usage.outputTokens) {
1101
1126
  progress.setOutputTokens(context.usage.outputTokens, false);
1102
1127
  }
1128
+ progress.setCachedTokens(
1129
+ context.usage.cachedInputTokens ?? 0,
1130
+ context.usage.cacheCreationInputTokens ?? 0
1131
+ );
1103
1132
  }
1104
1133
  },
1105
1134
  // onLLMCallComplete: Finalize metrics after each LLM call
@@ -1118,11 +1147,13 @@ async function executeAgent(promptArg, options, env) {
1118
1147
  let callCost;
1119
1148
  if (context.usage && client.modelRegistry) {
1120
1149
  try {
1121
- 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;
1122
1151
  const costResult = client.modelRegistry.estimateCost(
1123
1152
  modelName,
1124
1153
  context.usage.inputTokens,
1125
- context.usage.outputTokens
1154
+ context.usage.outputTokens,
1155
+ context.usage.cachedInputTokens ?? 0,
1156
+ context.usage.cacheCreationInputTokens ?? 0
1126
1157
  );
1127
1158
  if (costResult) callCost = costResult.totalCost;
1128
1159
  } catch {
@@ -1130,7 +1161,7 @@ async function executeAgent(promptArg, options, env) {
1130
1161
  }
1131
1162
  const callElapsed = progress.getCallElapsedSeconds();
1132
1163
  progress.endCall(context.usage);
1133
- if (stderrTTY) {
1164
+ if (!options.quiet) {
1134
1165
  const summary = renderSummary({
1135
1166
  iterations: context.iteration + 1,
1136
1167
  model: options.model,
@@ -1209,9 +1240,27 @@ Command rejected by user with message: "${response}"`
1209
1240
  builder.withGadgets(...gadgets);
1210
1241
  }
1211
1242
  builder.withParameterFormat(options.parameterFormat);
1212
- const markers = generateMarkers();
1213
- builder.withGadgetStartPrefix(markers.startPrefix);
1214
- builder.withGadgetEndPrefix(markers.endPrefix);
1243
+ if (options.gadgetStartPrefix) {
1244
+ builder.withGadgetStartPrefix(options.gadgetStartPrefix);
1245
+ }
1246
+ if (options.gadgetEndPrefix) {
1247
+ builder.withGadgetEndPrefix(options.gadgetEndPrefix);
1248
+ }
1249
+ builder.withSyntheticGadgetCall(
1250
+ "TellUser",
1251
+ {
1252
+ message: "\u{1F44B} Hello! I'm ready to help.\n\nHere's what I can do:\n- Analyze your codebase\n- Execute commands\n- Answer questions\n\nWhat would you like me to work on?",
1253
+ done: false,
1254
+ type: "info"
1255
+ },
1256
+ "\u2139\uFE0F \u{1F44B} Hello! I'm ready to help.\n\nHere's what I can do:\n- Analyze your codebase\n- Execute commands\n- Answer questions\n\nWhat would you like me to work on?"
1257
+ );
1258
+ builder.withTextOnlyHandler("acknowledge");
1259
+ builder.withTextWithGadgetsHandler({
1260
+ gadgetName: "TellUser",
1261
+ parameterMapping: (text) => ({ message: text, done: false, type: "info" }),
1262
+ resultMapping: (text) => `\u2139\uFE0F ${text}`
1263
+ });
1215
1264
  const agent = builder.ask(prompt);
1216
1265
  for await (const event of agent.run()) {
1217
1266
  if (event.type === "text") {
@@ -1219,7 +1268,14 @@ Command rejected by user with message: "${response}"`
1219
1268
  printer.write(event.content);
1220
1269
  } else if (event.type === "gadget_result") {
1221
1270
  progress.pause();
1222
- 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 {
1223
1279
  const tokenCount = await countGadgetOutputTokens(event.result.result);
1224
1280
  env.stderr.write(`${formatGadgetSummary({ ...event.result, tokenCount })}
1225
1281
  `);
@@ -1228,7 +1284,7 @@ Command rejected by user with message: "${response}"`
1228
1284
  }
1229
1285
  progress.complete();
1230
1286
  printer.ensureNewline();
1231
- if (stderrTTY && iterations > 1) {
1287
+ if (!options.quiet && iterations > 1) {
1232
1288
  env.stderr.write(`${chalk3.dim("\u2500".repeat(40))}
1233
1289
  `);
1234
1290
  const summary = renderOverallSummary({
@@ -1301,7 +1357,7 @@ async function executeComplete(promptArg, options, env) {
1301
1357
  progress.endCall(usage);
1302
1358
  progress.complete();
1303
1359
  printer.ensureNewline();
1304
- if (stderrTTY) {
1360
+ if (stderrTTY && !options.quiet) {
1305
1361
  const summary = renderSummary({ finishReason, usage, cost: progress.getTotalCost() });
1306
1362
  if (summary) {
1307
1363
  env.stderr.write(`${summary}
@@ -1322,9 +1378,102 @@ import { existsSync, readFileSync } from "node:fs";
1322
1378
  import { homedir } from "node:os";
1323
1379
  import { join } from "node:path";
1324
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
1325
1462
  var GLOBAL_CONFIG_KEYS = /* @__PURE__ */ new Set(["log-level", "log-file", "log-reset"]);
1326
1463
  var VALID_LOG_LEVELS = ["silly", "trace", "debug", "info", "warn", "error", "fatal"];
1327
- 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
+ ]);
1328
1477
  var AGENT_CONFIG_KEYS = /* @__PURE__ */ new Set([
1329
1478
  "model",
1330
1479
  "system",
@@ -1333,16 +1482,22 @@ var AGENT_CONFIG_KEYS = /* @__PURE__ */ new Set([
1333
1482
  "gadget",
1334
1483
  "parameter-format",
1335
1484
  "builtins",
1336
- "builtin-interaction"
1485
+ "builtin-interaction",
1486
+ "gadget-start-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
1337
1495
  ]);
1338
1496
  var CUSTOM_CONFIG_KEYS = /* @__PURE__ */ new Set([
1339
1497
  ...COMPLETE_CONFIG_KEYS,
1340
1498
  ...AGENT_CONFIG_KEYS,
1341
1499
  "type",
1342
- "description",
1343
- "log-level",
1344
- "log-file",
1345
- "log-reset"
1500
+ "description"
1346
1501
  ]);
1347
1502
  var VALID_PARAMETER_FORMATS = ["json", "yaml", "toml", "auto"];
1348
1503
  function getConfigPath() {
@@ -1393,6 +1548,39 @@ function validateStringArray(value, key, section) {
1393
1548
  }
1394
1549
  return value;
1395
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
+ }
1396
1584
  function validateBaseConfig(raw, section) {
1397
1585
  const result = {};
1398
1586
  if ("model" in raw) {
@@ -1407,6 +1595,9 @@ function validateBaseConfig(raw, section) {
1407
1595
  max: 2
1408
1596
  });
1409
1597
  }
1598
+ if ("inherits" in raw) {
1599
+ result.inherits = validateInherits(raw.inherits, section);
1600
+ }
1410
1601
  return result;
1411
1602
  }
1412
1603
  function validateGlobalConfig(raw, section) {
@@ -1419,23 +1610,7 @@ function validateGlobalConfig(raw, section) {
1419
1610
  throw new ConfigError(`[${section}].${key} is not a valid option`);
1420
1611
  }
1421
1612
  }
1422
- const result = {};
1423
- if ("log-level" in rawObj) {
1424
- const level = validateString(rawObj["log-level"], "log-level", section);
1425
- if (!VALID_LOG_LEVELS.includes(level)) {
1426
- throw new ConfigError(
1427
- `[${section}].log-level must be one of: ${VALID_LOG_LEVELS.join(", ")}`
1428
- );
1429
- }
1430
- result["log-level"] = level;
1431
- }
1432
- if ("log-file" in rawObj) {
1433
- result["log-file"] = validateString(rawObj["log-file"], "log-file", section);
1434
- }
1435
- if ("log-reset" in rawObj) {
1436
- result["log-reset"] = validateBoolean(rawObj["log-reset"], "log-reset", section);
1437
- }
1438
- return result;
1613
+ return validateLoggingConfig(rawObj, section);
1439
1614
  }
1440
1615
  function validateCompleteConfig(raw, section) {
1441
1616
  if (typeof raw !== "object" || raw === null) {
@@ -1447,13 +1622,19 @@ function validateCompleteConfig(raw, section) {
1447
1622
  throw new ConfigError(`[${section}].${key} is not a valid option`);
1448
1623
  }
1449
1624
  }
1450
- const result = { ...validateBaseConfig(rawObj, section) };
1625
+ const result = {
1626
+ ...validateBaseConfig(rawObj, section),
1627
+ ...validateLoggingConfig(rawObj, section)
1628
+ };
1451
1629
  if ("max-tokens" in rawObj) {
1452
1630
  result["max-tokens"] = validateNumber(rawObj["max-tokens"], "max-tokens", section, {
1453
1631
  integer: true,
1454
1632
  min: 1
1455
1633
  });
1456
1634
  }
1635
+ if ("quiet" in rawObj) {
1636
+ result.quiet = validateBoolean(rawObj.quiet, "quiet", section);
1637
+ }
1457
1638
  return result;
1458
1639
  }
1459
1640
  function validateAgentConfig(raw, section) {
@@ -1466,7 +1647,10 @@ function validateAgentConfig(raw, section) {
1466
1647
  throw new ConfigError(`[${section}].${key} is not a valid option`);
1467
1648
  }
1468
1649
  }
1469
- const result = { ...validateBaseConfig(rawObj, section) };
1650
+ const result = {
1651
+ ...validateBaseConfig(rawObj, section),
1652
+ ...validateLoggingConfig(rawObj, section)
1653
+ };
1470
1654
  if ("max-iterations" in rawObj) {
1471
1655
  result["max-iterations"] = validateNumber(rawObj["max-iterations"], "max-iterations", section, {
1472
1656
  integer: true,
@@ -1495,6 +1679,23 @@ function validateAgentConfig(raw, section) {
1495
1679
  section
1496
1680
  );
1497
1681
  }
1682
+ if ("gadget-start-prefix" in rawObj) {
1683
+ result["gadget-start-prefix"] = validateString(
1684
+ rawObj["gadget-start-prefix"],
1685
+ "gadget-start-prefix",
1686
+ section
1687
+ );
1688
+ }
1689
+ if ("gadget-end-prefix" in rawObj) {
1690
+ result["gadget-end-prefix"] = validateString(
1691
+ rawObj["gadget-end-prefix"],
1692
+ "gadget-end-prefix",
1693
+ section
1694
+ );
1695
+ }
1696
+ if ("quiet" in rawObj) {
1697
+ result.quiet = validateBoolean(rawObj.quiet, "quiet", section);
1698
+ }
1498
1699
  return result;
1499
1700
  }
1500
1701
  function validateCustomConfig(raw, section) {
@@ -1550,26 +1751,42 @@ function validateCustomConfig(raw, section) {
1550
1751
  section
1551
1752
  );
1552
1753
  }
1754
+ if ("gadget-start-prefix" in rawObj) {
1755
+ result["gadget-start-prefix"] = validateString(
1756
+ rawObj["gadget-start-prefix"],
1757
+ "gadget-start-prefix",
1758
+ section
1759
+ );
1760
+ }
1761
+ if ("gadget-end-prefix" in rawObj) {
1762
+ result["gadget-end-prefix"] = validateString(
1763
+ rawObj["gadget-end-prefix"],
1764
+ "gadget-end-prefix",
1765
+ section
1766
+ );
1767
+ }
1553
1768
  if ("max-tokens" in rawObj) {
1554
1769
  result["max-tokens"] = validateNumber(rawObj["max-tokens"], "max-tokens", section, {
1555
1770
  integer: true,
1556
1771
  min: 1
1557
1772
  });
1558
1773
  }
1559
- if ("log-level" in rawObj) {
1560
- const level = validateString(rawObj["log-level"], "log-level", section);
1561
- if (!VALID_LOG_LEVELS.includes(level)) {
1562
- throw new ConfigError(
1563
- `[${section}].log-level must be one of: ${VALID_LOG_LEVELS.join(", ")}`
1564
- );
1565
- }
1566
- result["log-level"] = level;
1774
+ if ("quiet" in rawObj) {
1775
+ result.quiet = validateBoolean(rawObj.quiet, "quiet", section);
1567
1776
  }
1568
- if ("log-file" in rawObj) {
1569
- 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`);
1570
1783
  }
1571
- if ("log-reset" in rawObj) {
1572
- 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;
1573
1790
  }
1574
1791
  return result;
1575
1792
  }
@@ -1587,6 +1804,8 @@ function validateConfig(raw, configPath) {
1587
1804
  result.complete = validateCompleteConfig(value, key);
1588
1805
  } else if (key === "agent") {
1589
1806
  result.agent = validateAgentConfig(value, key);
1807
+ } else if (key === "prompts") {
1808
+ result.prompts = validatePromptsConfig(value, key);
1590
1809
  } else {
1591
1810
  result[key] = validateCustomConfig(value, key);
1592
1811
  }
@@ -1622,12 +1841,119 @@ function loadConfig() {
1622
1841
  configPath
1623
1842
  );
1624
1843
  }
1625
- return validateConfig(raw, configPath);
1844
+ const validated = validateConfig(raw, configPath);
1845
+ const inherited = resolveInheritance(validated, configPath);
1846
+ return resolveTemplatesInConfig(inherited, configPath);
1626
1847
  }
1627
1848
  function getCustomCommandNames(config) {
1628
- const reserved = /* @__PURE__ */ new Set(["global", "complete", "agent"]);
1849
+ const reserved = /* @__PURE__ */ new Set(["global", "complete", "agent", "prompts"]);
1629
1850
  return Object.keys(config).filter((key) => !reserved.has(key));
1630
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
+ }
1631
1957
 
1632
1958
  // src/cli/models-command.ts
1633
1959
  import chalk4 from "chalk";