llmist 1.5.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
@@ -6553,7 +6553,11 @@ var OPTION_FLAGS = {
6553
6553
  logLlmResponses: "--log-llm-responses [dir]",
6554
6554
  noBuiltins: "--no-builtins",
6555
6555
  noBuiltinInteraction: "--no-builtin-interaction",
6556
- quiet: "-q, --quiet"
6556
+ quiet: "-q, --quiet",
6557
+ docker: "--docker",
6558
+ dockerRo: "--docker-ro",
6559
+ noDocker: "--no-docker",
6560
+ dockerDev: "--docker-dev"
6557
6561
  };
6558
6562
  var OPTION_DESCRIPTIONS = {
6559
6563
  model: "Model identifier, e.g. openai:gpt-5-nano or anthropic:claude-sonnet-4-5.",
@@ -6569,7 +6573,11 @@ var OPTION_DESCRIPTIONS = {
6569
6573
  logLlmResponses: "Save raw LLM responses as plain text. Optional dir, defaults to ~/.llmist/logs/responses/",
6570
6574
  noBuiltins: "Disable built-in gadgets (AskUser, TellUser).",
6571
6575
  noBuiltinInteraction: "Disable interactive gadgets (AskUser) while keeping TellUser.",
6572
- 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)."
6573
6581
  };
6574
6582
  var SUMMARY_PREFIX = "[llmist]";
6575
6583
 
@@ -6579,7 +6587,7 @@ var import_commander2 = require("commander");
6579
6587
  // package.json
6580
6588
  var package_default = {
6581
6589
  name: "llmist",
6582
- version: "1.4.0",
6590
+ version: "1.5.0",
6583
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.",
6584
6592
  type: "module",
6585
6593
  main: "dist/index.cjs",
@@ -8389,7 +8397,7 @@ function addAgentOptions(cmd, defaults) {
8389
8397
  OPTION_FLAGS.noBuiltinInteraction,
8390
8398
  OPTION_DESCRIPTIONS.noBuiltinInteraction,
8391
8399
  defaults?.["builtin-interaction"] !== false
8392
- ).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);
8393
8401
  }
8394
8402
  function configToCompleteOptions(config) {
8395
8403
  const result = {};
@@ -8424,668 +8432,791 @@ function configToAgentOptions(config) {
8424
8432
  if (config.quiet !== void 0) result.quiet = config.quiet;
8425
8433
  if (config["log-llm-requests"] !== void 0) result.logLlmRequests = config["log-llm-requests"];
8426
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"];
8427
8438
  return result;
8428
8439
  }
8429
8440
 
8430
- // src/cli/agent-command.ts
8431
- function createHumanInputHandler(env, progress, keyboard) {
8432
- const stdout = env.stdout;
8433
- if (!isInteractive(env.stdin) || typeof stdout.isTTY !== "boolean" || !stdout.isTTY) {
8434
- 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";
8435
8479
  }
8436
- return async (question) => {
8437
- progress.pause();
8438
- if (keyboard.cleanupEsc) {
8439
- keyboard.cleanupEsc();
8440
- keyboard.cleanupEsc = null;
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)) {
8491
+ try {
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
+ );
8441
8499
  }
8442
- const rl = (0, import_promises3.createInterface)({ input: env.stdin, output: env.stdout });
8500
+ }
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
8517
+ );
8518
+ }
8519
+ }
8520
+ function validatePrompts(prompts, configPath) {
8521
+ const eta = createTemplateEngine(prompts, configPath);
8522
+ for (const [name, template] of Object.entries(prompts)) {
8443
8523
  try {
8444
- const questionLine = question.trim() ? `
8445
- ${renderMarkdownWithSeparators(question.trim())}` : "";
8446
- let isFirst = true;
8447
- while (true) {
8448
- const statsPrompt = progress.formatPrompt();
8449
- const prompt = isFirst ? `${questionLine}
8450
- ${statsPrompt}` : statsPrompt;
8451
- isFirst = false;
8452
- const answer = await rl.question(prompt);
8453
- const trimmed = answer.trim();
8454
- if (trimmed) {
8455
- return trimmed;
8456
- }
8457
- }
8458
- } finally {
8459
- rl.close();
8460
- keyboard.restore();
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
+ );
8461
8531
  }
8462
- };
8532
+ }
8463
8533
  }
8464
- async function executeAgent(promptArg, options, env) {
8465
- const prompt = await resolvePrompt(promptArg, env);
8466
- const client = env.createClient();
8467
- const registry = new GadgetRegistry();
8468
- const stdinIsInteractive = isInteractive(env.stdin);
8469
- if (options.builtins !== false) {
8470
- for (const gadget of builtinGadgets) {
8471
- if (gadget.name === "AskUser" && (options.builtinInteraction === false || !stdinIsInteractive)) {
8472
- continue;
8473
- }
8474
- registry.registerByClass(gadget);
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
+ );
8475
8545
  }
8476
8546
  }
8477
- const gadgetSpecifiers = options.gadget ?? [];
8478
- if (gadgetSpecifiers.length > 0) {
8479
- const gadgets2 = await loadGadgets(gadgetSpecifiers, process.cwd());
8480
- for (const gadget of gadgets2) {
8481
- registry.registerByClass(gadget);
8547
+ }
8548
+ function hasTemplateSyntax(str) {
8549
+ return str.includes("<%");
8550
+ }
8551
+
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`);
8482
8658
  }
8483
8659
  }
8484
- const printer = new StreamPrinter(env.stdout);
8485
- const stderrTTY = env.stderr.isTTY === true;
8486
- const progress = new StreamProgress(env.stderr, stderrTTY, client.modelRegistry);
8487
- const abortController = new AbortController();
8488
- let wasCancelled = false;
8489
- let isStreaming = false;
8490
- const stdinStream = env.stdin;
8491
- const handleCancel = () => {
8492
- if (!abortController.signal.aborted) {
8493
- wasCancelled = true;
8494
- abortController.abort();
8495
- progress.pause();
8496
- env.stderr.write(import_chalk5.default.yellow(`
8497
- [Cancelled] ${progress.formatStats()}
8498
- `));
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`);
8670
+ }
8499
8671
  }
8500
- };
8501
- const keyboard = {
8502
- cleanupEsc: null,
8503
- cleanupSigint: null,
8504
- restore: () => {
8505
- if (stdinIsInteractive && stdinStream.isTTY && !wasCancelled) {
8506
- keyboard.cleanupEsc = createEscKeyListener(stdinStream, handleCancel);
8507
- }
8672
+ return value;
8673
+ }
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
+ );
8508
8688
  }
8509
- };
8510
- const handleQuit = () => {
8511
- keyboard.cleanupEsc?.();
8512
- keyboard.cleanupSigint?.();
8513
- progress.complete();
8514
- printer.ensureNewline();
8515
- const summary = renderOverallSummary({
8516
- totalTokens: usage?.totalTokens,
8517
- iterations,
8518
- elapsedSeconds: progress.getTotalElapsedSeconds(),
8519
- cost: progress.getTotalCost()
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
8520
8729
  });
8521
- if (summary) {
8522
- env.stderr.write(`${import_chalk5.default.dim("\u2500".repeat(40))}
8523
- `);
8524
- env.stderr.write(`${summary}
8525
- `);
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"`);
8526
8741
  }
8527
- env.stderr.write(import_chalk5.default.dim("[Quit]\n"));
8528
- process.exit(130);
8529
- };
8530
- if (stdinIsInteractive && stdinStream.isTTY) {
8531
- keyboard.cleanupEsc = createEscKeyListener(stdinStream, handleCancel);
8742
+ result["docker-cwd-permission"] = perm;
8532
8743
  }
