llmist 1.4.0 → 1.6.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.cjs CHANGED
@@ -830,38 +830,83 @@ function formatParamsAsBlock(params, prefix = "", argPrefix = GADGET_ARG_PREFIX)
830
830
  }
831
831
  return lines.join("\n");
832
832
  }
833
- function formatSchemaAsPlainText(schema, indent = "") {
833
+ function formatParamLine(key, propObj, isRequired, indent = "") {
834
+ const type = propObj.type;
835
+ const description = propObj.description;
836
+ const enumValues = propObj.enum;
837
+ let line = `${indent}- ${key}`;
838
+ if (type === "array") {
839
+ const items = propObj.items;
840
+ const itemType = items?.type || "any";
841
+ line += ` (array of ${itemType})`;
842
+ } else if (type === "object" && propObj.properties) {
843
+ line += " (object)";
844
+ } else {
845
+ line += ` (${type})`;
846
+ }
847
+ if (isRequired && indent !== "") {
848
+ line += " [required]";
849
+ }
850
+ if (description) {
851
+ line += `: ${description}`;
852
+ }
853
+ if (enumValues) {
854
+ line += ` - one of: ${enumValues.map((v) => `"${v}"`).join(", ")}`;
855
+ }
856
+ return line;
857
+ }
858
+ function formatSchemaAsPlainText(schema, indent = "", atRoot = true) {
834
859
  const lines = [];
835
860
  const properties = schema.properties || {};
836
861
  const required = schema.required || [];
837
- for (const [key, prop] of Object.entries(properties)) {
838
- const propObj = prop;
839
- const type = propObj.type;
840
- const description = propObj.description;
841
- const isRequired = required.includes(key);
842
- const enumValues = propObj.enum;
843
- let line = `${indent}- ${key}`;
844
- if (type === "array") {
845
- const items = propObj.items;
846
- const itemType = items?.type || "any";
847
- line += ` (array of ${itemType})`;
848
- } else if (type === "object" && propObj.properties) {
849
- line += " (object)";
850
- } else {
851
- line += ` (${type})`;
862
+ if (atRoot && indent === "") {
863
+ const requiredProps = [];
864
+ const optionalProps = [];
865
+ for (const [key, prop] of Object.entries(properties)) {
866
+ if (required.includes(key)) {
867
+ requiredProps.push([key, prop]);
868
+ } else {
869
+ optionalProps.push([key, prop]);
870
+ }
852
871
  }
853
- if (isRequired) {
854
- line += " [required]";
872
+ const reqCount = requiredProps.length;
873
+ const optCount = optionalProps.length;
874
+ if (reqCount > 0 || optCount > 0) {
875
+ const parts = [];
876
+ if (reqCount > 0) parts.push(`${reqCount} required`);
877
+ if (optCount > 0) parts.push(`${optCount} optional`);
878
+ lines.push(parts.join(", "));
879
+ lines.push("");
855
880
  }
856
- if (description) {
857
- line += `: ${description}`;
881
+ if (reqCount > 0) {
882
+ lines.push("REQUIRED Parameters:");
883
+ for (const [key, prop] of requiredProps) {
884
+ lines.push(formatParamLine(key, prop, true, ""));
885
+ const propObj = prop;
886
+ if (propObj.type === "object" && propObj.properties) {
887
+ lines.push(formatSchemaAsPlainText(propObj, " ", false));
888
+ }
889
+ }
858
890
  }
859
- if (enumValues) {
860
- line += ` - one of: ${enumValues.map((v) => `"${v}"`).join(", ")}`;
891
+ if (optCount > 0) {
892
+ if (reqCount > 0) lines.push("");
893
+ lines.push("OPTIONAL Parameters:");
894
+ for (const [key, prop] of optionalProps) {
895
+ lines.push(formatParamLine(key, prop, false, ""));
896
+ const propObj = prop;
897
+ if (propObj.type === "object" && propObj.properties) {
898
+ lines.push(formatSchemaAsPlainText(propObj, " ", false));
899
+ }
900
+ }
861
901
  }
862
- lines.push(line);
863
- if (type === "object" && propObj.properties) {
864
- lines.push(formatSchemaAsPlainText(propObj, indent + " "));
902
+ return lines.join("\n");
903
+ }
904
+ for (const [key, prop] of Object.entries(properties)) {
905
+ const isRequired = required.includes(key);
906
+ lines.push(formatParamLine(key, prop, isRequired, indent));
907
+ const propObj = prop;
908
+ if (propObj.type === "object" && propObj.properties) {
909
+ lines.push(formatSchemaAsPlainText(propObj, indent + " ", false));
865
910
  }
866
911
  }
867
912
  return lines.join("\n");
@@ -912,10 +957,11 @@ var init_gadget = __esm({
912
957
  * Generate instruction text for the LLM.
913
958
  * Combines name, description, and parameter schema into a formatted instruction.
914
959
  *
915
- * @param argPrefix - Optional custom argument prefix for block format examples
960
+ * @param optionsOrArgPrefix - Optional custom prefixes for examples, or just argPrefix string for backwards compatibility
916
961
  * @returns Formatted instruction string
917
962
  */
918
- getInstruction(argPrefix) {
963
+ getInstruction(optionsOrArgPrefix) {
964
+ const options = typeof optionsOrArgPrefix === "string" ? { argPrefix: optionsOrArgPrefix } : optionsOrArgPrefix;
919
965
  const parts = [];
920
966
  parts.push(this.description);
921
967
  if (this.parameterSchema) {
@@ -929,18 +975,25 @@ var init_gadget = __esm({
929
975
  }
930
976
  if (this.examples && this.examples.length > 0) {
931
977
  parts.push("\n\nExamples:");
932
- const effectiveArgPrefix = argPrefix ?? GADGET_ARG_PREFIX;
978
+ const effectiveArgPrefix = options?.argPrefix ?? GADGET_ARG_PREFIX;
979
+ const effectiveStartPrefix = options?.startPrefix ?? GADGET_START_PREFIX;
980
+ const effectiveEndPrefix = options?.endPrefix ?? GADGET_END_PREFIX;
981
+ const gadgetName = this.name || this.constructor.name;
933
982
  this.examples.forEach((example, index) => {
934
983
  if (index > 0) {
935
984
  parts.push("");
985
+ parts.push("---");
986
+ parts.push("");
936
987
  }
937
988
  if (example.comment) {
938
989
  parts.push(`# ${example.comment}`);
939
990
  }
940
- parts.push("Input:");
991
+ parts.push(`${effectiveStartPrefix}${gadgetName}`);
941
992
  parts.push(formatParamsAsBlock(example.params, "", effectiveArgPrefix));
993
+ parts.push(effectiveEndPrefix);
942
994
  if (example.output !== void 0) {
943
- parts.push("Output:");
995
+ parts.push("");
996
+ parts.push("Expected Output:");
944
997
  parts.push(example.output);
945
998
  }
946
999
  });
@@ -6500,7 +6553,11 @@ var OPTION_FLAGS = {
6500
6553
  logLlmResponses: "--log-llm-responses [dir]",
6501
6554
  noBuiltins: "--no-builtins",
6502
6555
  noBuiltinInteraction: "--no-builtin-interaction",
6503
- quiet: "-q, --quiet"
6556
+ quiet: "-q, --quiet",
6557
+ docker: "--docker",
6558
+ dockerRo: "--docker-ro",
6559
+ noDocker: "--no-docker",
6560
+ dockerDev: "--docker-dev"
6504
6561
  };
6505
6562
  var OPTION_DESCRIPTIONS = {
6506
6563
  model: "Model identifier, e.g. openai:gpt-5-nano or anthropic:claude-sonnet-4-5.",
@@ -6516,7 +6573,11 @@ var OPTION_DESCRIPTIONS = {
6516
6573
  logLlmResponses: "Save raw LLM responses as plain text. Optional dir, defaults to ~/.llmist/logs/responses/",
6517
6574
  noBuiltins: "Disable built-in gadgets (AskUser, TellUser).",
6518
6575
  noBuiltinInteraction: "Disable interactive gadgets (AskUser) while keeping TellUser.",
6519
- quiet: "Suppress all output except content (text and TellUser messages)."
6576
+ quiet: "Suppress all output except content (text and TellUser messages).",
6577
+ docker: "Run agent in a Docker sandbox container for security isolation.",
6578
+ dockerRo: "Run in Docker with current directory mounted read-only.",
6579
+ noDocker: "Disable Docker sandboxing (override config).",
6580
+ dockerDev: "Run in Docker dev mode (mount local source instead of npm install)."
6520
6581
  };
6521
6582
  var SUMMARY_PREFIX = "[llmist]";
6522
6583
 
@@ -6526,7 +6587,7 @@ var import_commander2 = require("commander");
6526
6587
  // package.json
6527
6588
  var package_default = {
6528
6589
  name: "llmist",
6529
- version: "1.3.1",
6590
+ version: "1.5.0",
6530
6591
  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.",
6531
6592
  type: "module",
6532
6593
  main: "dist/index.cjs",
@@ -8336,7 +8397,7 @@ function addAgentOptions(cmd, defaults) {
8336
8397
  OPTION_FLAGS.noBuiltinInteraction,
8337
8398
  OPTION_DESCRIPTIONS.noBuiltinInteraction,
8338
8399
  defaults?.["builtin-interaction"] !== false
8339
- ).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"]);
8400
+ ).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"]).option(OPTION_FLAGS.docker, OPTION_DESCRIPTIONS.docker).option(OPTION_FLAGS.dockerRo, OPTION_DESCRIPTIONS.dockerRo).option(OPTION_FLAGS.noDocker, OPTION_DESCRIPTIONS.noDocker).option(OPTION_FLAGS.dockerDev, OPTION_DESCRIPTIONS.dockerDev);
8340
8401
  }
8341
8402
  function configToCompleteOptions(config) {
8342
8403
  const result = {};
@@ -8371,668 +8432,791 @@ function configToAgentOptions(config) {
8371
8432
  if (config.quiet !== void 0) result.quiet = config.quiet;
8372
8433
  if (config["log-llm-requests"] !== void 0) result.logLlmRequests = config["log-llm-requests"];
8373
8434
  if (config["log-llm-responses"] !== void 0) result.logLlmResponses = config["log-llm-responses"];
8435
+ if (config.docker !== void 0) result.docker = config.docker;
8436
+ if (config["docker-cwd-permission"] !== void 0)
8437
+ result.dockerCwdPermission = config["docker-cwd-permission"];
8374
8438
  return result;
8375
8439
  }
8376
8440
 
8377
- // src/cli/agent-command.ts
8378
- function createHumanInputHandler(env, progress, keyboard) {
8379
- const stdout = env.stdout;
8380
- if (!isInteractive(env.stdin) || typeof stdout.isTTY !== "boolean" || !stdout.isTTY) {
8381
- return void 0;
8441
+ // src/cli/docker/types.ts
8442
+ var VALID_MOUNT_PERMISSIONS = ["ro", "rw"];
8443
+ var DOCKER_CONFIG_KEYS = /* @__PURE__ */ new Set([
8444
+ "enabled",
8445
+ "dockerfile",
8446
+ "cwd-permission",
8447
+ "config-permission",
8448
+ "mounts",
8449
+ "env-vars",
8450
+ "image-name",
8451
+ "dev-mode",
8452
+ "dev-source"
8453
+ ]);
8454
+ var DEFAULT_IMAGE_NAME = "llmist-sandbox";
8455
+ var DEFAULT_CWD_PERMISSION = "rw";
8456
+ var DEFAULT_CONFIG_PERMISSION = "ro";
8457
+ var FORWARDED_API_KEYS = [
8458
+ "ANTHROPIC_API_KEY",
8459
+ "OPENAI_API_KEY",
8460
+ "GEMINI_API_KEY"
8461
+ ];
8462
+ var DEV_IMAGE_NAME = "llmist-dev-sandbox";
8463
+ var DEV_SOURCE_MOUNT_TARGET = "/llmist-src";
8464
+
8465
+ // src/cli/config.ts
8466
+ var import_node_fs8 = require("fs");
8467
+ var import_node_os2 = require("os");
8468
+ var import_node_path8 = require("path");
8469
+ var import_js_toml = require("js-toml");
8470
+
8471
+ // src/cli/templates.ts
8472
+ var import_eta = require("eta");
8473
+ var TemplateError = class extends Error {
8474
+ constructor(message, promptName, configPath) {
8475
+ super(promptName ? `[prompts.${promptName}]: ${message}` : message);
8476
+ this.promptName = promptName;
8477
+ this.configPath = configPath;
8478
+ this.name = "TemplateError";
8382
8479
  }
8383
- return async (question) => {
8384
- progress.pause();
8385
- if (keyboard.cleanupEsc) {
8386
- keyboard.cleanupEsc();
8387
- keyboard.cleanupEsc = null;
8388
- }
8389
- const rl = (0, import_promises3.createInterface)({ input: env.stdin, output: env.stdout });
8480
+ };
8481
+ function createTemplateEngine(prompts, configPath) {
8482
+ const eta = new import_eta.Eta({
8483
+ views: "/",
8484
+ // Required but we use named templates
8485
+ autoEscape: false,
8486
+ // Don't escape - these are prompts, not HTML
8487
+ autoTrim: false
8488
+ // Preserve whitespace in prompts
8489
+ });
8490
+ for (const [name, template] of Object.entries(prompts)) {
8390
8491
  try {
8391
- const questionLine = question.trim() ? `
8392
- ${renderMarkdownWithSeparators(question.trim())}` : "";
8393
- let isFirst = true;
8394
- while (true) {
8395
- const statsPrompt = progress.formatPrompt();
8396
- const prompt = isFirst ? `${questionLine}
8397
- ${statsPrompt}` : statsPrompt;
8398
- isFirst = false;
8399
- const answer = await rl.question(prompt);
8400
- const trimmed = answer.trim();
8401
- if (trimmed) {
8402
- return trimmed;
8403
- }
8404
- }
8405
- } finally {
8406
- rl.close();
8407
- keyboard.restore();
8408
- }
8409
- };
8410
- }
8411
- async function executeAgent(promptArg, options, env) {
8412
- const prompt = await resolvePrompt(promptArg, env);
8413
- const client = env.createClient();
8414
- const registry = new GadgetRegistry();
8415
- const stdinIsInteractive = isInteractive(env.stdin);
8416
- if (options.builtins !== false) {
8417
- for (const gadget of builtinGadgets) {
8418
- if (gadget.name === "AskUser" && (options.builtinInteraction === false || !stdinIsInteractive)) {
8419
- continue;
8420
- }
8421
- registry.registerByClass(gadget);
8422
- }
8423
- }
8424
- const gadgetSpecifiers = options.gadget ?? [];
8425
- if (gadgetSpecifiers.length > 0) {
8426
- const gadgets2 = await loadGadgets(gadgetSpecifiers, process.cwd());
8427
- for (const gadget of gadgets2) {
8428
- registry.registerByClass(gadget);
8429
- }
8430
- }
8431
- const printer = new StreamPrinter(env.stdout);
8432
- const stderrTTY = env.stderr.isTTY === true;
8433
- const progress = new StreamProgress(env.stderr, stderrTTY, client.modelRegistry);
8434
- const abortController = new AbortController();
8435
- let wasCancelled = false;
8436
- let isStreaming = false;
8437
- const stdinStream = env.stdin;
8438
- const handleCancel = () => {
8439
- if (!abortController.signal.aborted) {
8440
- wasCancelled = true;
8441
- abortController.abort();
8442
- progress.pause();
8443
- env.stderr.write(import_chalk5.default.yellow(`
8444
- [Cancelled] ${progress.formatStats()}
8445
- `));
8446
- }
8447
- };
8448
- const keyboard = {
8449
- cleanupEsc: null,
8450
- cleanupSigint: null,
8451
- restore: () => {
8452
- if (stdinIsInteractive && stdinStream.isTTY && !wasCancelled) {
8453
- keyboard.cleanupEsc = createEscKeyListener(stdinStream, handleCancel);
8454
- }
8455
- }
8456
- };
8457
- const handleQuit = () => {
8458
- keyboard.cleanupEsc?.();
8459
- keyboard.cleanupSigint?.();
8460
- progress.complete();
8461
- printer.ensureNewline();
8462
- const summary = renderOverallSummary({
8463
- totalTokens: usage?.totalTokens,
8464
- iterations,
8465
- elapsedSeconds: progress.getTotalElapsedSeconds(),
8466
- cost: progress.getTotalCost()
8467
- });
8468
- if (summary) {
8469
- env.stderr.write(`${import_chalk5.default.dim("\u2500".repeat(40))}
8470
- `);
8471
- env.stderr.write(`${summary}
8472
- `);
8492
+ eta.loadTemplate(`@${name}`, template);
8493
+ } catch (error) {
8494
+ throw new TemplateError(
8495
+ error instanceof Error ? error.message : String(error),
8496
+ name,
8497
+ configPath
8498
+ );
8473
8499
  }
8474
- env.stderr.write(import_chalk5.default.dim("[Quit]\n"));
8475
- process.exit(130);
8476
- };
8477
- if (stdinIsInteractive && stdinStream.isTTY) {
8478
- keyboard.cleanupEsc = createEscKeyListener(stdinStream, handleCancel);
8479
8500
  }
8480
- keyboard.cleanupSigint = createSigintListener(
8481
- handleCancel,
8482
- handleQuit,
8483
- () => isStreaming && !abortController.signal.aborted,
8484
- env.stderr
8485
- );
8486
- const DEFAULT_APPROVAL_REQUIRED = ["RunCommand", "WriteFile", "EditFile"];
8487
- const userApprovals = options.gadgetApproval ?? {};
8488
- const gadgetApprovals = {
8489
- ...userApprovals
8490
- };
8491
- for (const gadget of DEFAULT_APPROVAL_REQUIRED) {
8492
- const normalizedGadget = gadget.toLowerCase();
8493
- const isConfigured = Object.keys(userApprovals).some(
8494
- (key) => key.toLowerCase() === normalizedGadget
8501
+ return eta;
8502
+ }
8503
+ function resolveTemplate(eta, template, context = {}, configPath) {
8504
+ try {
8505
+ const fullContext = {
8506
+ ...context,
8507
+ env: process.env,
8508
+ date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
8509
+ // "2025-12-01"
8510
+ };
8511
+ return eta.renderString(template, fullContext);
8512
+ } catch (error) {
8513
+ throw new TemplateError(
8514
+ error instanceof Error ? error.message : String(error),
8515
+ void 0,
8516
+ configPath
8495
8517
  );
8496
- if (!isConfigured) {
8497
- gadgetApprovals[gadget] = "approval-required";
8498
- }
8499
8518
  }
8500
- const approvalConfig = {
8501
- gadgetApprovals,
8502
- defaultMode: "allowed"
8503
- };
8504
- const approvalManager = new ApprovalManager(approvalConfig, env, progress);
8505
- let usage;
8506
- let iterations = 0;
8507
- const llmRequestsDir = resolveLogDir(options.logLlmRequests, "requests");
8508
- const llmResponsesDir = resolveLogDir(options.logLlmResponses, "responses");
8509
- let llmCallCounter = 0;
8510
- const countMessagesTokens = async (model, messages) => {
8519
+ }
8520
+ function validatePrompts(prompts, configPath) {
8521
+ const eta = createTemplateEngine(prompts, configPath);
8522
+ for (const [name, template] of Object.entries(prompts)) {
8511
8523
  try {
8512
- return await client.countTokens(model, messages);
8513
- } catch {
8514
- const totalChars = messages.reduce((sum, m) => sum + (m.content?.length ?? 0), 0);
8515
- return Math.round(totalChars / FALLBACK_CHARS_PER_TOKEN);
8524
+ eta.renderString(template, { env: {} });
8525
+ } catch (error) {
8526
+ throw new TemplateError(
8527
+ error instanceof Error ? error.message : String(error),
8528
+ name,
8529
+ configPath
8530
+ );
8516
8531
  }
8517
- };
8518
- const countGadgetOutputTokens = async (output) => {
8519
- if (!output) return void 0;
8520
- try {
8521
- const messages = [{ role: "assistant", content: output }];
8522
- return await client.countTokens(options.model, messages);
8523
- } catch {
8524
- return void 0;
8532
+ }
8533
+ }
8534
+ function validateEnvVars(template, promptName, configPath) {
8535
+ const envVarPattern = /<%=\s*it\.env\.(\w+)\s*%>/g;
8536
+ const matches = template.matchAll(envVarPattern);
8537
+ for (const match of matches) {
8538
+ const varName = match[1];
8539
+ if (process.env[varName] === void 0) {
8540
+ throw new TemplateError(
8541
+ `Environment variable '${varName}' is not set`,
8542
+ promptName,
8543
+ configPath
8544
+ );
8525
8545
  }
8526
- };
8527
- const builder = new AgentBuilder(client).withModel(options.model).withLogger(env.createLogger("llmist:cli:agent")).withHooks({
8528
- observers: {
8529
- // onLLMCallStart: Start progress indicator for each LLM call
8530
- // This showcases how to react to agent lifecycle events
8531
- onLLMCallStart: async (context) => {
8532
- isStreaming = true;
8533
- llmCallCounter++;
8534
- const inputTokens = await countMessagesTokens(
8535
- context.options.model,
8536
- context.options.messages
8537
- );
8538
- progress.startCall(context.options.model, inputTokens);
8539
- progress.setInputTokens(inputTokens, false);
8540
- if (llmRequestsDir) {
8541
- const filename = `${Date.now()}_call_${llmCallCounter}.request.txt`;
8542
- const content = formatLlmRequest(context.options.messages);
8543
- await writeLogFile(llmRequestsDir, filename, content);
8544
- }
8545
- },
8546
- // onStreamChunk: Real-time updates as LLM generates tokens
8547
- // This enables responsive UIs that show progress during generation
8548
- onStreamChunk: async (context) => {
8549
- progress.update(context.accumulatedText.length);
8550
- if (context.usage) {
8551
- if (context.usage.inputTokens) {
8552
- progress.setInputTokens(context.usage.inputTokens, false);
8553
- }
8554
- if (context.usage.outputTokens) {
8555
- progress.setOutputTokens(context.usage.outputTokens, false);
8556
- }
8557
- progress.setCachedTokens(
8558
- context.usage.cachedInputTokens ?? 0,
8559
- context.usage.cacheCreationInputTokens ?? 0
8560
- );
8561
- }
8562
- },
8563
- // onLLMCallComplete: Finalize metrics after each LLM call
8564
- // This is where you'd typically log metrics or update dashboards
8565
- onLLMCallComplete: async (context) => {
8566
- isStreaming = false;
8567
- usage = context.usage;
8568
- iterations = Math.max(iterations, context.iteration + 1);
8569
- if (context.usage) {
8570
- if (context.usage.inputTokens) {
8571
- progress.setInputTokens(context.usage.inputTokens, false);
8572
- }
8573
- if (context.usage.outputTokens) {
8574
- progress.setOutputTokens(context.usage.outputTokens, false);
8575
- }
8576
- }
8577
- let callCost;
8578
- if (context.usage && client.modelRegistry) {
8579
- try {
8580
- const modelName = context.options.model.includes(":") ? context.options.model.split(":")[1] : context.options.model;
8581
- const costResult = client.modelRegistry.estimateCost(
8582
- modelName,
8583
- context.usage.inputTokens,
8584
- context.usage.outputTokens,
8585
- context.usage.cachedInputTokens ?? 0,
8586
- context.usage.cacheCreationInputTokens ?? 0
8587
- );
8588
- if (costResult) callCost = costResult.totalCost;
8589
- } catch {
8590
- }
8591
- }
8592
- const callElapsed = progress.getCallElapsedSeconds();
8593
- progress.endCall(context.usage);
8594
- if (!options.quiet) {
8595
- const summary = renderSummary({
8596
- iterations: context.iteration + 1,
8597
- model: options.model,
8598
- usage: context.usage,
8599
- elapsedSeconds: callElapsed,
8600
- cost: callCost,
8601
- finishReason: context.finishReason
8602
- });
8603
- if (summary) {
8604
- env.stderr.write(`${summary}
8605
- `);
8606
- }
8607
- }
8608
- if (llmResponsesDir) {
8609
- const filename = `${Date.now()}_call_${llmCallCounter}.response.txt`;
8610
- await writeLogFile(llmResponsesDir, filename, context.rawResponse);
8611
- }
8612
- }
8613
- },
8614
- // SHOWCASE: Controller-based approval gating for gadgets
8615
- //
8616
- // This demonstrates how to add safety layers WITHOUT modifying gadgets.
8617
- // The ApprovalManager handles approval flows externally via beforeGadgetExecution.
8618
- // Approval modes are configurable via cli.toml:
8619
- // - "allowed": auto-proceed
8620
- // - "denied": auto-reject, return message to LLM
8621
- // - "approval-required": prompt user interactively
8622
- //
8623
- // Default: RunCommand, WriteFile, EditFile require approval unless overridden.
8624
- controllers: {
8625
- beforeGadgetExecution: async (ctx) => {
8626
- const mode = approvalManager.getApprovalMode(ctx.gadgetName);
8627
- if (mode === "allowed") {
8628
- return { action: "proceed" };
8629
- }
8630
- const stdinTTY = isInteractive(env.stdin);
8631
- const stderrTTY2 = env.stderr.isTTY === true;
8632
- const canPrompt = stdinTTY && stderrTTY2;
8633
- if (!canPrompt) {
8634
- if (mode === "approval-required") {
8635
- return {
8636
- action: "skip",
8637
- syntheticResult: `status=denied
8638
-
8639
- ${ctx.gadgetName} requires interactive approval. Run in a terminal to approve.`
8640
- };
8641
- }
8642
- if (mode === "denied") {
8643
- return {
8644
- action: "skip",
8645
- syntheticResult: `status=denied
8646
-
8647
- ${ctx.gadgetName} is denied by configuration.`
8648
- };
8649
- }
8650
- return { action: "proceed" };
8651
- }
8652
- const result = await approvalManager.requestApproval(ctx.gadgetName, ctx.parameters);
8653
- if (!result.approved) {
8654
- return {
8655
- action: "skip",
8656
- syntheticResult: `status=denied
8546
+ }
8547
+ }
8548
+ function hasTemplateSyntax(str) {
8549
+ return str.includes("<%");
8550
+ }
8657
8551
 
8658
- Denied: ${result.reason ?? "by user"}`
8659
- };
8660
- }
8661
- return { action: "proceed" };
8552
+ // src/cli/config.ts
8553
+ var VALID_APPROVAL_MODES = ["allowed", "denied", "approval-required"];
8554
+ var GLOBAL_CONFIG_KEYS = /* @__PURE__ */ new Set(["log-level", "log-file", "log-reset"]);
8555
+ var VALID_LOG_LEVELS = ["silly", "trace", "debug", "info", "warn", "error", "fatal"];
8556
+ var COMPLETE_CONFIG_KEYS = /* @__PURE__ */ new Set([
8557
+ "model",
8558
+ "system",
8559
+ "temperature",
8560
+ "max-tokens",
8561
+ "quiet",
8562
+ "inherits",
8563
+ "log-level",
8564
+ "log-file",
8565
+ "log-reset",
8566
+ "log-llm-requests",
8567
+ "log-llm-responses",
8568
+ "type",
8569
+ // Allowed for inheritance compatibility, ignored for built-in commands
8570
+ "docker",
8571
+ // Enable Docker sandboxing (only effective for agent type)
8572
+ "docker-cwd-permission"
8573
+ // Override CWD mount permission for this profile
8574
+ ]);
8575
+ var AGENT_CONFIG_KEYS = /* @__PURE__ */ new Set([
8576
+ "model",
8577
+ "system",
8578
+ "temperature",
8579
+ "max-iterations",
8580
+ "gadgets",
8581
+ // Full replacement (preferred)
8582
+ "gadget-add",
8583
+ // Add to inherited gadgets
8584
+ "gadget-remove",
8585
+ // Remove from inherited gadgets
8586
+ "gadget",
8587
+ // DEPRECATED: alias for gadgets
8588
+ "builtins",
8589
+ "builtin-interaction",
8590
+ "gadget-start-prefix",
8591
+ "gadget-end-prefix",
8592
+ "gadget-arg-prefix",
8593
+ "gadget-approval",
8594
+ "quiet",
8595
+ "inherits",
8596
+ "log-level",
8597
+ "log-file",
8598
+ "log-reset",
8599
+ "log-llm-requests",
8600
+ "log-llm-responses",
8601
+ "type",
8602
+ // Allowed for inheritance compatibility, ignored for built-in commands
8603
+ "docker",
8604
+ // Enable Docker sandboxing for this profile
8605
+ "docker-cwd-permission"
8606
+ // Override CWD mount permission for this profile
8607
+ ]);
8608
+ var CUSTOM_CONFIG_KEYS = /* @__PURE__ */ new Set([
8609
+ ...COMPLETE_CONFIG_KEYS,
8610
+ ...AGENT_CONFIG_KEYS,
8611
+ "type",
8612
+ "description"
8613
+ ]);
8614
+ function getConfigPath() {
8615
+ return (0, import_node_path8.join)((0, import_node_os2.homedir)(), ".llmist", "cli.toml");
8616
+ }
8617
+ var ConfigError = class extends Error {
8618
+ constructor(message, path5) {
8619
+ super(path5 ? `${path5}: ${message}` : message);
8620
+ this.path = path5;
8621
+ this.name = "ConfigError";
8622
+ }
8623
+ };
8624
+ function validateString(value, key, section) {
8625
+ if (typeof value !== "string") {
8626
+ throw new ConfigError(`[${section}].${key} must be a string`);
8627
+ }
8628
+ return value;
8629
+ }
8630
+ function validateNumber(value, key, section, opts) {
8631
+ if (typeof value !== "number") {
8632
+ throw new ConfigError(`[${section}].${key} must be a number`);
8633
+ }
8634
+ if (opts?.integer && !Number.isInteger(value)) {
8635
+ throw new ConfigError(`[${section}].${key} must be an integer`);
8636
+ }
8637
+ if (opts?.min !== void 0 && value < opts.min) {
8638
+ throw new ConfigError(`[${section}].${key} must be >= ${opts.min}`);
8639
+ }
8640
+ if (opts?.max !== void 0 && value > opts.max) {
8641
+ throw new ConfigError(`[${section}].${key} must be <= ${opts.max}`);
8642
+ }
8643
+ return value;
8644
+ }
8645
+ function validateBoolean(value, key, section) {
8646
+ if (typeof value !== "boolean") {
8647
+ throw new ConfigError(`[${section}].${key} must be a boolean`);
8648
+ }
8649
+ return value;
8650
+ }
8651
+ function validateStringArray(value, key, section) {
8652
+ if (!Array.isArray(value)) {
8653
+ throw new ConfigError(`[${section}].${key} must be an array`);
8654
+ }
8655
+ for (let i = 0; i < value.length; i++) {
8656
+ if (typeof value[i] !== "string") {
8657
+ throw new ConfigError(`[${section}].${key}[${i}] must be a string`);
8658
+ }
8659
+ }
8660
+ return value;
8661
+ }
8662
+ function validateInherits(value, section) {
8663
+ if (typeof value === "string") {
8664
+ return value;
8665
+ }
8666
+ if (Array.isArray(value)) {
8667
+ for (let i = 0; i < value.length; i++) {
8668
+ if (typeof value[i] !== "string") {
8669
+ throw new ConfigError(`[${section}].inherits[${i}] must be a string`);
8662
8670
  }
8663
8671
  }
8664
- });
8665
- if (options.system) {
8666
- builder.withSystem(options.system);
8672
+ return value;
8667
8673
  }
8668
- if (options.maxIterations !== void 0) {
8669
- builder.withMaxIterations(options.maxIterations);
8674
+ throw new ConfigError(`[${section}].inherits must be a string or array of strings`);
8675
+ }
8676
+ function validateGadgetApproval(value, section) {
8677
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
8678
+ throw new ConfigError(
8679
+ `[${section}].gadget-approval must be a table (e.g., { WriteFile = "approval-required" })`
8680
+ );
8681
+ }
8682
+ const result = {};
8683
+ for (const [gadgetName, mode] of Object.entries(value)) {
8684
+ if (typeof mode !== "string") {
8685
+ throw new ConfigError(
8686
+ `[${section}].gadget-approval.${gadgetName} must be a string`
8687
+ );
8688
+ }
8689
+ if (!VALID_APPROVAL_MODES.includes(mode)) {
8690
+ throw new ConfigError(
8691
+ `[${section}].gadget-approval.${gadgetName} must be one of: ${VALID_APPROVAL_MODES.join(", ")}`
8692
+ );
8693
+ }
8694
+ result[gadgetName] = mode;
8695
+ }
8696
+ return result;
8697
+ }
8698
+ function validateLoggingConfig(raw, section) {
8699
+ const result = {};
8700
+ if ("log-level" in raw) {
8701
+ const level = validateString(raw["log-level"], "log-level", section);
8702
+ if (!VALID_LOG_LEVELS.includes(level)) {
8703
+ throw new ConfigError(
8704
+ `[${section}].log-level must be one of: ${VALID_LOG_LEVELS.join(", ")}`
8705
+ );
8706
+ }
8707
+ result["log-level"] = level;
8708
+ }
8709
+ if ("log-file" in raw) {
8710
+ result["log-file"] = validateString(raw["log-file"], "log-file", section);
8711
+ }
8712
+ if ("log-reset" in raw) {
8713
+ result["log-reset"] = validateBoolean(raw["log-reset"], "log-reset", section);
8714
+ }
8715
+ return result;
8716
+ }
8717
+ function validateBaseConfig(raw, section) {
8718
+ const result = {};
8719
+ if ("model" in raw) {
8720
+ result.model = validateString(raw.model, "model", section);
8721
+ }
8722
+ if ("system" in raw) {
8723
+ result.system = validateString(raw.system, "system", section);
8724
+ }
8725
+ if ("temperature" in raw) {
8726
+ result.temperature = validateNumber(raw.temperature, "temperature", section, {
8727
+ min: 0,
8728
+ max: 2
8729
+ });
8730
+ }
8731
+ if ("inherits" in raw) {
8732
+ result.inherits = validateInherits(raw.inherits, section);
8733
+ }
8734
+ if ("docker" in raw) {
8735
+ result.docker = validateBoolean(raw.docker, "docker", section);
8736
+ }
8737
+ if ("docker-cwd-permission" in raw) {
8738
+ const perm = validateString(raw["docker-cwd-permission"], "docker-cwd-permission", section);
8739
+ if (perm !== "ro" && perm !== "rw") {
8740
+ throw new ConfigError(`[${section}].docker-cwd-permission must be "ro" or "rw"`);
8741
+ }
8742
+ result["docker-cwd-permission"] = perm;
8743
+ }
8744
+ return result;
8745
+ }
8746
+ function validateGlobalConfig(raw, section) {
8747
+ if (typeof raw !== "object" || raw === null) {
8748
+ throw new ConfigError(`[${section}] must be a table`);
8749
+ }
8750
+ const rawObj = raw;
8751
+ for (const key of Object.keys(rawObj)) {
8752
+ if (!GLOBAL_CONFIG_KEYS.has(key)) {
8753
+ throw new ConfigError(`[${section}].${key} is not a valid option`);
8754
+ }
8755
+ }
8756
+ return validateLoggingConfig(rawObj, section);
8757
+ }
8758
+ function validateCompleteConfig(raw, section) {
8759
+ if (typeof raw !== "object" || raw === null) {
8760
+ throw new ConfigError(`[${section}] must be a table`);
8761
+ }
8762
+ const rawObj = raw;
8763
+ for (const key of Object.keys(rawObj)) {
8764
+ if (!COMPLETE_CONFIG_KEYS.has(key)) {
8765
+ throw new ConfigError(`[${section}].${key} is not a valid option`);
8766
+ }
8767
+ }
8768
+ const result = {
8769
+ ...validateBaseConfig(rawObj, section),
8770
+ ...validateLoggingConfig(rawObj, section)
8771
+ };
8772
+ if ("max-tokens" in rawObj) {
8773
+ result["max-tokens"] = validateNumber(rawObj["max-tokens"], "max-tokens", section, {
8774
+ integer: true,
8775
+ min: 1
8776
+ });
8777
+ }
8778
+ if ("quiet" in rawObj) {
8779
+ result.quiet = validateBoolean(rawObj.quiet, "quiet", section);
8780
+ }
8781
+ if ("log-llm-requests" in rawObj) {
8782
+ result["log-llm-requests"] = validateStringOrBoolean(
8783
+ rawObj["log-llm-requests"],
8784
+ "log-llm-requests",
8785
+ section
8786
+ );
8787
+ }
8788
+ if ("log-llm-responses" in rawObj) {
8789
+ result["log-llm-responses"] = validateStringOrBoolean(
8790
+ rawObj["log-llm-responses"],
8791
+ "log-llm-responses",
8792
+ section
8793
+ );
8794
+ }
8795
+ return result;
8796
+ }
8797
+ function validateAgentConfig(raw, section) {
8798
+ if (typeof raw !== "object" || raw === null) {
8799
+ throw new ConfigError(`[${section}] must be a table`);
8800
+ }
8801
+ const rawObj = raw;
8802
+ for (const key of Object.keys(rawObj)) {
8803
+ if (!AGENT_CONFIG_KEYS.has(key)) {
8804
+ throw new ConfigError(`[${section}].${key} is not a valid option`);
8805
+ }
8806
+ }
8807
+ const result = {
8808
+ ...validateBaseConfig(rawObj, section),
8809
+ ...validateLoggingConfig(rawObj, section)
8810
+ };
8811
+ if ("max-iterations" in rawObj) {
8812
+ result["max-iterations"] = validateNumber(rawObj["max-iterations"], "max-iterations", section, {
8813
+ integer: true,
8814
+ min: 1
8815
+ });
8816
+ }
8817
+ if ("gadgets" in rawObj) {
8818
+ result.gadgets = validateStringArray(rawObj.gadgets, "gadgets", section);
8819
+ }
8820
+ if ("gadget-add" in rawObj) {
8821
+ result["gadget-add"] = validateStringArray(rawObj["gadget-add"], "gadget-add", section);
8822
+ }
8823
+ if ("gadget-remove" in rawObj) {
8824
+ result["gadget-remove"] = validateStringArray(rawObj["gadget-remove"], "gadget-remove", section);
8825
+ }
8826
+ if ("gadget" in rawObj) {
8827
+ result.gadget = validateStringArray(rawObj.gadget, "gadget", section);
8828
+ }
8829
+ if ("builtins" in rawObj) {
8830
+ result.builtins = validateBoolean(rawObj.builtins, "builtins", section);
8831
+ }
8832
+ if ("builtin-interaction" in rawObj) {
8833
+ result["builtin-interaction"] = validateBoolean(
8834
+ rawObj["builtin-interaction"],
8835
+ "builtin-interaction",
8836
+ section
8837
+ );
8838
+ }
8839
+ if ("gadget-start-prefix" in rawObj) {
8840
+ result["gadget-start-prefix"] = validateString(
8841
+ rawObj["gadget-start-prefix"],
8842
+ "gadget-start-prefix",
8843
+ section
8844
+ );
8845
+ }
8846
+ if ("gadget-end-prefix" in rawObj) {
8847
+ result["gadget-end-prefix"] = validateString(
8848
+ rawObj["gadget-end-prefix"],
8849
+ "gadget-end-prefix",
8850
+ section
8851
+ );
8852
+ }
8853
+ if ("gadget-arg-prefix" in rawObj) {
8854
+ result["gadget-arg-prefix"] = validateString(
8855
+ rawObj["gadget-arg-prefix"],
8856
+ "gadget-arg-prefix",
8857
+ section
8858
+ );
8859
+ }
8860
+ if ("gadget-approval" in rawObj) {
8861
+ result["gadget-approval"] = validateGadgetApproval(rawObj["gadget-approval"], section);
8862
+ }
8863
+ if ("quiet" in rawObj) {
8864
+ result.quiet = validateBoolean(rawObj.quiet, "quiet", section);
8865
+ }
8866
+ if ("log-llm-requests" in rawObj) {
8867
+ result["log-llm-requests"] = validateStringOrBoolean(
8868
+ rawObj["log-llm-requests"],
8869
+ "log-llm-requests",
8870
+ section
8871
+ );
8872
+ }
8873
+ if ("log-llm-responses" in rawObj) {
8874
+ result["log-llm-responses"] = validateStringOrBoolean(
8875
+ rawObj["log-llm-responses"],
8876
+ "log-llm-responses",
8877
+ section
8878
+ );
8879
+ }
8880
+ return result;
8881
+ }
8882
+ function validateStringOrBoolean(value, field, section) {
8883
+ if (typeof value === "string" || typeof value === "boolean") {
8884
+ return value;
8885
+ }
8886
+ throw new ConfigError(`[${section}].${field} must be a string or boolean`);
8887
+ }
8888
+ function validateCustomConfig(raw, section) {
8889
+ if (typeof raw !== "object" || raw === null) {
8890
+ throw new ConfigError(`[${section}] must be a table`);
8891
+ }
8892
+ const rawObj = raw;
8893
+ for (const key of Object.keys(rawObj)) {
8894
+ if (!CUSTOM_CONFIG_KEYS.has(key)) {
8895
+ throw new ConfigError(`[${section}].${key} is not a valid option`);
8896
+ }
8897
+ }
8898
+ let type = "agent";
8899
+ if ("type" in rawObj) {
8900
+ const typeValue = validateString(rawObj.type, "type", section);
8901
+ if (typeValue !== "agent" && typeValue !== "complete") {
8902
+ throw new ConfigError(`[${section}].type must be "agent" or "complete"`);
8903
+ }
8904
+ type = typeValue;
8905
+ }
8906
+ const result = {
8907
+ ...validateBaseConfig(rawObj, section),
8908
+ type
8909
+ };
8910
+ if ("description" in rawObj) {
8911
+ result.description = validateString(rawObj.description, "description", section);
8670
8912
  }
8671
- if (options.temperature !== void 0) {
8672
- builder.withTemperature(options.temperature);
8913
+ if ("max-iterations" in rawObj) {
8914
+ result["max-iterations"] = validateNumber(rawObj["max-iterations"], "max-iterations", section, {
8915
+ integer: true,
8916
+ min: 1
8917
+ });
8673
8918
  }
8674
- const humanInputHandler = createHumanInputHandler(env, progress, keyboard);
8675
- if (humanInputHandler) {
8676
- builder.onHumanInput(humanInputHandler);
8919
+ if ("gadgets" in rawObj) {
8920
+ result.gadgets = validateStringArray(rawObj.gadgets, "gadgets", section);
8677
8921
  }
8678
- builder.withSignal(abortController.signal);
8679
- const gadgets = registry.getAll();
8680
- if (gadgets.length > 0) {
8681
- builder.withGadgets(...gadgets);
8922
+ if ("gadget-add" in rawObj) {
8923
+ result["gadget-add"] = validateStringArray(rawObj["gadget-add"], "gadget-add", section);
8682
8924
  }
8683
- if (options.gadgetStartPrefix) {
8684
- builder.withGadgetStartPrefix(options.gadgetStartPrefix);
8925
+ if ("gadget-remove" in rawObj) {
8926
+ result["gadget-remove"] = validateStringArray(rawObj["gadget-remove"], "gadget-remove", section);
8685
8927
  }
8686
- if (options.gadgetEndPrefix) {
8687
- builder.withGadgetEndPrefix(options.gadgetEndPrefix);
8928
+ if ("gadget" in rawObj) {
8929
+ result.gadget = validateStringArray(rawObj.gadget, "gadget", section);
8688
8930
  }
8689
- if (options.gadgetArgPrefix) {
8690
- builder.withGadgetArgPrefix(options.gadgetArgPrefix);
8931
+ if ("builtins" in rawObj) {
8932
+ result.builtins = validateBoolean(rawObj.builtins, "builtins", section);
8691
8933
  }
8692
- builder.withSyntheticGadgetCall(
8693
- "TellUser",
8694
- {
8695
- 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?",
8696
- done: false,
8697
- type: "info"
8698
- },
8699
- "\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?"
8700
- );
8701
- builder.withTextOnlyHandler("acknowledge");
8702
- builder.withTextWithGadgetsHandler({
8703
- gadgetName: "TellUser",
8704
- parameterMapping: (text) => ({ message: text, done: false, type: "info" }),
8705
- resultMapping: (text) => `\u2139\uFE0F ${text}`
8706
- });
8707
- const agent = builder.ask(prompt);
8708
- let textBuffer = "";
8709
- const flushTextBuffer = () => {
8710
- if (textBuffer) {
8711
- const output = options.quiet ? textBuffer : renderMarkdownWithSeparators(textBuffer);
8712
- printer.write(output);
8713
- textBuffer = "";
8714
- }
8715
- };
8716
- try {
8717
- for await (const event of agent.run()) {
8718
- if (event.type === "text") {
8719
- progress.pause();
8720
- textBuffer += event.content;
8721
- } else if (event.type === "gadget_result") {
8722
- flushTextBuffer();
8723
- progress.pause();
8724
- if (options.quiet) {
8725
- if (event.result.gadgetName === "TellUser" && event.result.parameters?.message) {
8726
- const message = String(event.result.parameters.message);
8727
- env.stdout.write(`${message}
8728
- `);
8729
- }
8730
- } else {
8731
- const tokenCount = await countGadgetOutputTokens(event.result.result);
8732
- env.stderr.write(`${formatGadgetSummary({ ...event.result, tokenCount })}
8733
- `);
8734
- }
8735
- }
8736
- }
8737
- } catch (error) {
8738
- if (!isAbortError(error)) {
8739
- throw error;
8740
- }
8741
- } finally {
8742
- isStreaming = false;
8743
- keyboard.cleanupEsc?.();
8744
- keyboard.cleanupSigint?.();
8934
+ if ("builtin-interaction" in rawObj) {
8935
+ result["builtin-interaction"] = validateBoolean(
8936
+ rawObj["builtin-interaction"],
8937
+ "builtin-interaction",
8938
+ section
8939
+ );
8745
8940
  }
8746
- flushTextBuffer();
8747
- progress.complete();
8748
- printer.ensureNewline();
8749
- if (!options.quiet && iterations > 1) {
8750
- env.stderr.write(`${import_chalk5.default.dim("\u2500".repeat(40))}
8751
- `);
8752
- const summary = renderOverallSummary({
8753
- totalTokens: usage?.totalTokens,
8754
- iterations,
8755
- elapsedSeconds: progress.getTotalElapsedSeconds(),
8756
- cost: progress.getTotalCost()
8757
- });
8758
- if (summary) {
8759
- env.stderr.write(`${summary}
8760
- `);
8761
- }
8941
+ if ("gadget-start-prefix" in rawObj) {
8942
+ result["gadget-start-prefix"] = validateString(
8943
+ rawObj["gadget-start-prefix"],
8944
+ "gadget-start-prefix",
8945
+ section
8946
+ );
8762
8947
  }
8763
- }
8764
- function registerAgentCommand(program, env, config) {
8765
- const cmd = program.command(COMMANDS.agent).description("Run the llmist agent loop with optional gadgets.").argument("[prompt]", "Prompt for the agent loop. Falls back to stdin when available.");
8766
- addAgentOptions(cmd, config);
8767
- cmd.action(
8768
- (prompt, options) => executeAction(() => {
8769
- const mergedOptions = {
8770
- ...options,
8771
- gadgetApproval: config?.["gadget-approval"]
8772
- };
8773
- return executeAgent(prompt, mergedOptions, env);
8774
- }, env)
8775
- );
8776
- }
8777
-
8778
- // src/cli/complete-command.ts
8779
- init_messages();
8780
- init_model_shortcuts();
8781
- init_constants2();
8782
- async function executeComplete(promptArg, options, env) {
8783
- const prompt = await resolvePrompt(promptArg, env);
8784
- const client = env.createClient();
8785
- const model = resolveModel(options.model);
8786
- const builder = new LLMMessageBuilder();
8787
- if (options.system) {
8788
- builder.addSystem(options.system);
8948
+ if ("gadget-end-prefix" in rawObj) {
8949
+ result["gadget-end-prefix"] = validateString(
8950
+ rawObj["gadget-end-prefix"],
8951
+ "gadget-end-prefix",
8952
+ section
8953
+ );
8789
8954
  }
8790
- builder.addUser(prompt);
8791
- const messages = builder.build();
8792
- const llmRequestsDir = resolveLogDir(options.logLlmRequests, "requests");
8793
- const llmResponsesDir = resolveLogDir(options.logLlmResponses, "responses");
8794
- const timestamp = Date.now();
8795
- if (llmRequestsDir) {
8796
- const filename = `${timestamp}_complete.request.txt`;
8797
- const content = formatLlmRequest(messages);
8798
- await writeLogFile(llmRequestsDir, filename, content);
8955
+ if ("gadget-arg-prefix" in rawObj) {
8956
+ result["gadget-arg-prefix"] = validateString(
8957
+ rawObj["gadget-arg-prefix"],
8958
+ "gadget-arg-prefix",
8959
+ section
8960
+ );
8799
8961
  }
8800
- const stream2 = client.stream({
8801
- model,
8802
- messages,
8803
- temperature: options.temperature,
8804
- maxTokens: options.maxTokens
8805
- });
8806
- const printer = new StreamPrinter(env.stdout);
8807
- const stderrTTY = env.stderr.isTTY === true;
8808
- const progress = new StreamProgress(env.stderr, stderrTTY, client.modelRegistry);
8809
- const estimatedInputTokens = Math.round(prompt.length / FALLBACK_CHARS_PER_TOKEN);
8810
- progress.startCall(model, estimatedInputTokens);
8811
- let finishReason;
8812
- let usage;
8813
- let accumulatedResponse = "";
8814
- for await (const chunk of stream2) {
8815
- if (chunk.usage) {
8816
- usage = chunk.usage;
8817
- if (chunk.usage.inputTokens) {
8818
- progress.setInputTokens(chunk.usage.inputTokens, false);
8819
- }
8820
- if (chunk.usage.outputTokens) {
8821
- progress.setOutputTokens(chunk.usage.outputTokens, false);
8822
- }
8823
- }
8824
- if (chunk.text) {
8825
- progress.pause();
8826
- accumulatedResponse += chunk.text;
8827
- progress.update(accumulatedResponse.length);
8828
- printer.write(chunk.text);
8829
- }
8830
- if (chunk.finishReason !== void 0) {
8831
- finishReason = chunk.finishReason;
8832
- }
8962
+ if ("gadget-approval" in rawObj) {
8963
+ result["gadget-approval"] = validateGadgetApproval(rawObj["gadget-approval"], section);
8833
8964
  }
8834
- progress.endCall(usage);
8835
- progress.complete();
8836
- printer.ensureNewline();
8837
- if (llmResponsesDir) {
8838
- const filename = `${timestamp}_complete.response.txt`;
8839
- await writeLogFile(llmResponsesDir, filename, accumulatedResponse);
8965
+ if ("max-tokens" in rawObj) {
8966
+ result["max-tokens"] = validateNumber(rawObj["max-tokens"], "max-tokens", section, {
8967
+ integer: true,
8968
+ min: 1
8969
+ });
8840
8970
  }
8841
- if (stderrTTY && !options.quiet) {
8842
- const summary = renderSummary({ finishReason, usage, cost: progress.getTotalCost() });
8843
- if (summary) {
8844
- env.stderr.write(`${summary}
8845
- `);
8846
- }
8971
+ if ("quiet" in rawObj) {
8972
+ result.quiet = validateBoolean(rawObj.quiet, "quiet", section);
8847
8973
  }
8974
+ Object.assign(result, validateLoggingConfig(rawObj, section));
8975
+ return result;
8848
8976
  }
8849
- function registerCompleteCommand(program, env, config) {
8850
- const cmd = program.command(COMMANDS.complete).description("Stream a single completion from a specified model.").argument("[prompt]", "Prompt to send to the LLM. If omitted, stdin is used when available.");
8851
- addCompleteOptions(cmd, config);
8852
- cmd.action(
8853
- (prompt, options) => executeAction(() => executeComplete(prompt, options, env), env)
8854
- );
8855
- }
8856
-
8857
- // src/cli/config.ts
8858
- var import_node_fs8 = require("fs");
8859
- var import_node_os2 = require("os");
8860
- var import_node_path8 = require("path");
8861
- var import_js_toml = require("js-toml");
8862
-
8863
- // src/cli/templates.ts
8864
- var import_eta = require("eta");
8865
- var TemplateError = class extends Error {
8866
- constructor(message, promptName, configPath) {
8867
- super(promptName ? `[prompts.${promptName}]: ${message}` : message);
8868
- this.promptName = promptName;
8869
- this.configPath = configPath;
8870
- this.name = "TemplateError";
8977
+ function validatePromptsConfig(raw, section) {
8978
+ if (typeof raw !== "object" || raw === null) {
8979
+ throw new ConfigError(`[${section}] must be a table`);
8871
8980
  }
8872
- };
8873
- function createTemplateEngine(prompts, configPath) {
8874
- const eta = new import_eta.Eta({
8875
- views: "/",
8876
- // Required but we use named templates
8877
- autoEscape: false,
8878
- // Don't escape - these are prompts, not HTML
8879
- autoTrim: false
8880
- // Preserve whitespace in prompts
8881
- });
8882
- for (const [name, template] of Object.entries(prompts)) {
8981
+ const result = {};
8982
+ for (const [key, value] of Object.entries(raw)) {
8983
+ if (typeof value !== "string") {
8984
+ throw new ConfigError(`[${section}].${key} must be a string`);
8985
+ }
8986
+ result[key] = value;
8987
+ }
8988
+ return result;
8989
+ }
8990
+ function validateConfig(raw, configPath) {
8991
+ if (typeof raw !== "object" || raw === null) {
8992
+ throw new ConfigError("Config must be a TOML table", configPath);
8993
+ }
8994
+ const rawObj = raw;
8995
+ const result = {};
8996
+ for (const [key, value] of Object.entries(rawObj)) {
8883
8997
  try {
8884
- eta.loadTemplate(`@${name}`, template);
8998
+ if (key === "global") {
8999
+ result.global = validateGlobalConfig(value, key);
9000
+ } else if (key === "complete") {
9001
+ result.complete = validateCompleteConfig(value, key);
9002
+ } else if (key === "agent") {
9003
+ result.agent = validateAgentConfig(value, key);
9004
+ } else if (key === "prompts") {
9005
+ result.prompts = validatePromptsConfig(value, key);
9006
+ } else if (key === "docker") {
9007
+ result.docker = validateDockerConfig(value, key);
9008
+ } else {
9009
+ result[key] = validateCustomConfig(value, key);
9010
+ }
8885
9011
  } catch (error) {
8886
- throw new TemplateError(
8887
- error instanceof Error ? error.message : String(error),
8888
- name,
8889
- configPath
8890
- );
9012
+ if (error instanceof ConfigError) {
9013
+ throw new ConfigError(error.message, configPath);
9014
+ }
9015
+ throw error;
8891
9016
  }
8892
9017
  }
8893
- return eta;
9018
+ return result;
8894
9019
  }
8895
- function resolveTemplate(eta, template, context = {}, configPath) {
9020
+ function loadConfig() {
9021
+ const configPath = getConfigPath();
9022
+ if (!(0, import_node_fs8.existsSync)(configPath)) {
9023
+ return {};
9024
+ }
9025
+ let content;
8896
9026
  try {
8897
- const fullContext = {
8898
- ...context,
8899
- env: process.env,
8900
- date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
8901
- // "2025-12-01"
8902
- };
8903
- return eta.renderString(template, fullContext);
9027
+ content = (0, import_node_fs8.readFileSync)(configPath, "utf-8");
8904
9028
  } catch (error) {
8905
- throw new TemplateError(
8906
- error instanceof Error ? error.message : String(error),
8907
- void 0,
9029
+ throw new ConfigError(
9030
+ `Failed to read config file: ${error instanceof Error ? error.message : "Unknown error"}`,
9031
+ configPath
9032
+ );
9033
+ }
9034
+ let raw;
9035
+ try {
9036
+ raw = (0, import_js_toml.load)(content);
9037
+ } catch (error) {
9038
+ throw new ConfigError(
9039
+ `Invalid TOML syntax: ${error instanceof Error ? error.message : "Unknown error"}`,
8908
9040
  configPath
8909
9041
  );
8910
9042
  }
9043
+ const validated = validateConfig(raw, configPath);
9044
+ const inherited = resolveInheritance(validated, configPath);
9045
+ return resolveTemplatesInConfig(inherited, configPath);
8911
9046
  }
8912
- function validatePrompts(prompts, configPath) {
8913
- const eta = createTemplateEngine(prompts, configPath);
9047
+ function getCustomCommandNames(config) {
9048
+ const reserved = /* @__PURE__ */ new Set(["global", "complete", "agent", "prompts", "docker"]);
9049
+ return Object.keys(config).filter((key) => !reserved.has(key));
9050
+ }
9051
+ function resolveTemplatesInConfig(config, configPath) {
9052
+ const prompts = config.prompts ?? {};
9053
+ const hasPrompts = Object.keys(prompts).length > 0;
9054
+ let hasTemplates = false;
9055
+ for (const [sectionName, section] of Object.entries(config)) {
9056
+ if (sectionName === "global" || sectionName === "prompts") continue;
9057
+ if (!section || typeof section !== "object") continue;
9058
+ const sectionObj = section;
9059
+ if (typeof sectionObj.system === "string" && hasTemplateSyntax(sectionObj.system)) {
9060
+ hasTemplates = true;
9061
+ break;
9062
+ }
9063
+ }
9064
+ for (const template of Object.values(prompts)) {
9065
+ if (hasTemplateSyntax(template)) {
9066
+ hasTemplates = true;
9067
+ break;
9068
+ }
9069
+ }
9070
+ if (!hasPrompts && !hasTemplates) {
9071
+ return config;
9072
+ }
9073
+ try {
9074
+ validatePrompts(prompts, configPath);
9075
+ } catch (error) {
9076
+ if (error instanceof TemplateError) {
9077
+ throw new ConfigError(error.message, configPath);
9078
+ }
9079
+ throw error;
9080
+ }
8914
9081
  for (const [name, template] of Object.entries(prompts)) {
8915
9082
  try {
8916
- eta.renderString(template, { env: {} });
9083
+ validateEnvVars(template, name, configPath);
8917
9084
  } catch (error) {
8918
- throw new TemplateError(
8919
- error instanceof Error ? error.message : String(error),
8920
- name,
8921
- configPath
8922
- );
9085
+ if (error instanceof TemplateError) {
9086
+ throw new ConfigError(error.message, configPath);
9087
+ }
9088
+ throw error;
8923
9089
  }
8924
9090
  }
8925
- }
8926
- function validateEnvVars(template, promptName, configPath) {
8927
- const envVarPattern = /<%=\s*it\.env\.(\w+)\s*%>/g;
8928
- const matches = template.matchAll(envVarPattern);
8929
- for (const match of matches) {
8930
- const varName = match[1];
8931
- if (process.env[varName] === void 0) {
8932
- throw new TemplateError(
8933
- `Environment variable '${varName}' is not set`,
8934
- promptName,
8935
- configPath
8936
- );
9091
+ const eta = createTemplateEngine(prompts, configPath);
9092
+ const result = { ...config };
9093
+ for (const [sectionName, section] of Object.entries(config)) {
9094
+ if (sectionName === "global" || sectionName === "prompts") continue;
9095
+ if (!section || typeof section !== "object") continue;
9096
+ const sectionObj = section;
9097
+ if (typeof sectionObj.system === "string" && hasTemplateSyntax(sectionObj.system)) {
9098
+ try {
9099
+ validateEnvVars(sectionObj.system, void 0, configPath);
9100
+ } catch (error) {
9101
+ if (error instanceof TemplateError) {
9102
+ throw new ConfigError(`[${sectionName}].system: ${error.message}`, configPath);
9103
+ }
9104
+ throw error;
9105
+ }
9106
+ try {
9107
+ const resolved = resolveTemplate(eta, sectionObj.system, {}, configPath);
9108
+ result[sectionName] = {
9109
+ ...sectionObj,
9110
+ system: resolved
9111
+ };
9112
+ } catch (error) {
9113
+ if (error instanceof TemplateError) {
9114
+ throw new ConfigError(`[${sectionName}].system: ${error.message}`, configPath);
9115
+ }
9116
+ throw error;
9117
+ }
8937
9118
  }
8938
9119
  }
9120
+ return result;
8939
9121
  }
8940
- function hasTemplateSyntax(str) {
8941
- return str.includes("<%");
8942
- }
8943
-
8944
- // src/cli/config.ts
8945
- var VALID_APPROVAL_MODES = ["allowed", "denied", "approval-required"];
8946
- var GLOBAL_CONFIG_KEYS = /* @__PURE__ */ new Set(["log-level", "log-file", "log-reset"]);
8947
- var VALID_LOG_LEVELS = ["silly", "trace", "debug", "info", "warn", "error", "fatal"];
8948
- var COMPLETE_CONFIG_KEYS = /* @__PURE__ */ new Set([
8949
- "model",
8950
- "system",
8951
- "temperature",
8952
- "max-tokens",
8953
- "quiet",
8954
- "inherits",
8955
- "log-level",
8956
- "log-file",
8957
- "log-reset",
8958
- "log-llm-requests",
8959
- "log-llm-responses",
8960
- "type"
8961
- // Allowed for inheritance compatibility, ignored for built-in commands
8962
- ]);
8963
- var AGENT_CONFIG_KEYS = /* @__PURE__ */ new Set([
8964
- "model",
8965
- "system",
8966
- "temperature",
8967
- "max-iterations",
8968
- "gadgets",
8969
- // Full replacement (preferred)
8970
- "gadget-add",
8971
- // Add to inherited gadgets
8972
- "gadget-remove",
8973
- // Remove from inherited gadgets
8974
- "gadget",
8975
- // DEPRECATED: alias for gadgets
8976
- "builtins",
8977
- "builtin-interaction",
8978
- "gadget-start-prefix",
8979
- "gadget-end-prefix",
8980
- "gadget-arg-prefix",
8981
- "gadget-approval",
8982
- "quiet",
8983
- "inherits",
8984
- "log-level",
8985
- "log-file",
8986
- "log-reset",
8987
- "log-llm-requests",
8988
- "log-llm-responses",
8989
- "type"
8990
- // Allowed for inheritance compatibility, ignored for built-in commands
8991
- ]);
8992
- var CUSTOM_CONFIG_KEYS = /* @__PURE__ */ new Set([
8993
- ...COMPLETE_CONFIG_KEYS,
8994
- ...AGENT_CONFIG_KEYS,
8995
- "type",
8996
- "description"
8997
- ]);
8998
- function getConfigPath() {
8999
- return (0, import_node_path8.join)((0, import_node_os2.homedir)(), ".llmist", "cli.toml");
9122
+ function resolveGadgets(section, inheritedGadgets, sectionName, configPath) {
9123
+ const hasGadgets = "gadgets" in section;
9124
+ const hasGadgetLegacy = "gadget" in section;
9125
+ const hasGadgetAdd = "gadget-add" in section;
9126
+ const hasGadgetRemove = "gadget-remove" in section;
9127
+ if (hasGadgetLegacy && !hasGadgets) {
9128
+ console.warn(
9129
+ `[config] Warning: [${sectionName}].gadget is deprecated, use 'gadgets' (plural) instead`
9130
+ );
9131
+ }
9132
+ if ((hasGadgets || hasGadgetLegacy) && (hasGadgetAdd || hasGadgetRemove)) {
9133
+ throw new ConfigError(
9134
+ `[${sectionName}] Cannot use 'gadgets' with 'gadget-add'/'gadget-remove'. Use either full replacement (gadgets) OR modification (gadget-add/gadget-remove).`,
9135
+ configPath
9136
+ );
9137
+ }
9138
+ if (hasGadgets) {
9139
+ return section.gadgets;
9140
+ }
9141
+ if (hasGadgetLegacy) {
9142
+ return section.gadget;
9143
+ }
9144
+ let result = [...inheritedGadgets];
9145
+ if (hasGadgetRemove) {
9146
+ const toRemove = new Set(section["gadget-remove"]);
9147
+ result = result.filter((g) => !toRemove.has(g));
9148
+ }
9149
+ if (hasGadgetAdd) {
9150
+ const toAdd = section["gadget-add"];
9151
+ result.push(...toAdd);
9152
+ }
9153
+ return result;
9000
9154
  }
9001
- var ConfigError = class extends Error {
9002
- constructor(message, path5) {
9003
- super(path5 ? `${path5}: ${message}` : message);
9004
- this.path = path5;
9005
- this.name = "ConfigError";
9155
+ function resolveInheritance(config, configPath) {
9156
+ const resolved = {};
9157
+ const resolving = /* @__PURE__ */ new Set();
9158
+ function resolveSection(name) {
9159
+ if (name in resolved) {
9160
+ return resolved[name];
9161
+ }
9162
+ if (resolving.has(name)) {
9163
+ throw new ConfigError(`Circular inheritance detected: ${name}`, configPath);
9164
+ }
9165
+ const section = config[name];
9166
+ if (section === void 0 || typeof section !== "object") {
9167
+ throw new ConfigError(`Cannot inherit from unknown section: ${name}`, configPath);
9168
+ }
9169
+ resolving.add(name);
9170
+ const sectionObj = section;
9171
+ const inheritsRaw = sectionObj.inherits;
9172
+ const inheritsList = inheritsRaw ? Array.isArray(inheritsRaw) ? inheritsRaw : [inheritsRaw] : [];
9173
+ let merged = {};
9174
+ for (const parent of inheritsList) {
9175
+ const parentResolved = resolveSection(parent);
9176
+ merged = { ...merged, ...parentResolved };
9177
+ }
9178
+ const inheritedGadgets = merged.gadgets ?? [];
9179
+ const {
9180
+ inherits: _inherits,
9181
+ gadgets: _gadgets,
9182
+ gadget: _gadget,
9183
+ "gadget-add": _gadgetAdd,
9184
+ "gadget-remove": _gadgetRemove,
9185
+ ...ownValues
9186
+ } = sectionObj;
9187
+ merged = { ...merged, ...ownValues };
9188
+ const resolvedGadgets = resolveGadgets(sectionObj, inheritedGadgets, name, configPath);
9189
+ if (resolvedGadgets.length > 0) {
9190
+ merged.gadgets = resolvedGadgets;
9191
+ }
9192
+ delete merged["gadget"];
9193
+ delete merged["gadget-add"];
9194
+ delete merged["gadget-remove"];
9195
+ resolving.delete(name);
9196
+ resolved[name] = merged;
9197
+ return merged;
9006
9198
  }
9007
- };
9008
- function validateString(value, key, section) {
9009
- if (typeof value !== "string") {
9010
- throw new ConfigError(`[${section}].${key} must be a string`);
9199
+ for (const name of Object.keys(config)) {
9200
+ resolveSection(name);
9011
9201
  }
9012
- return value;
9202
+ return resolved;
9013
9203
  }
9014
- function validateNumber(value, key, section, opts) {
9015
- if (typeof value !== "number") {
9016
- throw new ConfigError(`[${section}].${key} must be a number`);
9017
- }
9018
- if (opts?.integer && !Number.isInteger(value)) {
9019
- throw new ConfigError(`[${section}].${key} must be an integer`);
9020
- }
9021
- if (opts?.min !== void 0 && value < opts.min) {
9022
- throw new ConfigError(`[${section}].${key} must be >= ${opts.min}`);
9023
- }
9024
- if (opts?.max !== void 0 && value > opts.max) {
9025
- throw new ConfigError(`[${section}].${key} must be <= ${opts.max}`);
9204
+
9205
+ // src/cli/docker/docker-config.ts
9206
+ var MOUNT_CONFIG_KEYS = /* @__PURE__ */ new Set(["source", "target", "permission"]);
9207
+ function validateString2(value, key, section) {
9208
+ if (typeof value !== "string") {
9209
+ throw new ConfigError(`[${section}].${key} must be a string`);
9026
9210
  }
9027
9211
  return value;
9028
9212
  }
9029
- function validateBoolean(value, key, section) {
9213
+ function validateBoolean2(value, key, section) {
9030
9214
  if (typeof value !== "boolean") {
9031
9215
  throw new ConfigError(`[${section}].${key} must be a boolean`);
9032
9216
  }
9033
9217
  return value;
9034
9218
  }
9035
- function validateStringArray(value, key, section) {
9219
+ function validateStringArray2(value, key, section) {
9036
9220
  if (!Array.isArray(value)) {
9037
9221
  throw new ConfigError(`[${section}].${key} must be an array`);
9038
9222
  }
@@ -9043,535 +9227,949 @@ function validateStringArray(value, key, section) {
9043
9227
  }
9044
9228
  return value;
9045
9229
  }
9046
- function validateInherits(value, section) {
9047
- if (typeof value === "string") {
9048
- return value;
9049
- }
9050
- if (Array.isArray(value)) {
9051
- for (let i = 0; i < value.length; i++) {
9052
- if (typeof value[i] !== "string") {
9053
- throw new ConfigError(`[${section}].inherits[${i}] must be a string`);
9054
- }
9055
- }
9056
- return value;
9230
+ function validateMountPermission(value, key, section) {
9231
+ const str = validateString2(value, key, section);
9232
+ if (!VALID_MOUNT_PERMISSIONS.includes(str)) {
9233
+ throw new ConfigError(
9234
+ `[${section}].${key} must be one of: ${VALID_MOUNT_PERMISSIONS.join(", ")}`
9235
+ );
9057
9236
  }
9058
- throw new ConfigError(`[${section}].inherits must be a string or array of strings`);
9237
+ return str;
9059
9238
  }
9060
- function validateGadgetApproval(value, section) {
9239
+ function validateMountConfig(value, index, section) {
9061
9240
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
9062
- throw new ConfigError(
9063
- `[${section}].gadget-approval must be a table (e.g., { WriteFile = "approval-required" })`
9064
- );
9241
+ throw new ConfigError(`[${section}].mounts[${index}] must be a table`);
9065
9242
  }
9066
- const result = {};
9067
- for (const [gadgetName, mode] of Object.entries(value)) {
9068
- if (typeof mode !== "string") {
9069
- throw new ConfigError(
9070
- `[${section}].gadget-approval.${gadgetName} must be a string`
9071
- );
9072
- }
9073
- if (!VALID_APPROVAL_MODES.includes(mode)) {
9074
- throw new ConfigError(
9075
- `[${section}].gadget-approval.${gadgetName} must be one of: ${VALID_APPROVAL_MODES.join(", ")}`
9076
- );
9243
+ const rawObj = value;
9244
+ const mountSection = `${section}.mounts[${index}]`;
9245
+ for (const key of Object.keys(rawObj)) {
9246
+ if (!MOUNT_CONFIG_KEYS.has(key)) {
9247
+ throw new ConfigError(`[${mountSection}].${key} is not a valid mount option`);
9077
9248
  }
9078
- result[gadgetName] = mode;
9079
9249
  }
9080
- return result;
9081
- }
9082
- function validateLoggingConfig(raw, section) {
9083
- const result = {};
9084
- if ("log-level" in raw) {
9085
- const level = validateString(raw["log-level"], "log-level", section);
9086
- if (!VALID_LOG_LEVELS.includes(level)) {
9087
- throw new ConfigError(
9088
- `[${section}].log-level must be one of: ${VALID_LOG_LEVELS.join(", ")}`
9089
- );
9090
- }
9091
- result["log-level"] = level;
9250
+ if (!("source" in rawObj)) {
9251
+ throw new ConfigError(`[${mountSection}] missing required field 'source'`);
9092
9252
  }
9093
- if ("log-file" in raw) {
9094
- result["log-file"] = validateString(raw["log-file"], "log-file", section);
9253
+ if (!("target" in rawObj)) {
9254
+ throw new ConfigError(`[${mountSection}] missing required field 'target'`);
9095
9255
  }
9096
- if ("log-reset" in raw) {
9097
- result["log-reset"] = validateBoolean(raw["log-reset"], "log-reset", section);
9256
+ if (!("permission" in rawObj)) {
9257
+ throw new ConfigError(`[${mountSection}] missing required field 'permission'`);
9098
9258
  }
9099
- return result;
9259
+ return {
9260
+ source: validateString2(rawObj.source, "source", mountSection),
9261
+ target: validateString2(rawObj.target, "target", mountSection),
9262
+ permission: validateMountPermission(rawObj.permission, "permission", mountSection)
9263
+ };
9100
9264
  }
9101
- function validateBaseConfig(raw, section) {
9102
- const result = {};
9103
- if ("model" in raw) {
9104
- result.model = validateString(raw.model, "model", section);
9105
- }
9106
- if ("system" in raw) {
9107
- result.system = validateString(raw.system, "system", section);
9108
- }
9109
- if ("temperature" in raw) {
9110
- result.temperature = validateNumber(raw.temperature, "temperature", section, {
9111
- min: 0,
9112
- max: 2
9113
- });
9265
+ function validateMountsArray(value, section) {
9266
+ if (!Array.isArray(value)) {
9267
+ throw new ConfigError(`[${section}].mounts must be an array of tables`);
9114
9268
  }
9115
- if ("inherits" in raw) {
9116
- result.inherits = validateInherits(raw.inherits, section);
9269
+ const result = [];
9270
+ for (let i = 0; i < value.length; i++) {
9271
+ result.push(validateMountConfig(value[i], i, section));
9117
9272
  }
9118
9273
  return result;
9119
9274
  }
9120
- function validateGlobalConfig(raw, section) {
9121
- if (typeof raw !== "object" || raw === null) {
9122
- throw new ConfigError(`[${section}] must be a table`);
9123
- }
9124
- const rawObj = raw;
9125
- for (const key of Object.keys(rawObj)) {
9126
- if (!GLOBAL_CONFIG_KEYS.has(key)) {
9127
- throw new ConfigError(`[${section}].${key} is not a valid option`);
9128
- }
9129
- }
9130
- return validateLoggingConfig(rawObj, section);
9131
- }
9132
- function validateCompleteConfig(raw, section) {
9275
+ function validateDockerConfig(raw, section) {
9133
9276
  if (typeof raw !== "object" || raw === null) {
9134
9277
  throw new ConfigError(`[${section}] must be a table`);
9135
9278
  }
9136
9279
  const rawObj = raw;
9137
9280
  for (const key of Object.keys(rawObj)) {
9138
- if (!COMPLETE_CONFIG_KEYS.has(key)) {
9281
+ if (!DOCKER_CONFIG_KEYS.has(key)) {
9139
9282
  throw new ConfigError(`[${section}].${key} is not a valid option`);
9140
9283
  }
9141
9284
  }
9142
- const result = {
9143
- ...validateBaseConfig(rawObj, section),
9144
- ...validateLoggingConfig(rawObj, section)
9145
- };
9146
- if ("max-tokens" in rawObj) {
9147
- result["max-tokens"] = validateNumber(rawObj["max-tokens"], "max-tokens", section, {
9148
- integer: true,
9149
- min: 1
9150
- });
9285
+ const result = {};
9286
+ if ("enabled" in rawObj) {
9287
+ result.enabled = validateBoolean2(rawObj.enabled, "enabled", section);
9151
9288
  }
9152
- if ("quiet" in rawObj) {
9153
- result.quiet = validateBoolean(rawObj.quiet, "quiet", section);
9289
+ if ("dockerfile" in rawObj) {
9290
+ result.dockerfile = validateString2(rawObj.dockerfile, "dockerfile", section);
9154
9291
  }
9155
- if ("log-llm-requests" in rawObj) {
9156
- result["log-llm-requests"] = validateStringOrBoolean(
9157
- rawObj["log-llm-requests"],
9158
- "log-llm-requests",
9292
+ if ("cwd-permission" in rawObj) {
9293
+ result["cwd-permission"] = validateMountPermission(
9294
+ rawObj["cwd-permission"],
9295
+ "cwd-permission",
9159
9296
  section
9160
9297
  );
9161
9298
  }
9162
- if ("log-llm-responses" in rawObj) {
9163
- result["log-llm-responses"] = validateStringOrBoolean(
9164
- rawObj["log-llm-responses"],
9165
- "log-llm-responses",
9299
+ if ("config-permission" in rawObj) {
9300
+ result["config-permission"] = validateMountPermission(
9301
+ rawObj["config-permission"],
9302
+ "config-permission",
9166
9303
  section
9167
9304
  );
9168
9305
  }
9169
- return result;
9170
- }
9171
- function validateAgentConfig(raw, section) {
9172
- if (typeof raw !== "object" || raw === null) {
9173
- throw new ConfigError(`[${section}] must be a table`);
9174
- }
9175
- const rawObj = raw;
9176
- for (const key of Object.keys(rawObj)) {
9177
- if (!AGENT_CONFIG_KEYS.has(key)) {
9178
- throw new ConfigError(`[${section}].${key} is not a valid option`);
9179
- }
9180
- }
9181
- const result = {
9182
- ...validateBaseConfig(rawObj, section),
9183
- ...validateLoggingConfig(rawObj, section)
9184
- };
9185
- if ("max-iterations" in rawObj) {
9186
- result["max-iterations"] = validateNumber(rawObj["max-iterations"], "max-iterations", section, {
9187
- integer: true,
9188
- min: 1
9189
- });
9190
- }
9191
- if ("gadgets" in rawObj) {
9192
- result.gadgets = validateStringArray(rawObj.gadgets, "gadgets", section);
9193
- }
9194
- if ("gadget-add" in rawObj) {
9195
- result["gadget-add"] = validateStringArray(rawObj["gadget-add"], "gadget-add", section);
9306
+ if ("mounts" in rawObj) {
9307
+ result.mounts = validateMountsArray(rawObj.mounts, section);
9196
9308
  }
9197
- if ("gadget-remove" in rawObj) {
9198
- result["gadget-remove"] = validateStringArray(rawObj["gadget-remove"], "gadget-remove", section);
9309
+ if ("env-vars" in rawObj) {
9310
+ result["env-vars"] = validateStringArray2(rawObj["env-vars"], "env-vars", section);
9199
9311
  }
9200
- if ("gadget" in rawObj) {
9201
- result.gadget = validateStringArray(rawObj.gadget, "gadget", section);
9312
+ if ("image-name" in rawObj) {
9313
+ result["image-name"] = validateString2(rawObj["image-name"], "image-name", section);
9202
9314
  }
9203
- if ("builtins" in rawObj) {
9204
- result.builtins = validateBoolean(rawObj.builtins, "builtins", section);
9315
+ if ("dev-mode" in rawObj) {
9316
+ result["dev-mode"] = validateBoolean2(rawObj["dev-mode"], "dev-mode", section);
9205
9317
  }
9206
- if ("builtin-interaction" in rawObj) {
9207
- result["builtin-interaction"] = validateBoolean(
9208
- rawObj["builtin-interaction"],
9209
- "builtin-interaction",
9210
- section
9211
- );
9318
+ if ("dev-source" in rawObj) {
9319
+ result["dev-source"] = validateString2(rawObj["dev-source"], "dev-source", section);
9212
9320
  }
9213
- if ("gadget-start-prefix" in rawObj) {
9214
- result["gadget-start-prefix"] = validateString(
9215
- rawObj["gadget-start-prefix"],
9216
- "gadget-start-prefix",
9217
- section
9218
- );
9321
+ return result;
9322
+ }
9323
+
9324
+ // src/cli/docker/dockerfile.ts
9325
+ var DEFAULT_DOCKERFILE = `# llmist sandbox image
9326
+ # Auto-generated - customize via [docker].dockerfile in cli.toml
9327
+
9328
+ FROM oven/bun:1-debian
9329
+
9330
+ # Install essential tools
9331
+ RUN apt-get update && apt-get install -y --no-install-recommends \\
9332
+ # ripgrep for fast file searching
9333
+ ripgrep \\
9334
+ # git for version control operations
9335
+ git \\
9336
+ # curl for downloads and API calls
9337
+ curl \\
9338
+ # ca-certificates for HTTPS
9339
+ ca-certificates \\
9340
+ && rm -rf /var/lib/apt/lists/*
9341
+
9342
+ # Install ast-grep for code search/refactoring
9343
+ # Using the official install script
9344
+ RUN curl -fsSL https://raw.githubusercontent.com/ast-grep/ast-grep/main/install.sh | bash \\
9345
+ && mv /root/.local/bin/ast-grep /usr/local/bin/ 2>/dev/null || true \\
9346
+ && mv /root/.local/bin/sg /usr/local/bin/ 2>/dev/null || true
9347
+
9348
+ # Install llmist globally via bun
9349
+ RUN bun add -g llmist
9350
+
9351
+ # Working directory (host CWD will be mounted here)
9352
+ WORKDIR /workspace
9353
+
9354
+ # Entry point - llmist with all arguments forwarded
9355
+ ENTRYPOINT ["llmist"]
9356
+ `;
9357
+ var DEV_DOCKERFILE = `# llmist DEV sandbox image
9358
+ # For development/testing with local source code
9359
+
9360
+ FROM oven/bun:1-debian
9361
+
9362
+ # Install essential tools (same as production)
9363
+ RUN apt-get update && apt-get install -y --no-install-recommends \\
9364
+ ripgrep \\
9365
+ git \\
9366
+ curl \\
9367
+ ca-certificates \\
9368
+ && rm -rf /var/lib/apt/lists/*
9369
+
9370
+ # Install ast-grep for code search/refactoring
9371
+ RUN curl -fsSL https://raw.githubusercontent.com/ast-grep/ast-grep/main/install.sh | bash \\
9372
+ && mv /root/.local/bin/ast-grep /usr/local/bin/ 2>/dev/null || true \\
9373
+ && mv /root/.local/bin/sg /usr/local/bin/ 2>/dev/null || true
9374
+
9375
+ # Working directory (host CWD will be mounted here)
9376
+ WORKDIR /workspace
9377
+
9378
+ # Entry point - run llmist from mounted source
9379
+ # Source is mounted at ${DEV_SOURCE_MOUNT_TARGET}
9380
+ ENTRYPOINT ["bun", "run", "${DEV_SOURCE_MOUNT_TARGET}/src/cli.ts"]
9381
+ `;
9382
+ function resolveDockerfile(config, devMode = false) {
9383
+ if (config.dockerfile) {
9384
+ return config.dockerfile;
9219
9385
  }
9220
- if ("gadget-end-prefix" in rawObj) {
9221
- result["gadget-end-prefix"] = validateString(
9222
- rawObj["gadget-end-prefix"],
9223
- "gadget-end-prefix",
9224
- section
9225
- );
9386
+ return devMode ? DEV_DOCKERFILE : DEFAULT_DOCKERFILE;
9387
+ }
9388
+ function computeDockerfileHash(dockerfile) {
9389
+ const encoder = new TextEncoder();
9390
+ const data = encoder.encode(dockerfile);
9391
+ return Bun.hash(data).toString(16);
9392
+ }
9393
+
9394
+ // src/cli/docker/image-manager.ts
9395
+ var import_node_fs9 = require("fs");
9396
+ var import_node_os3 = require("os");
9397
+ var import_node_path9 = require("path");
9398
+ var CACHE_DIR = (0, import_node_path9.join)((0, import_node_os3.homedir)(), ".llmist", "docker-cache");
9399
+ var HASH_FILE = "image-hash.json";
9400
+ function ensureCacheDir() {
9401
+ if (!(0, import_node_fs9.existsSync)(CACHE_DIR)) {
9402
+ (0, import_node_fs9.mkdirSync)(CACHE_DIR, { recursive: true });
9226
9403
  }
9227
- if ("gadget-arg-prefix" in rawObj) {
9228
- result["gadget-arg-prefix"] = validateString(
9229
- rawObj["gadget-arg-prefix"],
9230
- "gadget-arg-prefix",
9231
- section
9232
- );
9404
+ }
9405
+ function getCachedHash(imageName) {
9406
+ const hashPath = (0, import_node_path9.join)(CACHE_DIR, HASH_FILE);
9407
+ if (!(0, import_node_fs9.existsSync)(hashPath)) {
9408
+ return void 0;
9233
9409
  }
9234
- if ("gadget-approval" in rawObj) {
9235
- result["gadget-approval"] = validateGadgetApproval(rawObj["gadget-approval"], section);
9410
+ try {
9411
+ const content = (0, import_node_fs9.readFileSync)(hashPath, "utf-8");
9412
+ const cache = JSON.parse(content);
9413
+ return cache[imageName]?.dockerfileHash;
9414
+ } catch {
9415
+ return void 0;
9236
9416
  }
9237
- if ("quiet" in rawObj) {
9238
- result.quiet = validateBoolean(rawObj.quiet, "quiet", section);
9417
+ }
9418
+ function setCachedHash(imageName, hash) {
9419
+ ensureCacheDir();
9420
+ const hashPath = (0, import_node_path9.join)(CACHE_DIR, HASH_FILE);
9421
+ let cache = {};
9422
+ if ((0, import_node_fs9.existsSync)(hashPath)) {
9423
+ try {
9424
+ const content = (0, import_node_fs9.readFileSync)(hashPath, "utf-8");
9425
+ cache = JSON.parse(content);
9426
+ } catch {
9427
+ cache = {};
9428
+ }
9239
9429
  }
9240
- if ("log-llm-requests" in rawObj) {
9241
- result["log-llm-requests"] = validateStringOrBoolean(
9242
- rawObj["log-llm-requests"],
9243
- "log-llm-requests",
9244
- section
9245
- );
9430
+ cache[imageName] = {
9431
+ imageName,
9432
+ dockerfileHash: hash,
9433
+ builtAt: (/* @__PURE__ */ new Date()).toISOString()
9434
+ };
9435
+ (0, import_node_fs9.writeFileSync)(hashPath, JSON.stringify(cache, null, 2));
9436
+ }
9437
+ var DockerBuildError = class extends Error {
9438
+ constructor(message, output) {
9439
+ super(message);
9440
+ this.output = output;
9441
+ this.name = "DockerBuildError";
9246
9442
  }
9247
- if ("log-llm-responses" in rawObj) {
9248
- result["log-llm-responses"] = validateStringOrBoolean(
9249
- rawObj["log-llm-responses"],
9250
- "log-llm-responses",
9251
- section
9443
+ };
9444
+ async function buildImage(imageName, dockerfile) {
9445
+ ensureCacheDir();
9446
+ const dockerfilePath = (0, import_node_path9.join)(CACHE_DIR, "Dockerfile");
9447
+ (0, import_node_fs9.writeFileSync)(dockerfilePath, dockerfile);
9448
+ const proc = Bun.spawn(
9449
+ ["docker", "build", "-t", imageName, "-f", dockerfilePath, CACHE_DIR],
9450
+ {
9451
+ stdout: "pipe",
9452
+ stderr: "pipe"
9453
+ }
9454
+ );
9455
+ const exitCode = await proc.exited;
9456
+ const stdout = await new Response(proc.stdout).text();
9457
+ const stderr = await new Response(proc.stderr).text();
9458
+ if (exitCode !== 0) {
9459
+ const output = [stdout, stderr].filter(Boolean).join("\n");
9460
+ throw new DockerBuildError(
9461
+ `Docker build failed with exit code ${exitCode}`,
9462
+ output
9252
9463
  );
9253
9464
  }
9254
- return result;
9255
9465
  }
9256
- function validateStringOrBoolean(value, field, section) {
9257
- if (typeof value === "string" || typeof value === "boolean") {
9258
- return value;
9466
+ async function ensureImage(imageName = DEFAULT_IMAGE_NAME, dockerfile) {
9467
+ const hash = computeDockerfileHash(dockerfile);
9468
+ const cachedHash = getCachedHash(imageName);
9469
+ if (cachedHash === hash) {
9470
+ return imageName;
9259
9471
  }
9260
- throw new ConfigError(`[${section}].${field} must be a string or boolean`);
9472
+ console.error(`Building Docker image '${imageName}'...`);
9473
+ await buildImage(imageName, dockerfile);
9474
+ setCachedHash(imageName, hash);
9475
+ console.error(`Docker image '${imageName}' built successfully.`);
9476
+ return imageName;
9261
9477
  }
9262
- function validateCustomConfig(raw, section) {
9263
- if (typeof raw !== "object" || raw === null) {
9264
- throw new ConfigError(`[${section}] must be a table`);
9478
+
9479
+ // src/cli/docker/docker-wrapper.ts
9480
+ var import_node_fs10 = require("fs");
9481
+ var import_node_path10 = require("path");
9482
+ var import_node_os4 = require("os");
9483
+ var DockerUnavailableError = class extends Error {
9484
+ constructor() {
9485
+ super(
9486
+ "Docker is required but not available. Install Docker or disable Docker sandboxing in your configuration."
9487
+ );
9488
+ this.name = "DockerUnavailableError";
9265
9489
  }
9266
- const rawObj = raw;
9267
- for (const key of Object.keys(rawObj)) {
9268
- if (!CUSTOM_CONFIG_KEYS.has(key)) {
9269
- throw new ConfigError(`[${section}].${key} is not a valid option`);
9270
- }
9490
+ };
9491
+ var DockerSkipError = class extends Error {
9492
+ constructor() {
9493
+ super("Docker execution skipped - already inside container");
9494
+ this.name = "DockerSkipError";
9271
9495
  }
9272
- let type = "agent";
9273
- if ("type" in rawObj) {
9274
- const typeValue = validateString(rawObj.type, "type", section);
9275
- if (typeValue !== "agent" && typeValue !== "complete") {
9276
- throw new ConfigError(`[${section}].type must be "agent" or "complete"`);
9496
+ };
9497
+ async function checkDockerAvailable() {
9498
+ try {
9499
+ const proc = Bun.spawn(["docker", "info"], {
9500
+ stdout: "pipe",
9501
+ stderr: "pipe"
9502
+ });
9503
+ await proc.exited;
9504
+ return proc.exitCode === 0;
9505
+ } catch {
9506
+ return false;
9507
+ }
9508
+ }
9509
+ function isInsideContainer() {
9510
+ if ((0, import_node_fs10.existsSync)("/.dockerenv")) {
9511
+ return true;
9512
+ }
9513
+ try {
9514
+ const cgroup = (0, import_node_fs10.readFileSync)("/proc/1/cgroup", "utf-8");
9515
+ if (cgroup.includes("docker") || cgroup.includes("containerd")) {
9516
+ return true;
9277
9517
  }
9278
- type = typeValue;
9518
+ } catch {
9279
9519
  }
9280
- const result = {
9281
- ...validateBaseConfig(rawObj, section),
9282
- type
9283
- };
9284
- if ("description" in rawObj) {
9285
- result.description = validateString(rawObj.description, "description", section);
9520
+ return false;
9521
+ }
9522
+ function autoDetectDevSource() {
9523
+ const scriptPath = process.argv[1];
9524
+ if (!scriptPath || !scriptPath.endsWith("src/cli.ts")) {
9525
+ return void 0;
9286
9526
  }
9287
- if ("max-iterations" in rawObj) {
9288
- result["max-iterations"] = validateNumber(rawObj["max-iterations"], "max-iterations", section, {
9289
- integer: true,
9290
- min: 1
9291
- });
9527
+ const srcDir = (0, import_node_path10.dirname)(scriptPath);
9528
+ const projectDir = (0, import_node_path10.dirname)(srcDir);
9529
+ const packageJsonPath = (0, import_node_path10.join)(projectDir, "package.json");
9530
+ if (!(0, import_node_fs10.existsSync)(packageJsonPath)) {
9531
+ return void 0;
9292
9532
  }
9293
- if ("gadgets" in rawObj) {
9294
- result.gadgets = validateStringArray(rawObj.gadgets, "gadgets", section);
9533
+ try {
9534
+ const pkg = JSON.parse((0, import_node_fs10.readFileSync)(packageJsonPath, "utf-8"));
9535
+ if (pkg.name === "llmist") {
9536
+ return projectDir;
9537
+ }
9538
+ } catch {
9295
9539
  }
9296
- if ("gadget-add" in rawObj) {
9297
- result["gadget-add"] = validateStringArray(rawObj["gadget-add"], "gadget-add", section);
9540
+ return void 0;
9541
+ }
9542
+ function resolveDevMode(config, cliDevMode) {
9543
+ const enabled = cliDevMode || config?.["dev-mode"] || process.env.LLMIST_DEV_MODE === "1";
9544
+ if (!enabled) {
9545
+ return { enabled: false, sourcePath: void 0 };
9298
9546
  }
9299
- if ("gadget-remove" in rawObj) {
9300
- result["gadget-remove"] = validateStringArray(rawObj["gadget-remove"], "gadget-remove", section);
9547
+ const sourcePath = config?.["dev-source"] || process.env.LLMIST_DEV_SOURCE || autoDetectDevSource();
9548
+ if (!sourcePath) {
9549
+ throw new Error(
9550
+ "Docker dev mode enabled but llmist source path not found. Set [docker].dev-source in config, LLMIST_DEV_SOURCE env var, or run from the llmist source directory (bun src/cli.ts)."
9551
+ );
9301
9552
  }
9302
- if ("gadget" in rawObj) {
9303
- result.gadget = validateStringArray(rawObj.gadget, "gadget", section);
9553
+ return { enabled: true, sourcePath };
9554
+ }
9555
+ function expandHome(path5) {
9556
+ if (path5.startsWith("~")) {
9557
+ return path5.replace(/^~/, (0, import_node_os4.homedir)());
9304
9558
  }
9305
- if ("builtins" in rawObj) {
9306
- result.builtins = validateBoolean(rawObj.builtins, "builtins", section);
9559
+ return path5;
9560
+ }
9561
+ function buildDockerRunArgs(ctx, imageName, devMode) {
9562
+ const args = ["run", "--rm"];
9563
+ const timestamp = Date.now();
9564
+ const random = Math.random().toString(36).slice(2, 8);
9565
+ const containerName = `llmist-${timestamp}-${random}`;
9566
+ args.push("--name", containerName);
9567
+ if (process.stdin.isTTY) {
9568
+ args.push("-it");
9307
9569
  }
9308
- if ("builtin-interaction" in rawObj) {
9309
- result["builtin-interaction"] = validateBoolean(
9310
- rawObj["builtin-interaction"],
9311
- "builtin-interaction",
9312
- section
9313
- );
9570
+ const cwdPermission = ctx.options.dockerRo ? "ro" : ctx.profileCwdPermission ?? ctx.config["cwd-permission"] ?? DEFAULT_CWD_PERMISSION;
9571
+ args.push("-v", `${ctx.cwd}:/workspace:${cwdPermission}`);
9572
+ args.push("-w", "/workspace");
9573
+ const configPermission = ctx.config["config-permission"] ?? DEFAULT_CONFIG_PERMISSION;
9574
+ const llmistDir = expandHome("~/.llmist");
9575
+ args.push("-v", `${llmistDir}:/root/.llmist:${configPermission}`);
9576
+ if (devMode.enabled && devMode.sourcePath) {
9577
+ const expandedSource = expandHome(devMode.sourcePath);
9578
+ args.push("-v", `${expandedSource}:${DEV_SOURCE_MOUNT_TARGET}:ro`);
9314
9579
  }
9315
- if ("gadget-start-prefix" in rawObj) {
9316
- result["gadget-start-prefix"] = validateString(
9317
- rawObj["gadget-start-prefix"],
9318
- "gadget-start-prefix",
9319
- section
9320
- );
9580
+ if (ctx.config.mounts) {
9581
+ for (const mount of ctx.config.mounts) {
9582
+ const source = expandHome(mount.source);
9583
+ args.push("-v", `${source}:${mount.target}:${mount.permission}`);
9584
+ }
9321
9585
  }
9322
- if ("gadget-end-prefix" in rawObj) {
9323
- result["gadget-end-prefix"] = validateString(
9324
- rawObj["gadget-end-prefix"],
9325
- "gadget-end-prefix",
9326
- section
9327
- );
9586
+ for (const key of FORWARDED_API_KEYS) {
9587
+ if (process.env[key]) {
9588
+ args.push("-e", key);
9589
+ }
9328
9590
  }
9329
- if ("gadget-arg-prefix" in rawObj) {
9330
- result["gadget-arg-prefix"] = validateString(
9331
- rawObj["gadget-arg-prefix"],
9332
- "gadget-arg-prefix",
9333
- section
9334
- );
9591
+ if (ctx.config["env-vars"]) {
9592
+ for (const key of ctx.config["env-vars"]) {
9593
+ if (process.env[key]) {
9594
+ args.push("-e", key);
9595
+ }
9596
+ }
9335
9597
  }
9336
- if ("gadget-approval" in rawObj) {
9337
- result["gadget-approval"] = validateGadgetApproval(rawObj["gadget-approval"], section);
9598
+ args.push(imageName);
9599
+ args.push(...ctx.forwardArgs);
9600
+ return args;
9601
+ }
9602
+ function filterDockerArgs(argv) {
9603
+ const dockerFlags = /* @__PURE__ */ new Set(["--docker", "--docker-ro", "--no-docker", "--docker-dev"]);
9604
+ return argv.filter((arg) => !dockerFlags.has(arg));
9605
+ }
9606
+ function resolveDockerEnabled(config, options, profileDocker) {
9607
+ if (options.noDocker) {
9608
+ return false;
9338
9609
  }
9339
- if ("max-tokens" in rawObj) {
9340
- result["max-tokens"] = validateNumber(rawObj["max-tokens"], "max-tokens", section, {
9341
- integer: true,
9342
- min: 1
9343
- });
9610
+ if (options.docker || options.dockerRo) {
9611
+ return true;
9344
9612
  }
9345
- if ("quiet" in rawObj) {
9346
- result.quiet = validateBoolean(rawObj.quiet, "quiet", section);
9613
+ if (profileDocker !== void 0) {
9614
+ return profileDocker;
9347
9615
  }
9348
- Object.assign(result, validateLoggingConfig(rawObj, section));
9349
- return result;
9616
+ return config?.enabled ?? false;
9350
9617
  }
9351
- function validatePromptsConfig(raw, section) {
9352
- if (typeof raw !== "object" || raw === null) {
9353
- throw new ConfigError(`[${section}] must be a table`);
9618
+ async function executeInDocker(ctx, devMode) {
9619
+ if (isInsideContainer()) {
9620
+ console.error(
9621
+ "Warning: Docker mode requested but already inside a container. Proceeding without re-containerization."
9622
+ );
9623
+ throw new DockerSkipError();
9354
9624
  }
9355
- const result = {};
9356
- for (const [key, value] of Object.entries(raw)) {
9357
- if (typeof value !== "string") {
9358
- throw new ConfigError(`[${section}].${key} must be a string`);
9625
+ const available = await checkDockerAvailable();
9626
+ if (!available) {
9627
+ throw new DockerUnavailableError();
9628
+ }
9629
+ const dockerfile = resolveDockerfile(ctx.config, devMode.enabled);
9630
+ const imageName = devMode.enabled ? DEV_IMAGE_NAME : ctx.config["image-name"] ?? DEFAULT_IMAGE_NAME;
9631
+ if (devMode.enabled) {
9632
+ console.error(`[dev mode] Mounting source from ${devMode.sourcePath}`);
9633
+ }
9634
+ try {
9635
+ await ensureImage(imageName, dockerfile);
9636
+ } catch (error) {
9637
+ if (error instanceof DockerBuildError) {
9638
+ console.error("Docker build failed:");
9639
+ console.error(error.output);
9640
+ throw error;
9359
9641
  }
9360
- result[key] = value;
9642
+ throw error;
9361
9643
  }
9362
- return result;
9644
+ const dockerArgs = buildDockerRunArgs(ctx, imageName, devMode);
9645
+ const proc = Bun.spawn(["docker", ...dockerArgs], {
9646
+ stdin: "inherit",
9647
+ stdout: "inherit",
9648
+ stderr: "inherit"
9649
+ });
9650
+ const exitCode = await proc.exited;
9651
+ process.exit(exitCode);
9363
9652
  }
9364
- function validateConfig(raw, configPath) {
9365
- if (typeof raw !== "object" || raw === null) {
9366
- throw new ConfigError("Config must be a TOML table", configPath);
9653
+ function createDockerContext(config, options, argv, cwd, profileCwdPermission) {
9654
+ return {
9655
+ config: config ?? {},
9656
+ options,
9657
+ forwardArgs: filterDockerArgs(argv),
9658
+ cwd,
9659
+ profileCwdPermission
9660
+ };
9661
+ }
9662
+
9663
+ // src/cli/agent-command.ts
9664
+ function createHumanInputHandler(env, progress, keyboard) {
9665
+ const stdout = env.stdout;
9666
+ if (!isInteractive(env.stdin) || typeof stdout.isTTY !== "boolean" || !stdout.isTTY) {
9667
+ return void 0;
9367
9668
  }
9368
- const rawObj = raw;
9369
- const result = {};
9370
- for (const [key, value] of Object.entries(rawObj)) {
9669
+ return async (question) => {
9670
+ progress.pause();
9671
+ if (keyboard.cleanupEsc) {
9672
+ keyboard.cleanupEsc();
9673
+ keyboard.cleanupEsc = null;
9674
+ }
9675
+ const rl = (0, import_promises3.createInterface)({ input: env.stdin, output: env.stdout });
9371
9676
  try {
9372
- if (key === "global") {
9373
- result.global = validateGlobalConfig(value, key);
9374
- } else if (key === "complete") {
9375
- result.complete = validateCompleteConfig(value, key);
9376
- } else if (key === "agent") {
9377
- result.agent = validateAgentConfig(value, key);
9378
- } else if (key === "prompts") {
9379
- result.prompts = validatePromptsConfig(value, key);
9380
- } else {
9381
- result[key] = validateCustomConfig(value, key);
9677
+ const questionLine = question.trim() ? `
9678
+ ${renderMarkdownWithSeparators(question.trim())}` : "";
9679
+ let isFirst = true;
9680
+ while (true) {
9681
+ const statsPrompt = progress.formatPrompt();
9682
+ const prompt = isFirst ? `${questionLine}
9683
+ ${statsPrompt}` : statsPrompt;
9684
+ isFirst = false;
9685
+ const answer = await rl.question(prompt);
9686
+ const trimmed = answer.trim();
9687
+ if (trimmed) {
9688
+ return trimmed;
9689
+ }
9382
9690
  }
9691
+ } finally {
9692
+ rl.close();
9693
+ keyboard.restore();
9694
+ }
9695
+ };
9696
+ }
9697
+ async function executeAgent(promptArg, options, env) {
9698
+ const dockerOptions = {
9699
+ docker: options.docker ?? false,
9700
+ dockerRo: options.dockerRo ?? false,
9701
+ noDocker: options.noDocker ?? false,
9702
+ dockerDev: options.dockerDev ?? false
9703
+ };
9704
+ const dockerEnabled = resolveDockerEnabled(
9705
+ env.dockerConfig,
9706
+ dockerOptions,
9707
+ options.docker
9708
+ // Profile-level docker: true/false
9709
+ );
9710
+ if (dockerEnabled) {
9711
+ const devMode = resolveDevMode(env.dockerConfig, dockerOptions.dockerDev);
9712
+ const ctx = createDockerContext(
9713
+ env.dockerConfig,
9714
+ dockerOptions,
9715
+ env.argv.slice(2),
9716
+ // Remove 'node' and script path
9717
+ process.cwd(),
9718
+ options.dockerCwdPermission
9719
+ // Profile-level CWD permission override
9720
+ );
9721
+ try {
9722
+ await executeInDocker(ctx, devMode);
9383
9723
  } catch (error) {
9384
- if (error instanceof ConfigError) {
9385
- throw new ConfigError(error.message, configPath);
9724
+ if (error instanceof Error && error.message === "SKIP_DOCKER") {
9725
+ } else {
9726
+ throw error;
9386
9727
  }
9387
- throw error;
9388
9728
  }
9389
9729
  }
9390
- return result;
9391
- }
9392
- function loadConfig() {
9393
- const configPath = getConfigPath();
9394
- if (!(0, import_node_fs8.existsSync)(configPath)) {
9395
- return {};
9396
- }
9397
- let content;
9398
- try {
9399
- content = (0, import_node_fs8.readFileSync)(configPath, "utf-8");
9400
- } catch (error) {
9401
- throw new ConfigError(
9402
- `Failed to read config file: ${error instanceof Error ? error.message : "Unknown error"}`,
9403
- configPath
9404
- );
9730
+ const prompt = await resolvePrompt(promptArg, env);
9731
+ const client = env.createClient();
9732
+ const registry = new GadgetRegistry();
9733
+ const stdinIsInteractive = isInteractive(env.stdin);
9734
+ if (options.builtins !== false) {
9735
+ for (const gadget of builtinGadgets) {
9736
+ if (gadget.name === "AskUser" && (options.builtinInteraction === false || !stdinIsInteractive)) {
9737
+ continue;
9738
+ }
9739
+ registry.registerByClass(gadget);
9740
+ }
9405
9741
  }
9406
- let raw;
9407
- try {
9408
- raw = (0, import_js_toml.load)(content);
9409
- } catch (error) {
9410
- throw new ConfigError(
9411
- `Invalid TOML syntax: ${error instanceof Error ? error.message : "Unknown error"}`,
9412
- configPath
9413
- );
9742
+ const gadgetSpecifiers = options.gadget ?? [];
9743
+ if (gadgetSpecifiers.length > 0) {
9744
+ const gadgets2 = await loadGadgets(gadgetSpecifiers, process.cwd());
9745
+ for (const gadget of gadgets2) {
9746
+ registry.registerByClass(gadget);
9747
+ }
9414
9748
  }
9415
- const validated = validateConfig(raw, configPath);
9416
- const inherited = resolveInheritance(validated, configPath);
9417
- return resolveTemplatesInConfig(inherited, configPath);
9418
- }
9419
- function getCustomCommandNames(config) {
9420
- const reserved = /* @__PURE__ */ new Set(["global", "complete", "agent", "prompts"]);
9421
- return Object.keys(config).filter((key) => !reserved.has(key));
9422
- }
9423
- function resolveTemplatesInConfig(config, configPath) {
9424
- const prompts = config.prompts ?? {};
9425
- const hasPrompts = Object.keys(prompts).length > 0;
9426
- let hasTemplates = false;
9427
- for (const [sectionName, section] of Object.entries(config)) {
9428
- if (sectionName === "global" || sectionName === "prompts") continue;
9429
- if (!section || typeof section !== "object") continue;
9430
- const sectionObj = section;
9431
- if (typeof sectionObj.system === "string" && hasTemplateSyntax(sectionObj.system)) {
9432
- hasTemplates = true;
9433
- break;
9749
+ const printer = new StreamPrinter(env.stdout);
9750
+ const stderrTTY = env.stderr.isTTY === true;
9751
+ const progress = new StreamProgress(env.stderr, stderrTTY, client.modelRegistry);
9752
+ const abortController = new AbortController();
9753
+ let wasCancelled = false;
9754
+ let isStreaming = false;
9755
+ const stdinStream = env.stdin;
9756
+ const handleCancel = () => {
9757
+ if (!abortController.signal.aborted) {
9758
+ wasCancelled = true;
9759
+ abortController.abort();
9760
+ progress.pause();
9761
+ env.stderr.write(import_chalk5.default.yellow(`
9762
+ [Cancelled] ${progress.formatStats()}
9763
+ `));
9434
9764
  }
9435
- }
9436
- for (const template of Object.values(prompts)) {
9437
- if (hasTemplateSyntax(template)) {
9438
- hasTemplates = true;
9439
- break;
9765
+ };
9766
+ const keyboard = {
9767
+ cleanupEsc: null,
9768
+ cleanupSigint: null,
9769
+ restore: () => {
9770
+ if (stdinIsInteractive && stdinStream.isTTY && !wasCancelled) {
9771
+ keyboard.cleanupEsc = createEscKeyListener(stdinStream, handleCancel);
9772
+ }
9440
9773
  }
9774
+ };
9775
+ const handleQuit = () => {
9776
+ keyboard.cleanupEsc?.();
9777
+ keyboard.cleanupSigint?.();
9778
+ progress.complete();
9779
+ printer.ensureNewline();
9780
+ const summary = renderOverallSummary({
9781
+ totalTokens: usage?.totalTokens,
9782
+ iterations,
9783
+ elapsedSeconds: progress.getTotalElapsedSeconds(),
9784
+ cost: progress.getTotalCost()
9785
+ });
9786
+ if (summary) {
9787
+ env.stderr.write(`${import_chalk5.default.dim("\u2500".repeat(40))}
9788
+ `);
9789
+ env.stderr.write(`${summary}
9790
+ `);
9791
+ }
9792
+ env.stderr.write(import_chalk5.default.dim("[Quit]\n"));
9793
+ process.exit(130);
9794
+ };
9795
+ if (stdinIsInteractive && stdinStream.isTTY) {
9796
+ keyboard.cleanupEsc = createEscKeyListener(stdinStream, handleCancel);
9441
9797
  }
9442
- if (!hasPrompts && !hasTemplates) {
9443
- return config;
9444
- }
9445
- try {
9446
- validatePrompts(prompts, configPath);
9447
- } catch (error) {
9448
- if (error instanceof TemplateError) {
9449
- throw new ConfigError(error.message, configPath);
9798
+ keyboard.cleanupSigint = createSigintListener(
9799
+ handleCancel,
9800
+ handleQuit,
9801
+ () => isStreaming && !abortController.signal.aborted,
9802
+ env.stderr
9803
+ );
9804
+ const DEFAULT_APPROVAL_REQUIRED = ["RunCommand", "WriteFile", "EditFile"];
9805
+ const userApprovals = options.gadgetApproval ?? {};
9806
+ const gadgetApprovals = {
9807
+ ...userApprovals
9808
+ };
9809
+ for (const gadget of DEFAULT_APPROVAL_REQUIRED) {
9810
+ const normalizedGadget = gadget.toLowerCase();
9811
+ const isConfigured = Object.keys(userApprovals).some(
9812
+ (key) => key.toLowerCase() === normalizedGadget
9813
+ );
9814
+ if (!isConfigured) {
9815
+ gadgetApprovals[gadget] = "approval-required";
9450
9816
  }
9451
- throw error;
9452
9817
  }
9453
- for (const [name, template] of Object.entries(prompts)) {
9818
+ const approvalConfig = {
9819
+ gadgetApprovals,
9820
+ defaultMode: "allowed"
9821
+ };
9822
+ const approvalManager = new ApprovalManager(approvalConfig, env, progress);
9823
+ let usage;
9824
+ let iterations = 0;
9825
+ const llmRequestsDir = resolveLogDir(options.logLlmRequests, "requests");
9826
+ const llmResponsesDir = resolveLogDir(options.logLlmResponses, "responses");
9827
+ let llmCallCounter = 0;
9828
+ const countMessagesTokens = async (model, messages) => {
9454
9829
  try {
9455
- validateEnvVars(template, name, configPath);
9456
- } catch (error) {
9457
- if (error instanceof TemplateError) {
9458
- throw new ConfigError(error.message, configPath);
9459
- }
9460
- throw error;
9830
+ return await client.countTokens(model, messages);
9831
+ } catch {
9832
+ const totalChars = messages.reduce((sum, m) => sum + (m.content?.length ?? 0), 0);
9833
+ return Math.round(totalChars / FALLBACK_CHARS_PER_TOKEN);
9461
9834
  }
9462
- }
9463
- const eta = createTemplateEngine(prompts, configPath);
9464
- const result = { ...config };
9465
- for (const [sectionName, section] of Object.entries(config)) {
9466
- if (sectionName === "global" || sectionName === "prompts") continue;
9467
- if (!section || typeof section !== "object") continue;
9468
- const sectionObj = section;
9469
- if (typeof sectionObj.system === "string" && hasTemplateSyntax(sectionObj.system)) {
9470
- try {
9471
- validateEnvVars(sectionObj.system, void 0, configPath);
9472
- } catch (error) {
9473
- if (error instanceof TemplateError) {
9474
- throw new ConfigError(`[${sectionName}].system: ${error.message}`, configPath);
9835
+ };
9836
+ const countGadgetOutputTokens = async (output) => {
9837
+ if (!output) return void 0;
9838
+ try {
9839
+ const messages = [{ role: "assistant", content: output }];
9840
+ return await client.countTokens(options.model, messages);
9841
+ } catch {
9842
+ return void 0;
9843
+ }
9844
+ };
9845
+ const builder = new AgentBuilder(client).withModel(options.model).withLogger(env.createLogger("llmist:cli:agent")).withHooks({
9846
+ observers: {
9847
+ // onLLMCallStart: Start progress indicator for each LLM call
9848
+ // This showcases how to react to agent lifecycle events
9849
+ onLLMCallStart: async (context) => {
9850
+ isStreaming = true;
9851
+ llmCallCounter++;
9852
+ const inputTokens = await countMessagesTokens(
9853
+ context.options.model,
9854
+ context.options.messages
9855
+ );
9856
+ progress.startCall(context.options.model, inputTokens);
9857
+ progress.setInputTokens(inputTokens, false);
9858
+ if (llmRequestsDir) {
9859
+ const filename = `${Date.now()}_call_${llmCallCounter}.request.txt`;
9860
+ const content = formatLlmRequest(context.options.messages);
9861
+ await writeLogFile(llmRequestsDir, filename, content);
9862
+ }
9863
+ },
9864
+ // onStreamChunk: Real-time updates as LLM generates tokens
9865
+ // This enables responsive UIs that show progress during generation
9866
+ onStreamChunk: async (context) => {
9867
+ progress.update(context.accumulatedText.length);
9868
+ if (context.usage) {
9869
+ if (context.usage.inputTokens) {
9870
+ progress.setInputTokens(context.usage.inputTokens, false);
9871
+ }
9872
+ if (context.usage.outputTokens) {
9873
+ progress.setOutputTokens(context.usage.outputTokens, false);
9874
+ }
9875
+ progress.setCachedTokens(
9876
+ context.usage.cachedInputTokens ?? 0,
9877
+ context.usage.cacheCreationInputTokens ?? 0
9878
+ );
9879
+ }
9880
+ },
9881
+ // onLLMCallComplete: Finalize metrics after each LLM call
9882
+ // This is where you'd typically log metrics or update dashboards
9883
+ onLLMCallComplete: async (context) => {
9884
+ isStreaming = false;
9885
+ usage = context.usage;
9886
+ iterations = Math.max(iterations, context.iteration + 1);
9887
+ if (context.usage) {
9888
+ if (context.usage.inputTokens) {
9889
+ progress.setInputTokens(context.usage.inputTokens, false);
9890
+ }
9891
+ if (context.usage.outputTokens) {
9892
+ progress.setOutputTokens(context.usage.outputTokens, false);
9893
+ }
9894
+ }
9895
+ let callCost;
9896
+ if (context.usage && client.modelRegistry) {
9897
+ try {
9898
+ const modelName = context.options.model.includes(":") ? context.options.model.split(":")[1] : context.options.model;
9899
+ const costResult = client.modelRegistry.estimateCost(
9900
+ modelName,
9901
+ context.usage.inputTokens,
9902
+ context.usage.outputTokens,
9903
+ context.usage.cachedInputTokens ?? 0,
9904
+ context.usage.cacheCreationInputTokens ?? 0
9905
+ );
9906
+ if (costResult) callCost = costResult.totalCost;
9907
+ } catch {
9908
+ }
9909
+ }
9910
+ const callElapsed = progress.getCallElapsedSeconds();
9911
+ progress.endCall(context.usage);
9912
+ if (!options.quiet) {
9913
+ const summary = renderSummary({
9914
+ iterations: context.iteration + 1,
9915
+ model: options.model,
9916
+ usage: context.usage,
9917
+ elapsedSeconds: callElapsed,
9918
+ cost: callCost,
9919
+ finishReason: context.finishReason
9920
+ });
9921
+ if (summary) {
9922
+ env.stderr.write(`${summary}
9923
+ `);
9924
+ }
9925
+ }
9926
+ if (llmResponsesDir) {
9927
+ const filename = `${Date.now()}_call_${llmCallCounter}.response.txt`;
9928
+ await writeLogFile(llmResponsesDir, filename, context.rawResponse);
9475
9929
  }
9476
- throw error;
9477
9930
  }
9478
- try {
9479
- const resolved = resolveTemplate(eta, sectionObj.system, {}, configPath);
9480
- result[sectionName] = {
9481
- ...sectionObj,
9482
- system: resolved
9483
- };
9484
- } catch (error) {
9485
- if (error instanceof TemplateError) {
9486
- throw new ConfigError(`[${sectionName}].system: ${error.message}`, configPath);
9931
+ },
9932
+ // SHOWCASE: Controller-based approval gating for gadgets
9933
+ //
9934
+ // This demonstrates how to add safety layers WITHOUT modifying gadgets.
9935
+ // The ApprovalManager handles approval flows externally via beforeGadgetExecution.
9936
+ // Approval modes are configurable via cli.toml:
9937
+ // - "allowed": auto-proceed
9938
+ // - "denied": auto-reject, return message to LLM
9939
+ // - "approval-required": prompt user interactively
9940
+ //
9941
+ // Default: RunCommand, WriteFile, EditFile require approval unless overridden.
9942
+ controllers: {
9943
+ beforeGadgetExecution: async (ctx) => {
9944
+ const mode = approvalManager.getApprovalMode(ctx.gadgetName);
9945
+ if (mode === "allowed") {
9946
+ return { action: "proceed" };
9947
+ }
9948
+ const stdinTTY = isInteractive(env.stdin);
9949
+ const stderrTTY2 = env.stderr.isTTY === true;
9950
+ const canPrompt = stdinTTY && stderrTTY2;
9951
+ if (!canPrompt) {
9952
+ if (mode === "approval-required") {
9953
+ return {
9954
+ action: "skip",
9955
+ syntheticResult: `status=denied
9956
+
9957
+ ${ctx.gadgetName} requires interactive approval. Run in a terminal to approve.`
9958
+ };
9959
+ }
9960
+ if (mode === "denied") {
9961
+ return {
9962
+ action: "skip",
9963
+ syntheticResult: `status=denied
9964
+
9965
+ ${ctx.gadgetName} is denied by configuration.`
9966
+ };
9967
+ }
9968
+ return { action: "proceed" };
9969
+ }
9970
+ const result = await approvalManager.requestApproval(ctx.gadgetName, ctx.parameters);
9971
+ if (!result.approved) {
9972
+ return {
9973
+ action: "skip",
9974
+ syntheticResult: `status=denied
9975
+
9976
+ Denied: ${result.reason ?? "by user"}`
9977
+ };
9487
9978
  }
9488
- throw error;
9979
+ return { action: "proceed" };
9489
9980
  }
9490
9981
  }
9982
+ });
9983
+ if (options.system) {
9984
+ builder.withSystem(options.system);
9491
9985
  }
9492
- return result;
9493
- }
9494
- function resolveGadgets(section, inheritedGadgets, sectionName, configPath) {
9495
- const hasGadgets = "gadgets" in section;
9496
- const hasGadgetLegacy = "gadget" in section;
9497
- const hasGadgetAdd = "gadget-add" in section;
9498
- const hasGadgetRemove = "gadget-remove" in section;
9499
- if (hasGadgetLegacy && !hasGadgets) {
9500
- console.warn(
9501
- `[config] Warning: [${sectionName}].gadget is deprecated, use 'gadgets' (plural) instead`
9502
- );
9986
+ if (options.maxIterations !== void 0) {
9987
+ builder.withMaxIterations(options.maxIterations);
9503
9988
  }
9504
- if ((hasGadgets || hasGadgetLegacy) && (hasGadgetAdd || hasGadgetRemove)) {
9505
- throw new ConfigError(
9506
- `[${sectionName}] Cannot use 'gadgets' with 'gadget-add'/'gadget-remove'. Use either full replacement (gadgets) OR modification (gadget-add/gadget-remove).`,
9507
- configPath
9508
- );
9989
+ if (options.temperature !== void 0) {
9990
+ builder.withTemperature(options.temperature);
9509
9991
  }
9510
- if (hasGadgets) {
9511
- return section.gadgets;
9992
+ const humanInputHandler = createHumanInputHandler(env, progress, keyboard);
9993
+ if (humanInputHandler) {
9994
+ builder.onHumanInput(humanInputHandler);
9512
9995
  }
9513
- if (hasGadgetLegacy) {
9514
- return section.gadget;
9996
+ builder.withSignal(abortController.signal);
9997
+ const gadgets = registry.getAll();
9998
+ if (gadgets.length > 0) {
9999
+ builder.withGadgets(...gadgets);
9515
10000
  }
9516
- let result = [...inheritedGadgets];
9517
- if (hasGadgetRemove) {
9518
- const toRemove = new Set(section["gadget-remove"]);
9519
- result = result.filter((g) => !toRemove.has(g));
10001
+ if (options.gadgetStartPrefix) {
10002
+ builder.withGadgetStartPrefix(options.gadgetStartPrefix);
9520
10003
  }
9521
- if (hasGadgetAdd) {
9522
- const toAdd = section["gadget-add"];
9523
- result.push(...toAdd);
10004
+ if (options.gadgetEndPrefix) {
10005
+ builder.withGadgetEndPrefix(options.gadgetEndPrefix);
9524
10006
  }
9525
- return result;
9526
- }
9527
- function resolveInheritance(config, configPath) {
9528
- const resolved = {};
9529
- const resolving = /* @__PURE__ */ new Set();
9530
- function resolveSection(name) {
9531
- if (name in resolved) {
9532
- return resolved[name];
10007
+ if (options.gadgetArgPrefix) {
10008
+ builder.withGadgetArgPrefix(options.gadgetArgPrefix);
10009
+ }
10010
+ builder.withSyntheticGadgetCall(
10011
+ "TellUser",
10012
+ {
10013
+ 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?",
10014
+ done: false,
10015
+ type: "info"
10016
+ },
10017
+ "\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?"
10018
+ );
10019
+ builder.withTextOnlyHandler("acknowledge");
10020
+ builder.withTextWithGadgetsHandler({
10021
+ gadgetName: "TellUser",
10022
+ parameterMapping: (text) => ({ message: text, done: false, type: "info" }),
10023
+ resultMapping: (text) => `\u2139\uFE0F ${text}`
10024
+ });
10025
+ const agent = builder.ask(prompt);
10026
+ let textBuffer = "";
10027
+ const flushTextBuffer = () => {
10028
+ if (textBuffer) {
10029
+ const output = options.quiet ? textBuffer : renderMarkdownWithSeparators(textBuffer);
10030
+ printer.write(output);
10031
+ textBuffer = "";
9533
10032
  }
9534
- if (resolving.has(name)) {
9535
- throw new ConfigError(`Circular inheritance detected: ${name}`, configPath);
10033
+ };
10034
+ try {
10035
+ for await (const event of agent.run()) {
10036
+ if (event.type === "text") {
10037
+ progress.pause();
10038
+ textBuffer += event.content;
10039
+ } else if (event.type === "gadget_result") {
10040
+ flushTextBuffer();
10041
+ progress.pause();
10042
+ if (options.quiet) {
10043
+ if (event.result.gadgetName === "TellUser" && event.result.parameters?.message) {
10044
+ const message = String(event.result.parameters.message);
10045
+ env.stdout.write(`${message}
10046
+ `);
10047
+ }
10048
+ } else {
10049
+ const tokenCount = await countGadgetOutputTokens(event.result.result);
10050
+ env.stderr.write(`${formatGadgetSummary({ ...event.result, tokenCount })}
10051
+ `);
10052
+ }
10053
+ }
9536
10054
  }
9537
- const section = config[name];
9538
- if (section === void 0 || typeof section !== "object") {
9539
- throw new ConfigError(`Cannot inherit from unknown section: ${name}`, configPath);
10055
+ } catch (error) {
10056
+ if (!isAbortError(error)) {
10057
+ throw error;
9540
10058
  }
9541
- resolving.add(name);
9542
- const sectionObj = section;
9543
- const inheritsRaw = sectionObj.inherits;
9544
- const inheritsList = inheritsRaw ? Array.isArray(inheritsRaw) ? inheritsRaw : [inheritsRaw] : [];
9545
- let merged = {};
9546
- for (const parent of inheritsList) {
9547
- const parentResolved = resolveSection(parent);
9548
- merged = { ...merged, ...parentResolved };
10059
+ } finally {
10060
+ isStreaming = false;
10061
+ keyboard.cleanupEsc?.();
10062
+ keyboard.cleanupSigint?.();
10063
+ }
10064
+ flushTextBuffer();
10065
+ progress.complete();
10066
+ printer.ensureNewline();
10067
+ if (!options.quiet && iterations > 1) {
10068
+ env.stderr.write(`${import_chalk5.default.dim("\u2500".repeat(40))}
10069
+ `);
10070
+ const summary = renderOverallSummary({
10071
+ totalTokens: usage?.totalTokens,
10072
+ iterations,
10073
+ elapsedSeconds: progress.getTotalElapsedSeconds(),
10074
+ cost: progress.getTotalCost()
10075
+ });
10076
+ if (summary) {
10077
+ env.stderr.write(`${summary}
10078
+ `);
9549
10079
  }
9550
- const inheritedGadgets = merged.gadgets ?? [];
9551
- const {
9552
- inherits: _inherits,
9553
- gadgets: _gadgets,
9554
- gadget: _gadget,
9555
- "gadget-add": _gadgetAdd,
9556
- "gadget-remove": _gadgetRemove,
9557
- ...ownValues
9558
- } = sectionObj;
9559
- merged = { ...merged, ...ownValues };
9560
- const resolvedGadgets = resolveGadgets(sectionObj, inheritedGadgets, name, configPath);
9561
- if (resolvedGadgets.length > 0) {
9562
- merged.gadgets = resolvedGadgets;
10080
+ }
10081
+ }
10082
+ function registerAgentCommand(program, env, config) {
10083
+ const cmd = program.command(COMMANDS.agent).description("Run the llmist agent loop with optional gadgets.").argument("[prompt]", "Prompt for the agent loop. Falls back to stdin when available.");
10084
+ addAgentOptions(cmd, config);
10085
+ cmd.action(
10086
+ (prompt, options) => executeAction(() => {
10087
+ const mergedOptions = {
10088
+ ...options,
10089
+ gadgetApproval: config?.["gadget-approval"]
10090
+ };
10091
+ return executeAgent(prompt, mergedOptions, env);
10092
+ }, env)
10093
+ );
10094
+ }
10095
+
10096
+ // src/cli/complete-command.ts
10097
+ init_messages();
10098
+ init_model_shortcuts();
10099
+ init_constants2();
10100
+ async function executeComplete(promptArg, options, env) {
10101
+ const prompt = await resolvePrompt(promptArg, env);
10102
+ const client = env.createClient();
10103
+ const model = resolveModel(options.model);
10104
+ const builder = new LLMMessageBuilder();
10105
+ if (options.system) {
10106
+ builder.addSystem(options.system);
10107
+ }
10108
+ builder.addUser(prompt);
10109
+ const messages = builder.build();
10110
+ const llmRequestsDir = resolveLogDir(options.logLlmRequests, "requests");
10111
+ const llmResponsesDir = resolveLogDir(options.logLlmResponses, "responses");
10112
+ const timestamp = Date.now();
10113
+ if (llmRequestsDir) {
10114
+ const filename = `${timestamp}_complete.request.txt`;
10115
+ const content = formatLlmRequest(messages);
10116
+ await writeLogFile(llmRequestsDir, filename, content);
10117
+ }
10118
+ const stream2 = client.stream({
10119
+ model,
10120
+ messages,
10121
+ temperature: options.temperature,
10122
+ maxTokens: options.maxTokens
10123
+ });
10124
+ const printer = new StreamPrinter(env.stdout);
10125
+ const stderrTTY = env.stderr.isTTY === true;
10126
+ const progress = new StreamProgress(env.stderr, stderrTTY, client.modelRegistry);
10127
+ const estimatedInputTokens = Math.round(prompt.length / FALLBACK_CHARS_PER_TOKEN);
10128
+ progress.startCall(model, estimatedInputTokens);
10129
+ let finishReason;
10130
+ let usage;
10131
+ let accumulatedResponse = "";
10132
+ for await (const chunk of stream2) {
10133
+ if (chunk.usage) {
10134
+ usage = chunk.usage;
10135
+ if (chunk.usage.inputTokens) {
10136
+ progress.setInputTokens(chunk.usage.inputTokens, false);
10137
+ }
10138
+ if (chunk.usage.outputTokens) {
10139
+ progress.setOutputTokens(chunk.usage.outputTokens, false);
10140
+ }
10141
+ }
10142
+ if (chunk.text) {
10143
+ progress.pause();
10144
+ accumulatedResponse += chunk.text;
10145
+ progress.update(accumulatedResponse.length);
10146
+ printer.write(chunk.text);
10147
+ }
10148
+ if (chunk.finishReason !== void 0) {
10149
+ finishReason = chunk.finishReason;
9563
10150
  }
9564
- delete merged["gadget"];
9565
- delete merged["gadget-add"];
9566
- delete merged["gadget-remove"];
9567
- resolving.delete(name);
9568
- resolved[name] = merged;
9569
- return merged;
9570
10151
  }
9571
- for (const name of Object.keys(config)) {
9572
- resolveSection(name);
10152
+ progress.endCall(usage);
10153
+ progress.complete();
10154
+ printer.ensureNewline();
10155
+ if (llmResponsesDir) {
10156
+ const filename = `${timestamp}_complete.response.txt`;
10157
+ await writeLogFile(llmResponsesDir, filename, accumulatedResponse);
9573
10158
  }
9574
- return resolved;
10159
+ if (stderrTTY && !options.quiet) {
10160
+ const summary = renderSummary({ finishReason, usage, cost: progress.getTotalCost() });
10161
+ if (summary) {
10162
+ env.stderr.write(`${summary}
10163
+ `);
10164
+ }
10165
+ }
10166
+ }
10167
+ function registerCompleteCommand(program, env, config) {
10168
+ const cmd = program.command(COMMANDS.complete).description("Stream a single completion from a specified model.").argument("[prompt]", "Prompt to send to the LLM. If omitted, stdin is used when available.");
10169
+ addCompleteOptions(cmd, config);
10170
+ cmd.action(
10171
+ (prompt, options) => executeAction(() => executeComplete(prompt, options, env), env)
10172
+ );
9575
10173
  }
9576
10174
 
9577
10175
  // src/cli/gadget-command.ts
@@ -10261,7 +10859,11 @@ function createCommandEnvironment(baseEnv, config) {
10261
10859
  logFile: config["log-file"] ?? baseEnv.loggerConfig?.logFile,
10262
10860
  logReset: config["log-reset"] ?? baseEnv.loggerConfig?.logReset
10263
10861
  };
10264
- return createDefaultEnvironment(loggerConfig);
10862
+ return {
10863
+ ...baseEnv,
10864
+ loggerConfig,
10865
+ createLogger: createLoggerFactory(loggerConfig)
10866
+ };
10265
10867
  }
10266
10868
  function registerCustomCommand(program, name, config, env) {
10267
10869
  const type = config.type ?? "agent";
@@ -10337,7 +10939,12 @@ async function runCLI(overrides = {}) {
10337
10939
  logReset: globalOpts.logReset ?? config.global?.["log-reset"]
10338
10940
  };
10339
10941
  const defaultEnv = createDefaultEnvironment(loggerConfig);
10340
- const env = { ...defaultEnv, ...envOverrides };
10942
+ const env = {
10943
+ ...defaultEnv,
10944
+ ...envOverrides,
10945
+ // Pass Docker config from [docker] section
10946
+ dockerConfig: config.docker
10947
+ };
10341
10948
  const program = createProgram(env, config);
10342
10949
  await program.parseAsync(env.argv);
10343
10950
  }