8533
- keyboard.cleanupSigint = createSigintListener(
8534
- handleCancel,
8535
- handleQuit,
8536
- () => isStreaming && !abortController.signal.aborted,
8537
- env.stderr
8538
- );
8539
- const DEFAULT_APPROVAL_REQUIRED = ["RunCommand", "WriteFile", "EditFile"];
8540
- const userApprovals = options.gadgetApproval ?? {};
8541
- const gadgetApprovals = {
8542
- ...userApprovals
8543
- };
8544
- for (const gadget of DEFAULT_APPROVAL_REQUIRED) {
8545
- const normalizedGadget = gadget.toLowerCase();
8546
- const isConfigured = Object.keys(userApprovals).some(
8547
- (key) => key.toLowerCase() === normalizedGadget
8548
- );
8549
- if (!isConfigured) {
8550
- gadgetApprovals[gadget] = "approval-required";
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`);
8551
8754
  }
8552
8755
  }
8553
- const approvalConfig = {
8554
- gadgetApprovals,
8555
- defaultMode: "allowed"
8556
- };
8557
- const approvalManager = new ApprovalManager(approvalConfig, env, progress);
8558
- let usage;
8559
- let iterations = 0;
8560
- const llmRequestsDir = resolveLogDir(options.logLlmRequests, "requests");
8561
- const llmResponsesDir = resolveLogDir(options.logLlmResponses, "responses");
8562
- let llmCallCounter = 0;
8563
- const countMessagesTokens = async (model, messages) => {
8564
- try {
8565
- return await client.countTokens(model, messages);
8566
- } catch {
8567
- const totalChars = messages.reduce((sum, m) => sum + (m.content?.length ?? 0), 0);
8568
- return Math.round(totalChars / FALLBACK_CHARS_PER_TOKEN);
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`);
8569
8766
  }
8767
+ }
8768
+ const result = {
8769
+ ...validateBaseConfig(rawObj, section),
8770
+ ...validateLoggingConfig(rawObj, section)
8570
8771
  };
8571
- const countGadgetOutputTokens = async (output) => {
8572
- if (!output) return void 0;
8573
- try {
8574
- const messages = [{ role: "assistant", content: output }];
8575
- return await client.countTokens(options.model, messages);
8576
- } catch {
8577
- return void 0;
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`);
8578
8805
  }
8806
+ }
8807
+ const result = {
8808
+ ...validateBaseConfig(rawObj, section),
8809
+ ...validateLoggingConfig(rawObj, section)
8579
8810
  };
8580
- const builder = new AgentBuilder(client).withModel(options.model).withLogger(env.createLogger("llmist:cli:agent")).withHooks({
8581
- observers: {
8582
- // onLLMCallStart: Start progress indicator for each LLM call
8583
- // This showcases how to react to agent lifecycle events
8584
- onLLMCallStart: async (context) => {
8585
- isStreaming = true;
8586
- llmCallCounter++;
8587
- const inputTokens = await countMessagesTokens(
8588
- context.options.model,
8589
- context.options.messages
8590
- );
8591
- progress.startCall(context.options.model, inputTokens);
8592
- progress.setInputTokens(inputTokens, false);
8593
- if (llmRequestsDir) {
8594
- const filename = `${Date.now()}_call_${llmCallCounter}.request.txt`;
8595
- const content = formatLlmRequest(context.options.messages);
8596
- await writeLogFile(llmRequestsDir, filename, content);
8597
- }
8598
- },
8599
- // onStreamChunk: Real-time updates as LLM generates tokens
8600
- // This enables responsive UIs that show progress during generation
8601
- onStreamChunk: async (context) => {
8602
- progress.update(context.accumulatedText.length);
8603
- if (context.usage) {
8604
- if (context.usage.inputTokens) {
8605
- progress.setInputTokens(context.usage.inputTokens, false);
8606
- }
8607
- if (context.usage.outputTokens) {
8608
- progress.setOutputTokens(context.usage.outputTokens, false);
8609
- }
8610
- progress.setCachedTokens(
8611
- context.usage.cachedInputTokens ?? 0,
8612
- context.usage.cacheCreationInputTokens ?? 0
8613
- );
8614
- }
8615
- },
8616
- // onLLMCallComplete: Finalize metrics after each LLM call
8617
- // This is where you'd typically log metrics or update dashboards
8618
- onLLMCallComplete: async (context) => {
8619
- isStreaming = false;
8620
- usage = context.usage;
8621
- iterations = Math.max(iterations, context.iteration + 1);
8622
- if (context.usage) {
8623
- if (context.usage.inputTokens) {
8624
- progress.setInputTokens(context.usage.inputTokens, false);
8625
- }
8626
- if (context.usage.outputTokens) {
8627
- progress.setOutputTokens(context.usage.outputTokens, false);
8628
- }
8629
- }
8630
- let callCost;
8631
- if (context.usage && client.modelRegistry) {
8632
- try {
8633
- const modelName = context.options.model.includes(":") ? context.options.model.split(":")[1] : context.options.model;
8634
- const costResult = client.modelRegistry.estimateCost(
8635
- modelName,
8636
- context.usage.inputTokens,
8637
- context.usage.outputTokens,
8638
- context.usage.cachedInputTokens ?? 0,
8639
- context.usage.cacheCreationInputTokens ?? 0
8640
- );
8641
- if (costResult) callCost = costResult.totalCost;
8642
- } catch {
8643
- }
8644
- }
8645
- const callElapsed = progress.getCallElapsedSeconds();
8646
- progress.endCall(context.usage);
8647
- if (!options.quiet) {
8648
- const summary = renderSummary({
8649
- iterations: context.iteration + 1,
8650
- model: options.model,
8651
- usage: context.usage,
8652
- elapsedSeconds: callElapsed,
8653
- cost: callCost,
8654
- finishReason: context.finishReason
8655
- });
8656
- if (summary) {
8657
- env.stderr.write(`${summary}
8658
- `);
8659
- }
8660
- }
8661
- if (llmResponsesDir) {
8662
- const filename = `${Date.now()}_call_${llmCallCounter}.response.txt`;
8663
- await writeLogFile(llmResponsesDir, filename, context.rawResponse);
8664
- }
8665
- }
8666
- },
8667
- // SHOWCASE: Controller-based approval gating for gadgets
8668
- //
8669
- // This demonstrates how to add safety layers WITHOUT modifying gadgets.
8670
- // The ApprovalManager handles approval flows externally via beforeGadgetExecution.
8671
- // Approval modes are configurable via cli.toml:
8672
- // - "allowed": auto-proceed
8673
- // - "denied": auto-reject, return message to LLM
8674
- // - "approval-required": prompt user interactively
8675
- //
8676
- // Default: RunCommand, WriteFile, EditFile require approval unless overridden.
8677
- controllers: {
8678
- beforeGadgetExecution: async (ctx) => {
8679
- const mode = approvalManager.getApprovalMode(ctx.gadgetName);
8680
- if (mode === "allowed") {
8681
- return { action: "proceed" };
8682
- }
8683
- const stdinTTY = isInteractive(env.stdin);
8684
- const stderrTTY2 = env.stderr.isTTY === true;
8685
- const canPrompt = stdinTTY && stderrTTY2;
8686
- if (!canPrompt) {
8687
- if (mode === "approval-required") {
8688
- return {
8689
- action: "skip",
8690
- syntheticResult: `status=denied
8691
-
8692
- ${ctx.gadgetName} requires interactive approval. Run in a terminal to approve.`
8693
- };
8694
- }
8695
- if (mode === "denied") {
8696
- return {
8697
- action: "skip",
8698
- syntheticResult: `status=denied
8699
-
8700
- ${ctx.gadgetName} is denied by configuration.`
8701
- };
8702
- }
8703
- return { action: "proceed" };
8704
- }
8705
- const result = await approvalManager.requestApproval(ctx.gadgetName, ctx.parameters);
8706
- if (!result.approved) {
8707
- return {
8708
- action: "skip",
8709
- syntheticResult: `status=denied
8710
-
8711
- Denied: ${result.reason ?? "by user"}`
8712
- };
8713
- }
8714
- return { action: "proceed" };
8715
- }
8716
- }
8717
- });
8718
- if (options.system) {
8719
- builder.withSystem(options.system);
8811
+ if ("max-iterations" in rawObj) {
8812
+ result["max-iterations"] = validateNumber(rawObj["max-iterations"], "max-iterations", section, {
8813
+ integer: true,
8814
+ min: 1
8815
+ });
8720
8816
  }
8721
- if (options.maxIterations !== void 0) {
8722
- builder.withMaxIterations(options.maxIterations);
8817
+ if ("gadgets" in rawObj) {
8818
+ result.gadgets = validateStringArray(rawObj.gadgets, "gadgets", section);
8723
8819
  }
8724
- if (options.temperature !== void 0) {
8725
- builder.withTemperature(options.temperature);
8820
+ if ("gadget-add" in rawObj) {
8821
+ result["gadget-add"] = validateStringArray(rawObj["gadget-add"], "gadget-add", section);
8726
8822
  }
8727
- const humanInputHandler = createHumanInputHandler(env, progress, keyboard);
8728
- if (humanInputHandler) {
8729
- builder.onHumanInput(humanInputHandler);
8823
+ if ("gadget-remove" in rawObj) {
8824
+ result["gadget-remove"] = validateStringArray(rawObj["gadget-remove"], "gadget-remove", section);
8730
8825
  }
8731
- builder.withSignal(abortController.signal);
8732
- const gadgets = registry.getAll();
8733
- if (gadgets.length > 0) {
8734
- builder.withGadgets(...gadgets);
8826
+ if ("gadget" in rawObj) {
8827
+ result.gadget = validateStringArray(rawObj.gadget, "gadget", section);
8735
8828
  }
8736
- if (options.gadgetStartPrefix) {
8737
- builder.withGadgetStartPrefix(options.gadgetStartPrefix);
8829
+ if ("builtins" in rawObj) {
8830
+ result.builtins = validateBoolean(rawObj.builtins, "builtins", section);
8738
8831
  }
8739
- if (options.gadgetEndPrefix) {
8740
- builder.withGadgetEndPrefix(options.gadgetEndPrefix);
8832
+ if ("builtin-interaction" in rawObj) {
8833
+ result["builtin-interaction"] = validateBoolean(
8834
+ rawObj["builtin-interaction"],
8835
+ "builtin-interaction",
8836
+ section
8837
+ );
8741
8838
  }
8742
- if (options.gadgetArgPrefix) {
8743
- builder.withGadgetArgPrefix(options.gadgetArgPrefix);
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
+ );
8744
8845
  }
8745
- builder.withSyntheticGadgetCall(
8746
- "TellUser",
8747
- {
8748
- 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?",
8749
- done: false,
8750
- type: "info"
8751
- },
8752
- "\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?"
8753
- );
8754
- builder.withTextOnlyHandler("acknowledge");
8755
- builder.withTextWithGadgetsHandler({
8756
- gadgetName: "TellUser",
8757
- parameterMapping: (text) => ({ message: text, done: false, type: "info" }),
8758
- resultMapping: (text) => `\u2139\uFE0F ${text}`
8759
- });
8760
- const agent = builder.ask(prompt);
8761
- let textBuffer = "";
8762
- const flushTextBuffer = () => {
8763
- if (textBuffer) {
8764
- const output = options.quiet ? textBuffer : renderMarkdownWithSeparators(textBuffer);
8765
- printer.write(output);
8766
- textBuffer = "";
8767
- }
8768
- };
8769
- try {
8770
- for await (const event of agent.run()) {
8771
- if (event.type === "text") {
8772
- progress.pause();
8773
- textBuffer += event.content;
8774
- } else if (event.type === "gadget_result") {
8775
- flushTextBuffer();
8776
- progress.pause();
8777
- if (options.quiet) {
8778
- if (event.result.gadgetName === "TellUser" && event.result.parameters?.message) {
8779
- const message = String(event.result.parameters.message);
8780
- env.stdout.write(`${message}
8781
- `);
8782
- }
8783
- } else {
8784
- const tokenCount = await countGadgetOutputTokens(event.result.result);
8785
- env.stderr.write(`${formatGadgetSummary({ ...event.result, tokenCount })}
8786
- `);
8787
- }
8788
- }
8789
- }
8790
- } catch (error) {
8791
- if (!isAbortError(error)) {
8792
- throw error;
8793
- }
8794
- } finally {
8795
- isStreaming = false;
8796
- keyboard.cleanupEsc?.();
8797
- keyboard.cleanupSigint?.();
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
+ );
8798
8852
  }
8799
- flushTextBuffer();
8800
- progress.complete();
8801
- printer.ensureNewline();
8802
- if (!options.quiet && iterations > 1) {
8803
- env.stderr.write(`${import_chalk5.default.dim("\u2500".repeat(40))}
8804
- `);
8805
- const summary = renderOverallSummary({
8806
- totalTokens: usage?.totalTokens,
8807
- iterations,
8808
- elapsedSeconds: progress.getTotalElapsedSeconds(),
8809
- cost: progress.getTotalCost()
8810
- });
8811
- if (summary) {
8812
- env.stderr.write(`${summary}
8813
- `);
8814
- }
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
+ );
8815
8879
  }
8880
+ return result;
8816
8881
  }
8817
- function registerAgentCommand(program, env, config) {
8818
- 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.");
8819
- addAgentOptions(cmd, config);
8820
- cmd.action(
8821
- (prompt, options) => executeAction(() => {
8822
- const mergedOptions = {
8823
- ...options,
8824
- gadgetApproval: config?.["gadget-approval"]
8825
- };
8826
- return executeAgent(prompt, mergedOptions, env);
8827
- }, env)
8828
- );
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`);
8829
8887
  }
8830
-
8831
- // src/cli/complete-command.ts
8832
- init_messages();
8833
- init_model_shortcuts();
8834
- init_constants2();
8835
- async function executeComplete(promptArg, options, env) {
8836
- const prompt = await resolvePrompt(promptArg, env);
8837
- const client = env.createClient();
8838
- const model = resolveModel(options.model);
8839
- const builder = new LLMMessageBuilder();
8840
- if (options.system) {
8841
- builder.addSystem(options.system);
8888
+ function validateCustomConfig(raw, section) {
8889
+ if (typeof raw !== "object" || raw === null) {
8890
+ throw new ConfigError(`[${section}] must be a table`);
8842
8891
  }
8843
- builder.addUser(prompt);
8844
- const messages = builder.build();
8845
- const llmRequestsDir = resolveLogDir(options.logLlmRequests, "requests");
8846
- const llmResponsesDir = resolveLogDir(options.logLlmResponses, "responses");
8847
- const timestamp = Date.now();
8848
- if (llmRequestsDir) {
8849
- const filename = `${timestamp}_complete.request.txt`;
8850
- const content = formatLlmRequest(messages);
8851
- await writeLogFile(llmRequestsDir, filename, content);
8852
- }
8853
- const stream2 = client.stream({
8854
- model,
8855
- messages,
8856
- temperature: options.temperature,
8857
- maxTokens: options.maxTokens
8858
- });
8859
- const printer = new StreamPrinter(env.stdout);
8860
- const stderrTTY = env.stderr.isTTY === true;
8861
- const progress = new StreamProgress(env.stderr, stderrTTY, client.modelRegistry);
8862
- const estimatedInputTokens = Math.round(prompt.length / FALLBACK_CHARS_PER_TOKEN);
8863
- progress.startCall(model, estimatedInputTokens);
8864
- let finishReason;
8865
- let usage;
8866
- let accumulatedResponse = "";
8867
- for await (const chunk of stream2) {
8868
- if (chunk.usage) {
8869
- usage = chunk.usage;
8870
- if (chunk.usage.inputTokens) {
8871
- progress.setInputTokens(chunk.usage.inputTokens, false);
8872
- }
8873
- if (chunk.usage.outputTokens) {
8874
- progress.setOutputTokens(chunk.usage.outputTokens, false);
8875
- }
8876
- }
8877
- if (chunk.text) {
8878
- progress.pause();
8879
- accumulatedResponse += chunk.text;
8880
- progress.update(accumulatedResponse.length);
8881
- printer.write(chunk.text);
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`);
8882
8896
  }
8883
- if (chunk.finishReason !== void 0) {
8884
- finishReason = chunk.finishReason;
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"`);
8885
8903
  }
8904
+ type = typeValue;
8886
8905
  }
8887
- progress.endCall(usage);
8888
- progress.complete();
8889
- printer.ensureNewline();
8890
- if (llmResponsesDir) {
8891
- const filename = `${timestamp}_complete.response.txt`;
8892
- await writeLogFile(llmResponsesDir, filename, accumulatedResponse);
8906
+ const result = {
8907
+ ...validateBaseConfig(rawObj, section),
8908
+ type
8909
+ };
8910
+ if ("description" in rawObj) {
8911
+ result.description = validateString(rawObj.description, "description", section);
8893
8912
  }
8894
- if (stderrTTY && !options.quiet) {
8895
- const summary = renderSummary({ finishReason, usage, cost: progress.getTotalCost() });
8896
- if (summary) {
8897
- env.stderr.write(`${summary}
8898
- `);
8899
- }
8913
+ if ("max-iterations" in rawObj) {
8914
+ result["max-iterations"] = validateNumber(rawObj["max-iterations"], "max-iterations", section, {
8915
+ integer: true,
8916
+ min: 1
8917
+ });
8918
+ }
8919
+ if ("gadgets" in rawObj) {
8920
+ result.gadgets = validateStringArray(rawObj.gadgets, "gadgets", section);
8921
+ }
8922
+ if ("gadget-add" in rawObj) {
8923
+ result["gadget-add"] = validateStringArray(rawObj["gadget-add"], "gadget-add", section);
8924
+ }
8925
+ if ("gadget-remove" in rawObj) {
8926
+ result["gadget-remove"] = validateStringArray(rawObj["gadget-remove"], "gadget-remove", section);
8927
+ }
8928
+ if ("gadget" in rawObj) {
8929
+ result.gadget = validateStringArray(rawObj.gadget, "gadget", section);
8930
+ }
8931
+ if ("builtins" in rawObj) {
8932
+ result.builtins = validateBoolean(rawObj.builtins, "builtins", section);
8933
+ }
8934
+ if ("builtin-interaction" in rawObj) {
8935
+ result["builtin-interaction"] = validateBoolean(
8936
+ rawObj["builtin-interaction"],
8937
+ "builtin-interaction",
8938
+ section
8939
+ );
8940
+ }
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
+ );
8947
+ }
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
+ );
8954
+ }
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
+ );
8961
+ }
8962
+ if ("gadget-approval" in rawObj) {
8963
+ result["gadget-approval"] = validateGadgetApproval(rawObj["gadget-approval"], section);
8964
+ }
8965
+ if ("max-tokens" in rawObj) {
8966
+ result["max-tokens"] = validateNumber(rawObj["max-tokens"], "max-tokens", section, {
8967
+ integer: true,
8968
+ min: 1
8969
+ });
8970
+ }
8971
+ if ("quiet" in rawObj) {
8972
+ result.quiet = validateBoolean(rawObj.quiet, "quiet", section);
8900
8973
  }
8974
+ Object.assign(result, validateLoggingConfig(rawObj, section));
8975
+ return result;
8901
8976
  }
8902
- function registerCompleteCommand(program, env, config) {
8903
- 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.");
8904
- addCompleteOptions(cmd, config);
8905
- cmd.action(
8906
- (prompt, options) => executeAction(() => executeComplete(prompt, options, env), env)
8907
- );
8977
+ function validatePromptsConfig(raw, section) {
8978
+ if (typeof raw !== "object" || raw === null) {
8979
+ throw new ConfigError(`[${section}] must be a table`);
8980
+ }
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;
8908
8989
  }
8909
-
8910
- // src/cli/config.ts
8911
- var import_node_fs8 = require("fs");
8912
- var import_node_os2 = require("os");
8913
- var import_node_path8 = require("path");
8914
- var import_js_toml = require("js-toml");
8915
-
8916
- // src/cli/templates.ts
8917
- var import_eta = require("eta");
8918
- var TemplateError = class extends Error {
8919
- constructor(message, promptName, configPath) {
8920
- super(promptName ? `[prompts.${promptName}]: ${message}` : message);
8921
- this.promptName = promptName;
8922
- this.configPath = configPath;
8923
- this.name = "TemplateError";
8990
+ function validateConfig(raw, configPath) {
8991
+ if (typeof raw !== "object" || raw === null) {
8992
+ throw new ConfigError("Config must be a TOML table", configPath);
8924
8993
  }
8925
- };
8926
- function createTemplateEngine(prompts, configPath) {
8927
- const eta = new import_eta.Eta({
8928
- views: "/",
8929
- // Required but we use named templates
8930
- autoEscape: false,
8931
- // Don't escape - these are prompts, not HTML
8932
- autoTrim: false
8933
- // Preserve whitespace in prompts
8934
- });
8935
- for (const [name, template] of Object.entries(prompts)) {
8994
+ const rawObj = raw;
8995
+ const result = {};
8996
+ for (const [key, value] of Object.entries(rawObj)) {
8936
8997
  try {
8937
- 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
+ }
8938
9011
  } catch (error) {
8939
- throw new TemplateError(
8940
- error instanceof Error ? error.message : String(error),
8941
- name,
8942
- configPath
8943
- );
9012
+ if (error instanceof ConfigError) {
9013
+ throw new ConfigError(error.message, configPath);
9014
+ }
9015
+ throw error;
8944
9016
  }
8945
9017
  }
8946
- return eta;
9018
+ return result;
8947
9019
  }
8948
- 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;
8949
9026
  try {
8950
- const fullContext = {
8951
- ...context,
8952
- env: process.env,
8953
- date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
8954
- // "2025-12-01"
8955
- };
8956
- return eta.renderString(template, fullContext);
9027
+ content = (0, import_node_fs8.readFileSync)(configPath, "utf-8");
8957
9028
  } catch (error) {
8958
- throw new TemplateError(
8959
- error instanceof Error ? error.message : String(error),
8960
- void 0,
9029
+ throw new ConfigError(
9030
+ `Failed to read config file: ${error instanceof Error ? error.message : "Unknown error"}`,
8961
9031
  configPath
8962
9032
  );
8963
9033
  }
8964
- }
8965
- function validatePrompts(prompts, configPath) {
8966
- const eta = createTemplateEngine(prompts, configPath);
8967
- for (const [name, template] of Object.entries(prompts)) {
8968
- try {
8969
- eta.renderString(template, { env: {} });
8970
- } catch (error) {
8971
- throw new TemplateError(
8972
- error instanceof Error ? error.message : String(error),
8973
- name,
8974
- configPath
8975
- );
8976
- }
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"}`,
9040
+ configPath
9041
+ );
8977
9042
  }
9043
+ const validated = validateConfig(raw, configPath);
9044
+ const inherited = resolveInheritance(validated, configPath);
9045
+ return resolveTemplatesInConfig(inherited, configPath);
8978
9046
  }
8979
- function validateEnvVars(template, promptName, configPath) {
8980
- const envVarPattern = /<%=\s*it\.env\.(\w+)\s*%>/g;
8981
- const matches = template.matchAll(envVarPattern);
8982
- for (const match of matches) {
8983
- const varName = match[1];
8984
- if (process.env[varName] === void 0) {
8985
- throw new TemplateError(
8986
- `Environment variable '${varName}' is not set`,
8987
- promptName,
8988
- configPath
8989
- );
8990
- }
8991
- }
8992
- }
8993
- function hasTemplateSyntax(str) {
8994
- return str.includes("<%");
8995
- }
8996
-
8997
- // src/cli/config.ts
8998
- var VALID_APPROVAL_MODES = ["allowed", "denied", "approval-required"];
8999
- var GLOBAL_CONFIG_KEYS = /* @__PURE__ */ new Set(["log-level", "log-file", "log-reset"]);
9000
- var VALID_LOG_LEVELS = ["silly", "trace", "debug", "info", "warn", "error", "fatal"];
9001
- var COMPLETE_CONFIG_KEYS = /* @__PURE__ */ new Set([
9002
- "model",
9003
- "system",
9004
- "temperature",
9005
- "max-tokens",
9006
- "quiet",
9007
- "inherits",
9008
- "log-level",
9009
- "log-file",
9010
- "log-reset",
9011
- "log-llm-requests",
9012
- "log-llm-responses",
9013
- "type"
9014
- // Allowed for inheritance compatibility, ignored for built-in commands
9015
- ]);
9016
- var AGENT_CONFIG_KEYS = /* @__PURE__ */ new Set([
9017
- "model",
9018
- "system",
9019
- "temperature",
9020
- "max-iterations",
9021
- "gadgets",
9022
- // Full replacement (preferred)
9023
- "gadget-add",
9024
- // Add to inherited gadgets
9025
- "gadget-remove",
9026
- // Remove from inherited gadgets
9027
- "gadget",
9028
- // DEPRECATED: alias for gadgets
9029
- "builtins",
9030
- "builtin-interaction",
9031
- "gadget-start-prefix",
9032
- "gadget-end-prefix",
9033
- "gadget-arg-prefix",
9034
- "gadget-approval",
9035
- "quiet",
9036
- "inherits",
9037
- "log-level",
9038
- "log-file",
9039
- "log-reset",
9040
- "log-llm-requests",
9041
- "log-llm-responses",
9042
- "type"
9043
- // Allowed for inheritance compatibility, ignored for built-in commands
9044
- ]);
9045
- var CUSTOM_CONFIG_KEYS = /* @__PURE__ */ new Set([
9046
- ...COMPLETE_CONFIG_KEYS,
9047
- ...AGENT_CONFIG_KEYS,
9048
- "type",
9049
- "description"
9050
- ]);
9051
- function getConfigPath() {
9052
- return (0, import_node_path8.join)((0, import_node_os2.homedir)(), ".llmist", "cli.toml");
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));
9053
9050
  }
9054
- var ConfigError = class extends Error {
9055
- constructor(message, path5) {
9056
- super(path5 ? `${path5}: ${message}` : message);
9057
- this.path = path5;
9058
- this.name = "ConfigError";
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
+ }
9059
9063
  }
9060
- };
9061
- function validateString(value, key, section) {
9062
- if (typeof value !== "string") {
9063
- throw new ConfigError(`[${section}].${key} must be a string`);
9064
+ for (const template of Object.values(prompts)) {
9065
+ if (hasTemplateSyntax(template)) {
9066
+ hasTemplates = true;
9067
+ break;
9068
+ }
9064
9069
  }
9065
- return value;
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
+ }
9081
+ for (const [name, template] of Object.entries(prompts)) {
9082
+ try {
9083
+ validateEnvVars(template, name, configPath);
9084
+ } catch (error) {
9085
+ if (error instanceof TemplateError) {
9086
+ throw new ConfigError(error.message, configPath);
9087
+ }
9088
+ throw error;
9089
+ }
9090
+ }
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
+ }
9118
+ }
9119
+ }
9120
+ return result;
9066
9121
  }
9067
- function validateNumber(value, key, section, opts) {
9068
- if (typeof value !== "number") {
9069
- throw new ConfigError(`[${section}].${key} must be a number`);
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
+ );
9070
9131
  }
9071
- if (opts?.integer && !Number.isInteger(value)) {
9072
- throw new ConfigError(`[${section}].${key} must be an integer`);
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
+ );
9073
9137
  }
9074
- if (opts?.min !== void 0 && value < opts.min) {
9075
- throw new ConfigError(`[${section}].${key} must be >= ${opts.min}`);
9138
+ if (hasGadgets) {
9139
+ return section.gadgets;
9076
9140
  }
9077
- if (opts?.max !== void 0 && value > opts.max) {
9078
- throw new ConfigError(`[${section}].${key} must be <= ${opts.max}`);
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;
9154
+ }
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;
9198
+ }
9199
+ for (const name of Object.keys(config)) {
9200
+ resolveSection(name);
9201
+ }
9202
+ return resolved;
9203
+ }
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`);
9079
9210
  }
9080
9211
  return value;
9081
9212
  }
9082
- function validateBoolean(value, key, section) {
9213
+ function validateBoolean2(value, key, section) {
9083
9214
  if (typeof value !== "boolean") {
9084
9215
  throw new ConfigError(`[${section}].${key} must be a boolean`);
9085
9216
  }
9086
9217
  return value;
9087
9218
  }
9088
- function validateStringArray(value, key, section) {
9219
+ function validateStringArray2(value, key, section) {
9089
9220
  if (!Array.isArray(value)) {
9090
9221
  throw new ConfigError(`[${section}].${key} must be an array`);
9091
9222
  }
@@ -9096,535 +9227,949 @@ function validateStringArray(value, key, section) {
9096
9227
  }
9097
9228
  return value;
9098
9229
  }
9099
- function validateInherits(value, section) {
9100
- if (typeof value === "string") {
9101
- return value;
9102
- }
9103
- if (Array.isArray(value)) {
9104
- for (let i = 0; i < value.length; i++) {
9105
- if (typeof value[i] !== "string") {
9106
- throw new ConfigError(`[${section}].inherits[${i}] must be a string`);
9107
- }
9108
- }
9109
- 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
+ );
9110
9236
  }
9111
- throw new ConfigError(`[${section}].inherits must be a string or array of strings`);
9237
+ return str;
9112
9238
  }
9113
- function validateGadgetApproval(value, section) {
9239
+ function validateMountConfig(value, index, section) {
9114
9240
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
9115
- throw new ConfigError(
9116
- `[${section}].gadget-approval must be a table (e.g., { WriteFile = "approval-required" })`
9117
- );
9241
+ throw new ConfigError(`[${section}].mounts[${index}] must be a table`);
9118
9242
  }
9119
- const result = {};
9120
- for (const [gadgetName, mode] of Object.entries(value)) {
9121
- if (typeof mode !== "string") {
9122
- throw new ConfigError(
9123
- `[${section}].gadget-approval.${gadgetName} must be a string`
9124
- );
9125
- }
9126
- if (!VALID_APPROVAL_MODES.includes(mode)) {
9127
- throw new ConfigError(
9128
- `[${section}].gadget-approval.${gadgetName} must be one of: ${VALID_APPROVAL_MODES.join(", ")}`
9129
- );
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`);
9130
9248
  }
9131
- result[gadgetName] = mode;
9132
9249
  }
9133
- return result;
9134
- }
9135
- function validateLoggingConfig(raw, section) {
9136
- const result = {};
9137
- if ("log-level" in raw) {
9138
- const level = validateString(raw["log-level"], "log-level", section);
9139
- if (!VALID_LOG_LEVELS.includes(level)) {
9140
- throw new ConfigError(
9141
- `[${section}].log-level must be one of: ${VALID_LOG_LEVELS.join(", ")}`
9142
- );
9143
- }
9144
- result["log-level"] = level;
9250
+ if (!("source" in rawObj)) {
9251
+ throw new ConfigError(`[${mountSection}] missing required field 'source'`);
9145
9252
  }
9146
- if ("log-file" in raw) {
9147
- result["log-file"] = validateString(raw["log-file"], "log-file", section);
9253
+ if (!("target" in rawObj)) {
9254
+ throw new ConfigError(`[${mountSection}] missing required field 'target'`);
9148
9255
  }
9149
- if ("log-reset" in raw) {
9150
- result["log-reset"] = validateBoolean(raw["log-reset"], "log-reset", section);
9256
+ if (!("permission" in rawObj)) {
9257
+ throw new ConfigError(`[${mountSection}] missing required field 'permission'`);
9151
9258
  }
9152
- 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
+ };
9153
9264
  }
9154
- function validateBaseConfig(raw, section) {
9155
- const result = {};
9156
- if ("model" in raw) {
9157
- result.model = validateString(raw.model, "model", section);
9158
- }
9159
- if ("system" in raw) {
9160
- result.system = validateString(raw.system, "system", section);
9161
- }
9162
- if ("temperature" in raw) {
9163
- result.temperature = validateNumber(raw.temperature, "temperature", section, {
9164
- min: 0,
9165
- max: 2
9166
- });
9265
+ function validateMountsArray(value, section) {
9266
+ if (!Array.isArray(value)) {
9267
+ throw new ConfigError(`[${section}].mounts must be an array of tables`);
9167
9268
  }
9168
- if ("inherits" in raw) {
9169
- 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));
9170
9272
  }
9171
9273
  return result;
9172
9274
  }
9173
- function validateGlobalConfig(raw, section) {
9174
- if (typeof raw !== "object" || raw === null) {
9175
- throw new ConfigError(`[${section}] must be a table`);
9176
- }
9177
- const rawObj = raw;
9178
- for (const key of Object.keys(rawObj)) {
9179
- if (!GLOBAL_CONFIG_KEYS.has(key)) {
9180
- throw new ConfigError(`[${section}].${key} is not a valid option`);
9181
- }
9182
- }
9183
- return validateLoggingConfig(rawObj, section);
9184
- }
9185
- function validateCompleteConfig(raw, section) {
9275
+ function validateDockerConfig(raw, section) {
9186
9276
  if (typeof raw !== "object" || raw === null) {
9187
9277
  throw new ConfigError(`[${section}] must be a table`);
9188
9278
  }
9189
9279
  const rawObj = raw;
9190
9280
  for (const key of Object.keys(rawObj)) {
9191
- if (!COMPLETE_CONFIG_KEYS.has(key)) {
9281
+ if (!DOCKER_CONFIG_KEYS.has(key)) {
9192
9282
  throw new ConfigError(`[${section}].${key} is not a valid option`);
9193
9283
  }
9194
9284
  }
9195
- const result = {
9196
- ...validateBaseConfig(rawObj, section),
9197
- ...validateLoggingConfig(rawObj, section)
9198
- };
9199
- if ("max-tokens" in rawObj) {
9200
- result["max-tokens"] = validateNumber(rawObj["max-tokens"], "max-tokens", section, {
9201
- integer: true,
9202
- min: 1
9203
- });
9285
+ const result = {};
9286
+ if ("enabled" in rawObj) {
9287
+ result.enabled = validateBoolean2(rawObj.enabled, "enabled", section);
9204
9288
  }
9205
- if ("quiet" in rawObj) {
9206
- result.quiet = validateBoolean(rawObj.quiet, "quiet", section);
9289
+ if ("dockerfile" in rawObj) {
9290
+ result.dockerfile = validateString2(rawObj.dockerfile, "dockerfile", section);
9207
9291
  }
9208
- if ("log-llm-requests" in rawObj) {
9209
- result["log-llm-requests"] = validateStringOrBoolean(
9210
- rawObj["log-llm-requests"],
9211
- "log-llm-requests",
9292
+ if ("cwd-permission" in rawObj) {
9293
+ result["cwd-permission"] = validateMountPermission(
9294
+ rawObj["cwd-permission"],
9295
+ "cwd-permission",
9212
9296
  section
9213
9297
  );
9214
9298
  }
9215
- if ("log-llm-responses" in rawObj) {
9216
- result["log-llm-responses"] = validateStringOrBoolean(
9217
- rawObj["log-llm-responses"],
9218
- "log-llm-responses",
9299
+ if ("config-permission" in rawObj) {
9300
+ result["config-permission"] = validateMountPermission(
9301
+ rawObj["config-permission"],
9302
+ "config-permission",
9219
9303
  section
9220
9304
  );
9221
9305
  }
9306
+ if ("mounts" in rawObj) {
9307
+ result.mounts = validateMountsArray(rawObj.mounts, section);
9308
+ }
9309
+ if ("env-vars" in rawObj) {
9310
+ result["env-vars"] = validateStringArray2(rawObj["env-vars"], "env-vars", section);
9311
+ }
9312
+ if ("image-name" in rawObj) {
9313
+ result["image-name"] = validateString2(rawObj["image-name"], "image-name", section);
9314
+ }
9315
+ if ("dev-mode" in rawObj) {
9316
+ result["dev-mode"] = validateBoolean2(rawObj["dev-mode"], "dev-mode", section);
9317
+ }
9318
+ if ("dev-source" in rawObj) {
9319
+ result["dev-source"] = validateString2(rawObj["dev-source"], "dev-source", section);
9320
+ }
9222
9321
  return result;
9223
9322
  }
9224
- function validateAgentConfig(raw, section) {
9225
- if (typeof raw !== "object" || raw === null) {
9226
- throw new ConfigError(`[${section}] must be a table`);
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;
9227
9385
  }
9228
- const rawObj = raw;
9229
- for (const key of Object.keys(rawObj)) {
9230
- if (!AGENT_CONFIG_KEYS.has(key)) {
9231
- throw new ConfigError(`[${section}].${key} is not a valid option`);
9232
- }
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 });
9233
9403
  }
9234
- const result = {
9235
- ...validateBaseConfig(rawObj, section),
9236
- ...validateLoggingConfig(rawObj, section)
9237
- };
9238
- if ("max-iterations" in rawObj) {
9239
- result["max-iterations"] = validateNumber(rawObj["max-iterations"], "max-iterations", section, {
9240
- integer: true,
9241
- min: 1
9242
- });
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;
9243
9409
  }
9244
- if ("gadgets" in rawObj) {
9245
- result.gadgets = validateStringArray(rawObj.gadgets, "gadgets", 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;
9246
9416
  }
9247
- if ("gadget-add" in rawObj) {
9248
- result["gadget-add"] = validateStringArray(rawObj["gadget-add"], "gadget-add", 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
+ }
9249
9429
  }
9250
- if ("gadget-remove" in rawObj) {
9251
- result["gadget-remove"] = validateStringArray(rawObj["gadget-remove"], "gadget-remove", section);
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";
9252
9442
  }
9253
- if ("gadget" in rawObj) {
9254
- result.gadget = validateStringArray(rawObj.gadget, "gadget", 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
9463
+ );
9255
9464
  }
9256
- if ("builtins" in rawObj) {
9257
- result.builtins = validateBoolean(rawObj.builtins, "builtins", section);
9465
+ }
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;
9258
9471
  }
9259
- if ("builtin-interaction" in rawObj) {
9260
- result["builtin-interaction"] = validateBoolean(
9261
- rawObj["builtin-interaction"],
9262
- "builtin-interaction",
9263
- section
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;
9477
+ }
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."
9264
9487
  );
9488
+ this.name = "DockerUnavailableError";
9265
9489
  }
9266
- if ("gadget-start-prefix" in rawObj) {
9267
- result["gadget-start-prefix"] = validateString(
9268
- rawObj["gadget-start-prefix"],
9269
- "gadget-start-prefix",
9270
- section
9271
- );
9490
+ };
9491
+ var DockerSkipError = class extends Error {
9492
+ constructor() {
9493
+ super("Docker execution skipped - already inside container");
9494
+ this.name = "DockerSkipError";
9272
9495
  }
9273
- if ("gadget-end-prefix" in rawObj) {
9274
- result["gadget-end-prefix"] = validateString(
9275
- rawObj["gadget-end-prefix"],
9276
- "gadget-end-prefix",
9277
- section
9278
- );
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;
9279
9507
  }
9280
- if ("gadget-arg-prefix" in rawObj) {
9281
- result["gadget-arg-prefix"] = validateString(
9282
- rawObj["gadget-arg-prefix"],
9283
- "gadget-arg-prefix",
9284
- section
9285
- );
9508
+ }
9509
+ function isInsideContainer() {
9510
+ if ((0, import_node_fs10.existsSync)("/.dockerenv")) {
9511
+ return true;
9286
9512
  }
9287
- if ("gadget-approval" in rawObj) {
9288
- result["gadget-approval"] = validateGadgetApproval(rawObj["gadget-approval"], section);
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;
9517
+ }
9518
+ } catch {
9289
9519
  }
9290
- if ("quiet" in rawObj) {
9291
- result.quiet = validateBoolean(rawObj.quiet, "quiet", 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;
9292
9526
  }
9293
- if ("log-llm-requests" in rawObj) {
9294
- result["log-llm-requests"] = validateStringOrBoolean(
9295
- rawObj["log-llm-requests"],
9296
- "log-llm-requests",
9297
- section
9298
- );
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;
9299
9532
  }
9300
- if ("log-llm-responses" in rawObj) {
9301
- result["log-llm-responses"] = validateStringOrBoolean(
9302
- rawObj["log-llm-responses"],
9303
- "log-llm-responses",
9304
- 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 {
9539
+ }
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 };
9546
+ }
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)."
9305
9551
  );
9306
9552
  }
9307
- return result;
9553
+ return { enabled: true, sourcePath };
9308
9554
  }
9309
- function validateStringOrBoolean(value, field, section) {
9310
- if (typeof value === "string" || typeof value === "boolean") {
9311
- return value;
9555
+ function expandHome(path5) {
9556
+ if (path5.startsWith("~")) {
9557
+ return path5.replace(/^~/, (0, import_node_os4.homedir)());
9312
9558
  }
9313
- throw new ConfigError(`[${section}].${field} must be a string or boolean`);
9559
+ return path5;
9314
9560
  }
9315
- function validateCustomConfig(raw, section) {
9316
- if (typeof raw !== "object" || raw === null) {
9317
- throw new ConfigError(`[${section}] must be a table`);
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");
9318
9569
  }
9319
- const rawObj = raw;
9320
- for (const key of Object.keys(rawObj)) {
9321
- if (!CUSTOM_CONFIG_KEYS.has(key)) {
9322
- throw new ConfigError(`[${section}].${key} is not a valid option`);
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`);
9579
+ }
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}`);
9323
9584
  }
9324
9585
  }
9325
- let type = "agent";
9326
- if ("type" in rawObj) {
9327
- const typeValue = validateString(rawObj.type, "type", section);
9328
- if (typeValue !== "agent" && typeValue !== "complete") {
9329
- throw new ConfigError(`[${section}].type must be "agent" or "complete"`);
9586
+ for (const key of FORWARDED_API_KEYS) {
9587
+ if (process.env[key]) {
9588
+ args.push("-e", key);
9330
9589
  }
9331
- type = typeValue;
9332
9590
  }
9333
- const result = {
9334
- ...validateBaseConfig(rawObj, section),
9335
- type
9336
- };
9337
- if ("description" in rawObj) {
9338
- result.description = validateString(rawObj.description, "description", section);
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
+ }
9339
9597
  }
9340
- if ("max-iterations" in rawObj) {
9341
- result["max-iterations"] = validateNumber(rawObj["max-iterations"], "max-iterations", section, {
9342
- integer: true,
9343
- min: 1
9344
- });
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;
9345
9609
  }
9346
- if ("gadgets" in rawObj) {
9347
- result.gadgets = validateStringArray(rawObj.gadgets, "gadgets", section);
9610
+ if (options.docker || options.dockerRo) {
9611
+ return true;
9348
9612
  }
9349
- if ("gadget-add" in rawObj) {
9350
- result["gadget-add"] = validateStringArray(rawObj["gadget-add"], "gadget-add", section);
9613
+ if (profileDocker !== void 0) {
9614
+ return profileDocker;
9351
9615
  }
9352
- if ("gadget-remove" in rawObj) {
9353
- result["gadget-remove"] = validateStringArray(rawObj["gadget-remove"], "gadget-remove", section);
9616
+ return config?.enabled ?? false;
9617
+ }
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
- if ("gadget" in rawObj) {
9356
- result.gadget = validateStringArray(rawObj.gadget, "gadget", section);
9625
+ const available = await checkDockerAvailable();
9626
+ if (!available) {
9627
+ throw new DockerUnavailableError();
9357
9628
  }
9358
- if ("builtins" in rawObj) {
9359
- result.builtins = validateBoolean(rawObj.builtins, "builtins", section);
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}`);
9360
9633
  }
9361
- if ("builtin-interaction" in rawObj) {
9362
- result["builtin-interaction"] = validateBoolean(
9363
- rawObj["builtin-interaction"],
9364
- "builtin-interaction",
9365
- section
9366
- );
9367
- }
9368
- if ("gadget-start-prefix" in rawObj) {
9369
- result["gadget-start-prefix"] = validateString(
9370
- rawObj["gadget-start-prefix"],
9371
- "gadget-start-prefix",
9372
- section
9373
- );
9374
- }
9375
- if ("gadget-end-prefix" in rawObj) {
9376
- result["gadget-end-prefix"] = validateString(
9377
- rawObj["gadget-end-prefix"],
9378
- "gadget-end-prefix",
9379
- section
9380
- );
9381
- }
9382
- if ("gadget-arg-prefix" in rawObj) {
9383
- result["gadget-arg-prefix"] = validateString(
9384
- rawObj["gadget-arg-prefix"],
9385
- "gadget-arg-prefix",
9386
- section
9387
- );
9388
- }
9389
- if ("gadget-approval" in rawObj) {
9390
- result["gadget-approval"] = validateGadgetApproval(rawObj["gadget-approval"], section);
9391
- }
9392
- if ("max-tokens" in rawObj) {
9393
- result["max-tokens"] = validateNumber(rawObj["max-tokens"], "max-tokens", section, {
9394
- integer: true,
9395
- min: 1
9396
- });
9397
- }
9398
- if ("quiet" in rawObj) {
9399
- result.quiet = validateBoolean(rawObj.quiet, "quiet", section);
9400
- }
9401
- Object.assign(result, validateLoggingConfig(rawObj, section));
9402
- return result;
9403
- }
9404
- function validatePromptsConfig(raw, section) {
9405
- if (typeof raw !== "object" || raw === null) {
9406
- throw new ConfigError(`[${section}] must be a table`);
9407
- }
9408
- const result = {};
9409
- for (const [key, value] of Object.entries(raw)) {
9410
- if (typeof value !== "string") {
9411
- throw new ConfigError(`[${section}].${key} must be a string`);
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;
9412
9641
  }
9413
- result[key] = value;
9642
+ throw error;
9414
9643
  }
9415
- 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);
9416
9652
  }
9417
- function validateConfig(raw, configPath) {
9418
- if (typeof raw !== "object" || raw === null) {
9419
- 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;
9420
9668
  }
9421
- const rawObj = raw;
9422
- const result = {};
9423
- 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 });
9424
9676
  try {
9425
- if (key === "global") {
9426
- result.global = validateGlobalConfig(value, key);
9427
- } else if (key === "complete") {
9428
- result.complete = validateCompleteConfig(value, key);
9429
- } else if (key === "agent") {
9430
- result.agent = validateAgentConfig(value, key);
9431
- } else if (key === "prompts") {
9432
- result.prompts = validatePromptsConfig(value, key);
9433
- } else {
9434
- result[key] = validateCustomConfig(value, key);
9435
- }
9436
- } catch (error) {
9437
- if (error instanceof ConfigError) {
9438
- throw new ConfigError(error.message, configPath);
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
+ }
9439
9690
  }
9440
- throw error;
9691
+ } finally {
9692
+ rl.close();
9693
+ keyboard.restore();
9441
9694
  }
9442
- }
9443
- return result;
9695
+ };
9444
9696
  }
9445
- function loadConfig() {
9446
- const configPath = getConfigPath();
9447
- if (!(0, import_node_fs8.existsSync)(configPath)) {
9448
- return {};
9449
- }
9450
- let content;
9451
- try {
9452
- content = (0, import_node_fs8.readFileSync)(configPath, "utf-8");
9453
- } catch (error) {
9454
- throw new ConfigError(
9455
- `Failed to read config file: ${error instanceof Error ? error.message : "Unknown error"}`,
9456
- configPath
9457
- );
9458
- }
9459
- let raw;
9460
- try {
9461
- raw = (0, import_js_toml.load)(content);
9462
- } catch (error) {
9463
- throw new ConfigError(
9464
- `Invalid TOML syntax: ${error instanceof Error ? error.message : "Unknown error"}`,
9465
- configPath
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
9466
9720
  );
9721
+ try {
9722
+ await executeInDocker(ctx, devMode);
9723
+ } catch (error) {
9724
+ if (error instanceof Error && error.message === "SKIP_DOCKER") {
9725
+ } else {
9726
+ throw error;
9727
+ }
9728
+ }
9467
9729
  }
9468
- const validated = validateConfig(raw, configPath);
9469
- const inherited = resolveInheritance(validated, configPath);
9470
- return resolveTemplatesInConfig(inherited, configPath);
9471
- }
9472
- function getCustomCommandNames(config) {
9473
- const reserved = /* @__PURE__ */ new Set(["global", "complete", "agent", "prompts"]);
9474
- return Object.keys(config).filter((key) => !reserved.has(key));
9475
- }
9476
- function resolveTemplatesInConfig(config, configPath) {
9477
- const prompts = config.prompts ?? {};
9478
- const hasPrompts = Object.keys(prompts).length > 0;
9479
- let hasTemplates = false;
9480
- for (const [sectionName, section] of Object.entries(config)) {
9481
- if (sectionName === "global" || sectionName === "prompts") continue;
9482
- if (!section || typeof section !== "object") continue;
9483
- const sectionObj = section;
9484
- if (typeof sectionObj.system === "string" && hasTemplateSyntax(sectionObj.system)) {
9485
- hasTemplates = true;
9486
- break;
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);
9487
9740
  }
9488
9741
  }
9489
- for (const template of Object.values(prompts)) {
9490
- if (hasTemplateSyntax(template)) {
9491
- hasTemplates = true;
9492
- break;
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);
9493
9747
  }
9494
9748
  }
9495
- if (!hasPrompts && !hasTemplates) {
9496
- return config;
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
+ `));
9764
+ }
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
+ }
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);
9497
9797
  }
9498
- try {
9499
- validatePrompts(prompts, configPath);
9500
- } catch (error) {
9501
- if (error instanceof TemplateError) {
9502
- 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";
9503
9816
  }
9504
- throw error;
9505
9817
  }
9506
- 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) => {
9507
9829
  try {
9508
- validateEnvVars(template, name, configPath);
9509
- } catch (error) {
9510
- if (error instanceof TemplateError) {
9511
- throw new ConfigError(error.message, configPath);
9512
- }
9513
- 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);
9514
9834
  }
9515
- }
9516
- const eta = createTemplateEngine(prompts, configPath);
9517
- const result = { ...config };
9518
- for (const [sectionName, section] of Object.entries(config)) {
9519
- if (sectionName === "global" || sectionName === "prompts") continue;
9520
- if (!section || typeof section !== "object") continue;
9521
- const sectionObj = section;
9522
- if (typeof sectionObj.system === "string" && hasTemplateSyntax(sectionObj.system)) {
9523
- try {
9524
- validateEnvVars(sectionObj.system, void 0, configPath);
9525
- } catch (error) {
9526
- if (error instanceof TemplateError) {
9527
- 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);
9528
9929
  }
9529
- throw error;
9530
9930
  }
9531
- try {
9532
- const resolved = resolveTemplate(eta, sectionObj.system, {}, configPath);
9533
- result[sectionName] = {
9534
- ...sectionObj,
9535
- system: resolved
9536
- };
9537
- } catch (error) {
9538
- if (error instanceof TemplateError) {
9539
- 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
+ };
9540
9978
  }
9541
- throw error;
9979
+ return { action: "proceed" };
9542
9980
  }
9543
9981
  }
9982
+ });
9983
+ if (options.system) {
9984
+ builder.withSystem(options.system);
9544
9985
  }
9545
- return result;
9546
- }
9547
- function resolveGadgets(section, inheritedGadgets, sectionName, configPath) {
9548
- const hasGadgets = "gadgets" in section;
9549
- const hasGadgetLegacy = "gadget" in section;
9550
- const hasGadgetAdd = "gadget-add" in section;
9551
- const hasGadgetRemove = "gadget-remove" in section;
9552
- if (hasGadgetLegacy && !hasGadgets) {
9553
- console.warn(
9554
- `[config] Warning: [${sectionName}].gadget is deprecated, use 'gadgets' (plural) instead`
9555
- );
9986
+ if (options.maxIterations !== void 0) {
9987
+ builder.withMaxIterations(options.maxIterations);
9556
9988
  }
9557
- if ((hasGadgets || hasGadgetLegacy) && (hasGadgetAdd || hasGadgetRemove)) {
9558
- throw new ConfigError(
9559
- `[${sectionName}] Cannot use 'gadgets' with 'gadget-add'/'gadget-remove'. Use either full replacement (gadgets) OR modification (gadget-add/gadget-remove).`,
9560
- configPath
9561
- );
9989
+ if (options.temperature !== void 0) {
9990
+ builder.withTemperature(options.temperature);
9562
9991
  }
9563
- if (hasGadgets) {
9564
- return section.gadgets;
9992
+ const humanInputHandler = createHumanInputHandler(env, progress, keyboard);
9993
+ if (humanInputHandler) {
9994
+ builder.onHumanInput(humanInputHandler);
9565
9995
  }
9566
- if (hasGadgetLegacy) {
9567
- return section.gadget;
9996
+ builder.withSignal(abortController.signal);
9997
+ const gadgets = registry.getAll();
9998
+ if (gadgets.length > 0) {
9999
+ builder.withGadgets(...gadgets);
9568
10000
  }
9569
- let result = [...inheritedGadgets];
9570
- if (hasGadgetRemove) {
9571
- const toRemove = new Set(section["gadget-remove"]);
9572
- result = result.filter((g) => !toRemove.has(g));
10001
+ if (options.gadgetStartPrefix) {
10002
+ builder.withGadgetStartPrefix(options.gadgetStartPrefix);
9573
10003
  }
9574
- if (hasGadgetAdd) {
9575
- const toAdd = section["gadget-add"];
9576
- result.push(...toAdd);
10004
+ if (options.gadgetEndPrefix) {
10005
+ builder.withGadgetEndPrefix(options.gadgetEndPrefix);
9577
10006
  }
9578
- return result;
9579
- }
9580
- function resolveInheritance(config, configPath) {
9581
- const resolved = {};
9582
- const resolving = /* @__PURE__ */ new Set();
9583
- function resolveSection(name) {
9584
- if (name in resolved) {
9585
- 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 = "";
9586
10032
  }
9587
- if (resolving.has(name)) {
9588
- 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
+ }
9589
10054
  }
9590
- const section = config[name];
9591
- if (section === void 0 || typeof section !== "object") {
9592
- throw new ConfigError(`Cannot inherit from unknown section: ${name}`, configPath);
10055
+ } catch (error) {
10056
+ if (!isAbortError(error)) {
10057
+ throw error;
9593
10058
  }
9594
- resolving.add(name);
9595
- const sectionObj = section;
9596
- const inheritsRaw = sectionObj.inherits;
9597
- const inheritsList = inheritsRaw ? Array.isArray(inheritsRaw) ? inheritsRaw : [inheritsRaw] : [];
9598
- let merged = {};
9599
- for (const parent of inheritsList) {
9600
- const parentResolved = resolveSection(parent);
9601
- 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
+ `);
9602
10079
  }
9603
- const inheritedGadgets = merged.gadgets ?? [];
9604
- const {
9605
- inherits: _inherits,
9606
- gadgets: _gadgets,
9607
- gadget: _gadget,
9608
- "gadget-add": _gadgetAdd,
9609
- "gadget-remove": _gadgetRemove,
9610
- ...ownValues
9611
- } = sectionObj;
9612
- merged = { ...merged, ...ownValues };
9613
- const resolvedGadgets = resolveGadgets(sectionObj, inheritedGadgets, name, configPath);
9614
- if (resolvedGadgets.length > 0) {
9615
- 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;
9616
10150
  }
9617
- delete merged["gadget"];
9618
- delete merged["gadget-add"];
9619
- delete merged["gadget-remove"];
9620
- resolving.delete(name);
9621
- resolved[name] = merged;
9622
- return merged;
9623
10151
  }
9624
- for (const name of Object.keys(config)) {
9625
- 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);
9626
10158
  }
9627
- 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
+ );
9628
10173
  }
9629
10174
 
9630
10175
  // src/cli/gadget-command.ts
@@ -10314,7 +10859,11 @@ function createCommandEnvironment(baseEnv, config) {
10314
10859
  logFile: config["log-file"] ?? baseEnv.loggerConfig?.logFile,
10315
10860
  logReset: config["log-reset"] ?? baseEnv.loggerConfig?.logReset
10316
10861
  };
10317
- return createDefaultEnvironment(loggerConfig);
10862
+ return {
10863
+ ...baseEnv,
10864
+ loggerConfig,
10865
+ createLogger: createLoggerFactory(loggerConfig)
10866
+ };
10318
10867
  }
10319
10868
  function registerCustomCommand(program, name, config, env) {
10320
10869
  const type = config.type ?? "agent";
@@ -10390,7 +10939,12 @@ async function runCLI(overrides = {}) {
10390
10939
  logReset: globalOpts.logReset ?? config.global?.["log-reset"]
10391
10940
  };
10392
10941
  const defaultEnv = createDefaultEnvironment(loggerConfig);
10393
- const env = { ...defaultEnv, ...envOverrides };
10942
+ const env = {
10943
+ ...defaultEnv,
10944
+ ...envOverrides,
10945
+ // Pass Docker config from [docker] section
10946
+ dockerConfig: config.docker
10947
+ };
10394
10948
  const program = createProgram(env, config);
10395
10949
  await program.parseAsync(env.argv);
10396
10950
  }