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.js CHANGED
@@ -54,7 +54,11 @@ var OPTION_FLAGS = {
54
54
  logLlmResponses: "--log-llm-responses [dir]",
55
55
  noBuiltins: "--no-builtins",
56
56
  noBuiltinInteraction: "--no-builtin-interaction",
57
- quiet: "-q, --quiet"
57
+ quiet: "-q, --quiet",
58
+ docker: "--docker",
59
+ dockerRo: "--docker-ro",
60
+ noDocker: "--no-docker",
61
+ dockerDev: "--docker-dev"
58
62
  };
59
63
  var OPTION_DESCRIPTIONS = {
60
64
  model: "Model identifier, e.g. openai:gpt-5-nano or anthropic:claude-sonnet-4-5.",
@@ -70,7 +74,11 @@ var OPTION_DESCRIPTIONS = {
70
74
  logLlmResponses: "Save raw LLM responses as plain text. Optional dir, defaults to ~/.llmist/logs/responses/",
71
75
  noBuiltins: "Disable built-in gadgets (AskUser, TellUser).",
72
76
  noBuiltinInteraction: "Disable interactive gadgets (AskUser) while keeping TellUser.",
73
- quiet: "Suppress all output except content (text and TellUser messages)."
77
+ quiet: "Suppress all output except content (text and TellUser messages).",
78
+ docker: "Run agent in a Docker sandbox container for security isolation.",
79
+ dockerRo: "Run in Docker with current directory mounted read-only.",
80
+ noDocker: "Disable Docker sandboxing (override config).",
81
+ dockerDev: "Run in Docker dev mode (mount local source instead of npm install)."
74
82
  };
75
83
  var SUMMARY_PREFIX = "[llmist]";
76
84
 
@@ -80,7 +88,7 @@ import { Command, InvalidArgumentError as InvalidArgumentError2 } from "commande
80
88
  // package.json
81
89
  var package_default = {
82
90
  name: "llmist",
83
- version: "1.4.0",
91
+ version: "1.5.0",
84
92
  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.",
85
93
  type: "module",
86
94
  main: "dist/index.cjs",
@@ -1830,7 +1838,7 @@ function addAgentOptions(cmd, defaults) {
1830
1838
  OPTION_FLAGS.noBuiltinInteraction,
1831
1839
  OPTION_DESCRIPTIONS.noBuiltinInteraction,
1832
1840
  defaults?.["builtin-interaction"] !== false
1833
- ).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"]);
1841
+ ).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);
1834
1842
  }
1835
1843
  function configToCompleteOptions(config) {
1836
1844
  const result = {};
@@ -1865,668 +1873,791 @@ function configToAgentOptions(config) {
1865
1873
  if (config.quiet !== void 0) result.quiet = config.quiet;
1866
1874
  if (config["log-llm-requests"] !== void 0) result.logLlmRequests = config["log-llm-requests"];
1867
1875
  if (config["log-llm-responses"] !== void 0) result.logLlmResponses = config["log-llm-responses"];
1876
+ if (config.docker !== void 0) result.docker = config.docker;
1877
+ if (config["docker-cwd-permission"] !== void 0)
1878
+ result.dockerCwdPermission = config["docker-cwd-permission"];
1868
1879
  return result;
1869
1880
  }
1870
1881
 
1871
- // src/cli/agent-command.ts
1872
- function createHumanInputHandler(env, progress, keyboard) {
1873
- const stdout = env.stdout;
1874
- if (!isInteractive(env.stdin) || typeof stdout.isTTY !== "boolean" || !stdout.isTTY) {
1875
- return void 0;
1882
+ // src/cli/docker/types.ts
1883
+ var VALID_MOUNT_PERMISSIONS = ["ro", "rw"];
1884
+ var DOCKER_CONFIG_KEYS = /* @__PURE__ */ new Set([
1885
+ "enabled",
1886
+ "dockerfile",
1887
+ "cwd-permission",
1888
+ "config-permission",
1889
+ "mounts",
1890
+ "env-vars",
1891
+ "image-name",
1892
+ "dev-mode",
1893
+ "dev-source"
1894
+ ]);
1895
+ var DEFAULT_IMAGE_NAME = "llmist-sandbox";
1896
+ var DEFAULT_CWD_PERMISSION = "rw";
1897
+ var DEFAULT_CONFIG_PERMISSION = "ro";
1898
+ var FORWARDED_API_KEYS = [
1899
+ "ANTHROPIC_API_KEY",
1900
+ "OPENAI_API_KEY",
1901
+ "GEMINI_API_KEY"
1902
+ ];
1903
+ var DEV_IMAGE_NAME = "llmist-dev-sandbox";
1904
+ var DEV_SOURCE_MOUNT_TARGET = "/llmist-src";
1905
+
1906
+ // src/cli/config.ts
1907
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
1908
+ import { homedir as homedir2 } from "node:os";
1909
+ import { join as join2 } from "node:path";
1910
+ import { load as parseToml } from "js-toml";
1911
+
1912
+ // src/cli/templates.ts
1913
+ import { Eta } from "eta";
1914
+ var TemplateError = class extends Error {
1915
+ constructor(message, promptName, configPath) {
1916
+ super(promptName ? `[prompts.${promptName}]: ${message}` : message);
1917
+ this.promptName = promptName;
1918
+ this.configPath = configPath;
1919
+ this.name = "TemplateError";
1876
1920
  }
1877
- return async (question) => {
1878
- progress.pause();
1879
- if (keyboard.cleanupEsc) {
1880
- keyboard.cleanupEsc();
1881
- keyboard.cleanupEsc = null;
1921
+ };
1922
+ function createTemplateEngine(prompts, configPath) {
1923
+ const eta = new Eta({
1924
+ views: "/",
1925
+ // Required but we use named templates
1926
+ autoEscape: false,
1927
+ // Don't escape - these are prompts, not HTML
1928
+ autoTrim: false
1929
+ // Preserve whitespace in prompts
1930
+ });
1931
+ for (const [name, template] of Object.entries(prompts)) {
1932
+ try {
1933
+ eta.loadTemplate(`@${name}`, template);
1934
+ } catch (error) {
1935
+ throw new TemplateError(
1936
+ error instanceof Error ? error.message : String(error),
1937
+ name,
1938
+ configPath
1939
+ );
1882
1940
  }
1883
- const rl = createInterface2({ input: env.stdin, output: env.stdout });
1941
+ }
1942
+ return eta;
1943
+ }
1944
+ function resolveTemplate(eta, template, context = {}, configPath) {
1945
+ try {
1946
+ const fullContext = {
1947
+ ...context,
1948
+ env: process.env,
1949
+ date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
1950
+ // "2025-12-01"
1951
+ };
1952
+ return eta.renderString(template, fullContext);
1953
+ } catch (error) {
1954
+ throw new TemplateError(
1955
+ error instanceof Error ? error.message : String(error),
1956
+ void 0,
1957
+ configPath
1958
+ );
1959
+ }
1960
+ }
1961
+ function validatePrompts(prompts, configPath) {
1962
+ const eta = createTemplateEngine(prompts, configPath);
1963
+ for (const [name, template] of Object.entries(prompts)) {
1884
1964
  try {
1885
- const questionLine = question.trim() ? `
1886
- ${renderMarkdownWithSeparators(question.trim())}` : "";
1887
- let isFirst = true;
1888
- while (true) {
1889
- const statsPrompt = progress.formatPrompt();
1890
- const prompt = isFirst ? `${questionLine}
1891
- ${statsPrompt}` : statsPrompt;
1892
- isFirst = false;
1893
- const answer = await rl.question(prompt);
1894
- const trimmed = answer.trim();
1895
- if (trimmed) {
1896
- return trimmed;
1897
- }
1898
- }
1899
- } finally {
1900
- rl.close();
1901
- keyboard.restore();
1965
+ eta.renderString(template, { env: {} });
1966
+ } catch (error) {
1967
+ throw new TemplateError(
1968
+ error instanceof Error ? error.message : String(error),
1969
+ name,
1970
+ configPath
1971
+ );
1902
1972
  }
1903
- };
1973
+ }
1904
1974
  }
1905
- async function executeAgent(promptArg, options, env) {
1906
- const prompt = await resolvePrompt(promptArg, env);
1907
- const client = env.createClient();
1908
- const registry = new GadgetRegistry();
1909
- const stdinIsInteractive = isInteractive(env.stdin);
1910
- if (options.builtins !== false) {
1911
- for (const gadget of builtinGadgets) {
1912
- if (gadget.name === "AskUser" && (options.builtinInteraction === false || !stdinIsInteractive)) {
1913
- continue;
1914
- }
1915
- registry.registerByClass(gadget);
1975
+ function validateEnvVars(template, promptName, configPath) {
1976
+ const envVarPattern = /<%=\s*it\.env\.(\w+)\s*%>/g;
1977
+ const matches = template.matchAll(envVarPattern);
1978
+ for (const match of matches) {
1979
+ const varName = match[1];
1980
+ if (process.env[varName] === void 0) {
1981
+ throw new TemplateError(
1982
+ `Environment variable '${varName}' is not set`,
1983
+ promptName,
1984
+ configPath
1985
+ );
1916
1986
  }
1917
1987
  }
1918
- const gadgetSpecifiers = options.gadget ?? [];
1919
- if (gadgetSpecifiers.length > 0) {
1920
- const gadgets2 = await loadGadgets(gadgetSpecifiers, process.cwd());
1921
- for (const gadget of gadgets2) {
1922
- registry.registerByClass(gadget);
1988
+ }
1989
+ function hasTemplateSyntax(str) {
1990
+ return str.includes("<%");
1991
+ }
1992
+
1993
+ // src/cli/config.ts
1994
+ var VALID_APPROVAL_MODES = ["allowed", "denied", "approval-required"];
1995
+ var GLOBAL_CONFIG_KEYS = /* @__PURE__ */ new Set(["log-level", "log-file", "log-reset"]);
1996
+ var VALID_LOG_LEVELS = ["silly", "trace", "debug", "info", "warn", "error", "fatal"];
1997
+ var COMPLETE_CONFIG_KEYS = /* @__PURE__ */ new Set([
1998
+ "model",
1999
+ "system",
2000
+ "temperature",
2001
+ "max-tokens",
2002
+ "quiet",
2003
+ "inherits",
2004
+ "log-level",
2005
+ "log-file",
2006
+ "log-reset",
2007
+ "log-llm-requests",
2008
+ "log-llm-responses",
2009
+ "type",
2010
+ // Allowed for inheritance compatibility, ignored for built-in commands
2011
+ "docker",
2012
+ // Enable Docker sandboxing (only effective for agent type)
2013
+ "docker-cwd-permission"
2014
+ // Override CWD mount permission for this profile
2015
+ ]);
2016
+ var AGENT_CONFIG_KEYS = /* @__PURE__ */ new Set([
2017
+ "model",
2018
+ "system",
2019
+ "temperature",
2020
+ "max-iterations",
2021
+ "gadgets",
2022
+ // Full replacement (preferred)
2023
+ "gadget-add",
2024
+ // Add to inherited gadgets
2025
+ "gadget-remove",
2026
+ // Remove from inherited gadgets
2027
+ "gadget",
2028
+ // DEPRECATED: alias for gadgets
2029
+ "builtins",
2030
+ "builtin-interaction",
2031
+ "gadget-start-prefix",
2032
+ "gadget-end-prefix",
2033
+ "gadget-arg-prefix",
2034
+ "gadget-approval",
2035
+ "quiet",
2036
+ "inherits",
2037
+ "log-level",
2038
+ "log-file",
2039
+ "log-reset",
2040
+ "log-llm-requests",
2041
+ "log-llm-responses",
2042
+ "type",
2043
+ // Allowed for inheritance compatibility, ignored for built-in commands
2044
+ "docker",
2045
+ // Enable Docker sandboxing for this profile
2046
+ "docker-cwd-permission"
2047
+ // Override CWD mount permission for this profile
2048
+ ]);
2049
+ var CUSTOM_CONFIG_KEYS = /* @__PURE__ */ new Set([
2050
+ ...COMPLETE_CONFIG_KEYS,
2051
+ ...AGENT_CONFIG_KEYS,
2052
+ "type",
2053
+ "description"
2054
+ ]);
2055
+ function getConfigPath() {
2056
+ return join2(homedir2(), ".llmist", "cli.toml");
2057
+ }
2058
+ var ConfigError = class extends Error {
2059
+ constructor(message, path5) {
2060
+ super(path5 ? `${path5}: ${message}` : message);
2061
+ this.path = path5;
2062
+ this.name = "ConfigError";
2063
+ }
2064
+ };
2065
+ function validateString(value, key, section) {
2066
+ if (typeof value !== "string") {
2067
+ throw new ConfigError(`[${section}].${key} must be a string`);
2068
+ }
2069
+ return value;
2070
+ }
2071
+ function validateNumber(value, key, section, opts) {
2072
+ if (typeof value !== "number") {
2073
+ throw new ConfigError(`[${section}].${key} must be a number`);
2074
+ }
2075
+ if (opts?.integer && !Number.isInteger(value)) {
2076
+ throw new ConfigError(`[${section}].${key} must be an integer`);
2077
+ }
2078
+ if (opts?.min !== void 0 && value < opts.min) {
2079
+ throw new ConfigError(`[${section}].${key} must be >= ${opts.min}`);
2080
+ }
2081
+ if (opts?.max !== void 0 && value > opts.max) {
2082
+ throw new ConfigError(`[${section}].${key} must be <= ${opts.max}`);
2083
+ }
2084
+ return value;
2085
+ }
2086
+ function validateBoolean(value, key, section) {
2087
+ if (typeof value !== "boolean") {
2088
+ throw new ConfigError(`[${section}].${key} must be a boolean`);
2089
+ }
2090
+ return value;
2091
+ }
2092
+ function validateStringArray(value, key, section) {
2093
+ if (!Array.isArray(value)) {
2094
+ throw new ConfigError(`[${section}].${key} must be an array`);
2095
+ }
2096
+ for (let i = 0; i < value.length; i++) {
2097
+ if (typeof value[i] !== "string") {
2098
+ throw new ConfigError(`[${section}].${key}[${i}] must be a string`);
1923
2099
  }
1924
2100
  }
1925
- const printer = new StreamPrinter(env.stdout);
1926
- const stderrTTY = env.stderr.isTTY === true;
1927
- const progress = new StreamProgress(env.stderr, stderrTTY, client.modelRegistry);
1928
- const abortController = new AbortController();
1929
- let wasCancelled = false;
1930
- let isStreaming = false;
1931
- const stdinStream = env.stdin;
1932
- const handleCancel = () => {
1933
- if (!abortController.signal.aborted) {
1934
- wasCancelled = true;
1935
- abortController.abort();
1936
- progress.pause();
1937
- env.stderr.write(chalk5.yellow(`
1938
- [Cancelled] ${progress.formatStats()}
1939
- `));
2101
+ return value;
2102
+ }
2103
+ function validateInherits(value, section) {
2104
+ if (typeof value === "string") {
2105
+ return value;
2106
+ }
2107
+ if (Array.isArray(value)) {
2108
+ for (let i = 0; i < value.length; i++) {
2109
+ if (typeof value[i] !== "string") {
2110
+ throw new ConfigError(`[${section}].inherits[${i}] must be a string`);
2111
+ }
1940
2112
  }
1941
- };
1942
- const keyboard = {
1943
- cleanupEsc: null,
1944
- cleanupSigint: null,
1945
- restore: () => {
1946
- if (stdinIsInteractive && stdinStream.isTTY && !wasCancelled) {
1947
- keyboard.cleanupEsc = createEscKeyListener(stdinStream, handleCancel);
1948
- }
2113
+ return value;
2114
+ }
2115
+ throw new ConfigError(`[${section}].inherits must be a string or array of strings`);
2116
+ }
2117
+ function validateGadgetApproval(value, section) {
2118
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
2119
+ throw new ConfigError(
2120
+ `[${section}].gadget-approval must be a table (e.g., { WriteFile = "approval-required" })`
2121
+ );
2122
+ }
2123
+ const result = {};
2124
+ for (const [gadgetName, mode] of Object.entries(value)) {
2125
+ if (typeof mode !== "string") {
2126
+ throw new ConfigError(
2127
+ `[${section}].gadget-approval.${gadgetName} must be a string`
2128
+ );
1949
2129
  }
1950
- };
1951
- const handleQuit = () => {
1952
- keyboard.cleanupEsc?.();
1953
- keyboard.cleanupSigint?.();
1954
- progress.complete();
1955
- printer.ensureNewline();
1956
- const summary = renderOverallSummary({
1957
- totalTokens: usage?.totalTokens,
1958
- iterations,
1959
- elapsedSeconds: progress.getTotalElapsedSeconds(),
1960
- cost: progress.getTotalCost()
2130
+ if (!VALID_APPROVAL_MODES.includes(mode)) {
2131
+ throw new ConfigError(
2132
+ `[${section}].gadget-approval.${gadgetName} must be one of: ${VALID_APPROVAL_MODES.join(", ")}`
2133
+ );
2134
+ }
2135
+ result[gadgetName] = mode;
2136
+ }
2137
+ return result;
2138
+ }
2139
+ function validateLoggingConfig(raw, section) {
2140
+ const result = {};
2141
+ if ("log-level" in raw) {
2142
+ const level = validateString(raw["log-level"], "log-level", section);
2143
+ if (!VALID_LOG_LEVELS.includes(level)) {
2144
+ throw new ConfigError(
2145
+ `[${section}].log-level must be one of: ${VALID_LOG_LEVELS.join(", ")}`
2146
+ );
2147
+ }
2148
+ result["log-level"] = level;
2149
+ }
2150
+ if ("log-file" in raw) {
2151
+ result["log-file"] = validateString(raw["log-file"], "log-file", section);
2152
+ }
2153
+ if ("log-reset" in raw) {
2154
+ result["log-reset"] = validateBoolean(raw["log-reset"], "log-reset", section);
2155
+ }
2156
+ return result;
2157
+ }
2158
+ function validateBaseConfig(raw, section) {
2159
+ const result = {};
2160
+ if ("model" in raw) {
2161
+ result.model = validateString(raw.model, "model", section);
2162
+ }
2163
+ if ("system" in raw) {
2164
+ result.system = validateString(raw.system, "system", section);
2165
+ }
2166
+ if ("temperature" in raw) {
2167
+ result.temperature = validateNumber(raw.temperature, "temperature", section, {
2168
+ min: 0,
2169
+ max: 2
1961
2170
  });
1962
- if (summary) {
1963
- env.stderr.write(`${chalk5.dim("\u2500".repeat(40))}
1964
- `);
1965
- env.stderr.write(`${summary}
1966
- `);
2171
+ }
2172
+ if ("inherits" in raw) {
2173
+ result.inherits = validateInherits(raw.inherits, section);
2174
+ }
2175
+ if ("docker" in raw) {
2176
+ result.docker = validateBoolean(raw.docker, "docker", section);
2177
+ }
2178
+ if ("docker-cwd-permission" in raw) {
2179
+ const perm = validateString(raw["docker-cwd-permission"], "docker-cwd-permission", section);
2180
+ if (perm !== "ro" && perm !== "rw") {
2181
+ throw new ConfigError(`[${section}].docker-cwd-permission must be "ro" or "rw"`);
1967
2182
  }
1968
- env.stderr.write(chalk5.dim("[Quit]\n"));
1969
- process.exit(130);
1970
- };
1971
- if (stdinIsInteractive && stdinStream.isTTY) {
1972
- keyboard.cleanupEsc = createEscKeyListener(stdinStream, handleCancel);
2183
+ result["docker-cwd-permission"] = perm;
1973
2184
  }
1974
- keyboard.cleanupSigint = createSigintListener(
1975
- handleCancel,
1976
- handleQuit,
1977
- () => isStreaming && !abortController.signal.aborted,
1978
- env.stderr
1979
- );
1980
- const DEFAULT_APPROVAL_REQUIRED = ["RunCommand", "WriteFile", "EditFile"];
1981
- const userApprovals = options.gadgetApproval ?? {};
1982
- const gadgetApprovals = {
1983
- ...userApprovals
1984
- };
1985
- for (const gadget of DEFAULT_APPROVAL_REQUIRED) {
1986
- const normalizedGadget = gadget.toLowerCase();
1987
- const isConfigured = Object.keys(userApprovals).some(
1988
- (key) => key.toLowerCase() === normalizedGadget
1989
- );
1990
- if (!isConfigured) {
1991
- gadgetApprovals[gadget] = "approval-required";
2185
+ return result;
2186
+ }
2187
+ function validateGlobalConfig(raw, section) {
2188
+ if (typeof raw !== "object" || raw === null) {
2189
+ throw new ConfigError(`[${section}] must be a table`);
2190
+ }
2191
+ const rawObj = raw;
2192
+ for (const key of Object.keys(rawObj)) {
2193
+ if (!GLOBAL_CONFIG_KEYS.has(key)) {
2194
+ throw new ConfigError(`[${section}].${key} is not a valid option`);
1992
2195
  }
1993
2196
  }
1994
- const approvalConfig = {
1995
- gadgetApprovals,
1996
- defaultMode: "allowed"
1997
- };
1998
- const approvalManager = new ApprovalManager(approvalConfig, env, progress);
1999
- let usage;
2000
- let iterations = 0;
2001
- const llmRequestsDir = resolveLogDir(options.logLlmRequests, "requests");
2002
- const llmResponsesDir = resolveLogDir(options.logLlmResponses, "responses");
2003
- let llmCallCounter = 0;
2004
- const countMessagesTokens = async (model, messages) => {
2005
- try {
2006
- return await client.countTokens(model, messages);
2007
- } catch {
2008
- const totalChars = messages.reduce((sum, m) => sum + (m.content?.length ?? 0), 0);
2009
- return Math.round(totalChars / FALLBACK_CHARS_PER_TOKEN);
2197
+ return validateLoggingConfig(rawObj, section);
2198
+ }
2199
+ function validateCompleteConfig(raw, section) {
2200
+ if (typeof raw !== "object" || raw === null) {
2201
+ throw new ConfigError(`[${section}] must be a table`);
2202
+ }
2203
+ const rawObj = raw;
2204
+ for (const key of Object.keys(rawObj)) {
2205
+ if (!COMPLETE_CONFIG_KEYS.has(key)) {
2206
+ throw new ConfigError(`[${section}].${key} is not a valid option`);
2010
2207
  }
2208
+ }
2209
+ const result = {
2210
+ ...validateBaseConfig(rawObj, section),
2211
+ ...validateLoggingConfig(rawObj, section)
2011
2212
  };
2012
- const countGadgetOutputTokens = async (output) => {
2013
- if (!output) return void 0;
2014
- try {
2015
- const messages = [{ role: "assistant", content: output }];
2016
- return await client.countTokens(options.model, messages);
2017
- } catch {
2018
- return void 0;
2213
+ if ("max-tokens" in rawObj) {
2214
+ result["max-tokens"] = validateNumber(rawObj["max-tokens"], "max-tokens", section, {
2215
+ integer: true,
2216
+ min: 1
2217
+ });
2218
+ }
2219
+ if ("quiet" in rawObj) {
2220
+ result.quiet = validateBoolean(rawObj.quiet, "quiet", section);
2221
+ }
2222
+ if ("log-llm-requests" in rawObj) {
2223
+ result["log-llm-requests"] = validateStringOrBoolean(
2224
+ rawObj["log-llm-requests"],
2225
+ "log-llm-requests",
2226
+ section
2227
+ );
2228
+ }
2229
+ if ("log-llm-responses" in rawObj) {
2230
+ result["log-llm-responses"] = validateStringOrBoolean(
2231
+ rawObj["log-llm-responses"],
2232
+ "log-llm-responses",
2233
+ section
2234
+ );
2235
+ }
2236
+ return result;
2237
+ }
2238
+ function validateAgentConfig(raw, section) {
2239
+ if (typeof raw !== "object" || raw === null) {
2240
+ throw new ConfigError(`[${section}] must be a table`);
2241
+ }
2242
+ const rawObj = raw;
2243
+ for (const key of Object.keys(rawObj)) {
2244
+ if (!AGENT_CONFIG_KEYS.has(key)) {
2245
+ throw new ConfigError(`[${section}].${key} is not a valid option`);
2019
2246
  }
2247
+ }
2248
+ const result = {
2249
+ ...validateBaseConfig(rawObj, section),
2250
+ ...validateLoggingConfig(rawObj, section)
2020
2251
  };
2021
- const builder = new AgentBuilder(client).withModel(options.model).withLogger(env.createLogger("llmist:cli:agent")).withHooks({
2022
- observers: {
2023
- // onLLMCallStart: Start progress indicator for each LLM call
2024
- // This showcases how to react to agent lifecycle events
2025
- onLLMCallStart: async (context) => {
2026
- isStreaming = true;
2027
- llmCallCounter++;
2028
- const inputTokens = await countMessagesTokens(
2029
- context.options.model,
2030
- context.options.messages
2031
- );
2032
- progress.startCall(context.options.model, inputTokens);
2033
- progress.setInputTokens(inputTokens, false);
2034
- if (llmRequestsDir) {
2035
- const filename = `${Date.now()}_call_${llmCallCounter}.request.txt`;
2036
- const content = formatLlmRequest(context.options.messages);
2037
- await writeLogFile(llmRequestsDir, filename, content);
2038
- }
2039
- },
2040
- // onStreamChunk: Real-time updates as LLM generates tokens
2041
- // This enables responsive UIs that show progress during generation
2042
- onStreamChunk: async (context) => {
2043
- progress.update(context.accumulatedText.length);
2044
- if (context.usage) {
2045
- if (context.usage.inputTokens) {
2046
- progress.setInputTokens(context.usage.inputTokens, false);
2047
- }
2048
- if (context.usage.outputTokens) {
2049
- progress.setOutputTokens(context.usage.outputTokens, false);
2050
- }
2051
- progress.setCachedTokens(
2052
- context.usage.cachedInputTokens ?? 0,
2053
- context.usage.cacheCreationInputTokens ?? 0
2054
- );
2055
- }
2056
- },
2057
- // onLLMCallComplete: Finalize metrics after each LLM call
2058
- // This is where you'd typically log metrics or update dashboards
2059
- onLLMCallComplete: async (context) => {
2060
- isStreaming = false;
2061
- usage = context.usage;
2062
- iterations = Math.max(iterations, context.iteration + 1);
2063
- if (context.usage) {
2064
- if (context.usage.inputTokens) {
2065
- progress.setInputTokens(context.usage.inputTokens, false);
2066
- }
2067
- if (context.usage.outputTokens) {
2068
- progress.setOutputTokens(context.usage.outputTokens, false);
2069
- }
2070
- }
2071
- let callCost;
2072
- if (context.usage && client.modelRegistry) {
2073
- try {
2074
- const modelName = context.options.model.includes(":") ? context.options.model.split(":")[1] : context.options.model;
2075
- const costResult = client.modelRegistry.estimateCost(
2076
- modelName,
2077
- context.usage.inputTokens,
2078
- context.usage.outputTokens,
2079
- context.usage.cachedInputTokens ?? 0,
2080
- context.usage.cacheCreationInputTokens ?? 0
2081
- );
2082
- if (costResult) callCost = costResult.totalCost;
2083
- } catch {
2084
- }
2085
- }
2086
- const callElapsed = progress.getCallElapsedSeconds();
2087
- progress.endCall(context.usage);
2088
- if (!options.quiet) {
2089
- const summary = renderSummary({
2090
- iterations: context.iteration + 1,
2091
- model: options.model,
2092
- usage: context.usage,
2093
- elapsedSeconds: callElapsed,
2094
- cost: callCost,
2095
- finishReason: context.finishReason
2096
- });
2097
- if (summary) {
2098
- env.stderr.write(`${summary}
2099
- `);
2100
- }
2101
- }
2102
- if (llmResponsesDir) {
2103
- const filename = `${Date.now()}_call_${llmCallCounter}.response.txt`;
2104
- await writeLogFile(llmResponsesDir, filename, context.rawResponse);
2105
- }
2106
- }
2107
- },
2108
- // SHOWCASE: Controller-based approval gating for gadgets
2109
- //
2110
- // This demonstrates how to add safety layers WITHOUT modifying gadgets.
2111
- // The ApprovalManager handles approval flows externally via beforeGadgetExecution.
2112
- // Approval modes are configurable via cli.toml:
2113
- // - "allowed": auto-proceed
2114
- // - "denied": auto-reject, return message to LLM
2115
- // - "approval-required": prompt user interactively
2116
- //
2117
- // Default: RunCommand, WriteFile, EditFile require approval unless overridden.
2118
- controllers: {
2119
- beforeGadgetExecution: async (ctx) => {
2120
- const mode = approvalManager.getApprovalMode(ctx.gadgetName);
2121
- if (mode === "allowed") {
2122
- return { action: "proceed" };
2123
- }
2124
- const stdinTTY = isInteractive(env.stdin);
2125
- const stderrTTY2 = env.stderr.isTTY === true;
2126
- const canPrompt = stdinTTY && stderrTTY2;
2127
- if (!canPrompt) {
2128
- if (mode === "approval-required") {
2129
- return {
2130
- action: "skip",
2131
- syntheticResult: `status=denied
2132
-
2133
- ${ctx.gadgetName} requires interactive approval. Run in a terminal to approve.`
2134
- };
2135
- }
2136
- if (mode === "denied") {
2137
- return {
2138
- action: "skip",
2139
- syntheticResult: `status=denied
2140
-
2141
- ${ctx.gadgetName} is denied by configuration.`
2142
- };
2143
- }
2144
- return { action: "proceed" };
2145
- }
2146
- const result = await approvalManager.requestApproval(ctx.gadgetName, ctx.parameters);
2147
- if (!result.approved) {
2148
- return {
2149
- action: "skip",
2150
- syntheticResult: `status=denied
2151
-
2152
- Denied: ${result.reason ?? "by user"}`
2153
- };
2154
- }
2155
- return { action: "proceed" };
2156
- }
2157
- }
2158
- });
2159
- if (options.system) {
2160
- builder.withSystem(options.system);
2252
+ if ("max-iterations" in rawObj) {
2253
+ result["max-iterations"] = validateNumber(rawObj["max-iterations"], "max-iterations", section, {
2254
+ integer: true,
2255
+ min: 1
2256
+ });
2161
2257
  }
2162
- if (options.maxIterations !== void 0) {
2163
- builder.withMaxIterations(options.maxIterations);
2258
+ if ("gadgets" in rawObj) {
2259
+ result.gadgets = validateStringArray(rawObj.gadgets, "gadgets", section);
2164
2260
  }
2165
- if (options.temperature !== void 0) {
2166
- builder.withTemperature(options.temperature);
2261
+ if ("gadget-add" in rawObj) {
2262
+ result["gadget-add"] = validateStringArray(rawObj["gadget-add"], "gadget-add", section);
2167
2263
  }
2168
- const humanInputHandler = createHumanInputHandler(env, progress, keyboard);
2169
- if (humanInputHandler) {
2170
- builder.onHumanInput(humanInputHandler);
2264
+ if ("gadget-remove" in rawObj) {
2265
+ result["gadget-remove"] = validateStringArray(rawObj["gadget-remove"], "gadget-remove", section);
2171
2266
  }
2172
- builder.withSignal(abortController.signal);
2173
- const gadgets = registry.getAll();
2174
- if (gadgets.length > 0) {
2175
- builder.withGadgets(...gadgets);
2267
+ if ("gadget" in rawObj) {
2268
+ result.gadget = validateStringArray(rawObj.gadget, "gadget", section);
2176
2269
  }
2177
- if (options.gadgetStartPrefix) {
2178
- builder.withGadgetStartPrefix(options.gadgetStartPrefix);
2270
+ if ("builtins" in rawObj) {
2271
+ result.builtins = validateBoolean(rawObj.builtins, "builtins", section);
2179
2272
  }
2180
- if (options.gadgetEndPrefix) {
2181
- builder.withGadgetEndPrefix(options.gadgetEndPrefix);
2273
+ if ("builtin-interaction" in rawObj) {
2274
+ result["builtin-interaction"] = validateBoolean(
2275
+ rawObj["builtin-interaction"],
2276
+ "builtin-interaction",
2277
+ section
2278
+ );
2182
2279
  }
2183
- if (options.gadgetArgPrefix) {
2184
- builder.withGadgetArgPrefix(options.gadgetArgPrefix);
2280
+ if ("gadget-start-prefix" in rawObj) {
2281
+ result["gadget-start-prefix"] = validateString(
2282
+ rawObj["gadget-start-prefix"],
2283
+ "gadget-start-prefix",
2284
+ section
2285
+ );
2185
2286
  }
2186
- builder.withSyntheticGadgetCall(
2187
- "TellUser",
2188
- {
2189
- 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?",
2190
- done: false,
2191
- type: "info"
2192
- },
2193
- "\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?"
2194
- );
2195
- builder.withTextOnlyHandler("acknowledge");
2196
- builder.withTextWithGadgetsHandler({
2197
- gadgetName: "TellUser",
2198
- parameterMapping: (text) => ({ message: text, done: false, type: "info" }),
2199
- resultMapping: (text) => `\u2139\uFE0F ${text}`
2200
- });
2201
- const agent = builder.ask(prompt);
2202
- let textBuffer = "";
2203
- const flushTextBuffer = () => {
2204
- if (textBuffer) {
2205
- const output = options.quiet ? textBuffer : renderMarkdownWithSeparators(textBuffer);
2206
- printer.write(output);
2207
- textBuffer = "";
2208
- }
2209
- };
2210
- try {
2211
- for await (const event of agent.run()) {
2212
- if (event.type === "text") {
2213
- progress.pause();
2214
- textBuffer += event.content;
2215
- } else if (event.type === "gadget_result") {
2216
- flushTextBuffer();
2217
- progress.pause();
2218
- if (options.quiet) {
2219
- if (event.result.gadgetName === "TellUser" && event.result.parameters?.message) {
2220
- const message = String(event.result.parameters.message);
2221
- env.stdout.write(`${message}
2222
- `);
2223
- }
2224
- } else {
2225
- const tokenCount = await countGadgetOutputTokens(event.result.result);
2226
- env.stderr.write(`${formatGadgetSummary({ ...event.result, tokenCount })}
2227
- `);
2228
- }
2229
- }
2230
- }
2231
- } catch (error) {
2232
- if (!isAbortError(error)) {
2233
- throw error;
2234
- }
2235
- } finally {
2236
- isStreaming = false;
2237
- keyboard.cleanupEsc?.();
2238
- keyboard.cleanupSigint?.();
2287
+ if ("gadget-end-prefix" in rawObj) {
2288
+ result["gadget-end-prefix"] = validateString(
2289
+ rawObj["gadget-end-prefix"],
2290
+ "gadget-end-prefix",
2291
+ section
2292
+ );
2239
2293
  }
2240
- flushTextBuffer();
2241
- progress.complete();
2242
- printer.ensureNewline();
2243
- if (!options.quiet && iterations > 1) {
2244
- env.stderr.write(`${chalk5.dim("\u2500".repeat(40))}
2245
- `);
2246
- const summary = renderOverallSummary({
2247
- totalTokens: usage?.totalTokens,
2248
- iterations,
2249
- elapsedSeconds: progress.getTotalElapsedSeconds(),
2250
- cost: progress.getTotalCost()
2251
- });
2252
- if (summary) {
2253
- env.stderr.write(`${summary}
2254
- `);
2255
- }
2294
+ if ("gadget-arg-prefix" in rawObj) {
2295
+ result["gadget-arg-prefix"] = validateString(
2296
+ rawObj["gadget-arg-prefix"],
2297
+ "gadget-arg-prefix",
2298
+ section
2299
+ );
2300
+ }
2301
+ if ("gadget-approval" in rawObj) {
2302
+ result["gadget-approval"] = validateGadgetApproval(rawObj["gadget-approval"], section);
2303
+ }
2304
+ if ("quiet" in rawObj) {
2305
+ result.quiet = validateBoolean(rawObj.quiet, "quiet", section);
2306
+ }
2307
+ if ("log-llm-requests" in rawObj) {
2308
+ result["log-llm-requests"] = validateStringOrBoolean(
2309
+ rawObj["log-llm-requests"],
2310
+ "log-llm-requests",
2311
+ section
2312
+ );
2313
+ }
2314
+ if ("log-llm-responses" in rawObj) {
2315
+ result["log-llm-responses"] = validateStringOrBoolean(
2316
+ rawObj["log-llm-responses"],
2317
+ "log-llm-responses",
2318
+ section
2319
+ );
2256
2320
  }
2321
+ return result;
2257
2322
  }
2258
- function registerAgentCommand(program, env, config) {
2259
- 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.");
2260
- addAgentOptions(cmd, config);
2261
- cmd.action(
2262
- (prompt, options) => executeAction(() => {
2263
- const mergedOptions = {
2264
- ...options,
2265
- gadgetApproval: config?.["gadget-approval"]
2266
- };
2267
- return executeAgent(prompt, mergedOptions, env);
2268
- }, env)
2269
- );
2323
+ function validateStringOrBoolean(value, field, section) {
2324
+ if (typeof value === "string" || typeof value === "boolean") {
2325
+ return value;
2326
+ }
2327
+ throw new ConfigError(`[${section}].${field} must be a string or boolean`);
2270
2328
  }
2271
-
2272
- // src/cli/complete-command.ts
2273
- init_messages();
2274
- init_model_shortcuts();
2275
- init_constants();
2276
- async function executeComplete(promptArg, options, env) {
2277
- const prompt = await resolvePrompt(promptArg, env);
2278
- const client = env.createClient();
2279
- const model = resolveModel(options.model);
2280
- const builder = new LLMMessageBuilder();
2281
- if (options.system) {
2282
- builder.addSystem(options.system);
2329
+ function validateCustomConfig(raw, section) {
2330
+ if (typeof raw !== "object" || raw === null) {
2331
+ throw new ConfigError(`[${section}] must be a table`);
2283
2332
  }
2284
- builder.addUser(prompt);
2285
- const messages = builder.build();
2286
- const llmRequestsDir = resolveLogDir(options.logLlmRequests, "requests");
2287
- const llmResponsesDir = resolveLogDir(options.logLlmResponses, "responses");
2288
- const timestamp = Date.now();
2289
- if (llmRequestsDir) {
2290
- const filename = `${timestamp}_complete.request.txt`;
2291
- const content = formatLlmRequest(messages);
2292
- await writeLogFile(llmRequestsDir, filename, content);
2293
- }
2294
- const stream = client.stream({
2295
- model,
2296
- messages,
2297
- temperature: options.temperature,
2298
- maxTokens: options.maxTokens
2299
- });
2300
- const printer = new StreamPrinter(env.stdout);
2301
- const stderrTTY = env.stderr.isTTY === true;
2302
- const progress = new StreamProgress(env.stderr, stderrTTY, client.modelRegistry);
2303
- const estimatedInputTokens = Math.round(prompt.length / FALLBACK_CHARS_PER_TOKEN);
2304
- progress.startCall(model, estimatedInputTokens);
2305
- let finishReason;
2306
- let usage;
2307
- let accumulatedResponse = "";
2308
- for await (const chunk of stream) {
2309
- if (chunk.usage) {
2310
- usage = chunk.usage;
2311
- if (chunk.usage.inputTokens) {
2312
- progress.setInputTokens(chunk.usage.inputTokens, false);
2313
- }
2314
- if (chunk.usage.outputTokens) {
2315
- progress.setOutputTokens(chunk.usage.outputTokens, false);
2316
- }
2317
- }
2318
- if (chunk.text) {
2319
- progress.pause();
2320
- accumulatedResponse += chunk.text;
2321
- progress.update(accumulatedResponse.length);
2322
- printer.write(chunk.text);
2333
+ const rawObj = raw;
2334
+ for (const key of Object.keys(rawObj)) {
2335
+ if (!CUSTOM_CONFIG_KEYS.has(key)) {
2336
+ throw new ConfigError(`[${section}].${key} is not a valid option`);
2323
2337
  }
2324
- if (chunk.finishReason !== void 0) {
2325
- finishReason = chunk.finishReason;
2338
+ }
2339
+ let type = "agent";
2340
+ if ("type" in rawObj) {
2341
+ const typeValue = validateString(rawObj.type, "type", section);
2342
+ if (typeValue !== "agent" && typeValue !== "complete") {
2343
+ throw new ConfigError(`[${section}].type must be "agent" or "complete"`);
2326
2344
  }
2345
+ type = typeValue;
2327
2346
  }
2328
- progress.endCall(usage);
2329
- progress.complete();
2330
- printer.ensureNewline();
2331
- if (llmResponsesDir) {
2332
- const filename = `${timestamp}_complete.response.txt`;
2333
- await writeLogFile(llmResponsesDir, filename, accumulatedResponse);
2347
+ const result = {
2348
+ ...validateBaseConfig(rawObj, section),
2349
+ type
2350
+ };
2351
+ if ("description" in rawObj) {
2352
+ result.description = validateString(rawObj.description, "description", section);
2334
2353
  }
2335
- if (stderrTTY && !options.quiet) {
2336
- const summary = renderSummary({ finishReason, usage, cost: progress.getTotalCost() });
2337
- if (summary) {
2338
- env.stderr.write(`${summary}
2339
- `);
2340
- }
2354
+ if ("max-iterations" in rawObj) {
2355
+ result["max-iterations"] = validateNumber(rawObj["max-iterations"], "max-iterations", section, {
2356
+ integer: true,
2357
+ min: 1
2358
+ });
2359
+ }
2360
+ if ("gadgets" in rawObj) {
2361
+ result.gadgets = validateStringArray(rawObj.gadgets, "gadgets", section);
2362
+ }
2363
+ if ("gadget-add" in rawObj) {
2364
+ result["gadget-add"] = validateStringArray(rawObj["gadget-add"], "gadget-add", section);
2365
+ }
2366
+ if ("gadget-remove" in rawObj) {
2367
+ result["gadget-remove"] = validateStringArray(rawObj["gadget-remove"], "gadget-remove", section);
2368
+ }
2369
+ if ("gadget" in rawObj) {
2370
+ result.gadget = validateStringArray(rawObj.gadget, "gadget", section);
2371
+ }
2372
+ if ("builtins" in rawObj) {
2373
+ result.builtins = validateBoolean(rawObj.builtins, "builtins", section);
2374
+ }
2375
+ if ("builtin-interaction" in rawObj) {
2376
+ result["builtin-interaction"] = validateBoolean(
2377
+ rawObj["builtin-interaction"],
2378
+ "builtin-interaction",
2379
+ section
2380
+ );
2381
+ }
2382
+ if ("gadget-start-prefix" in rawObj) {
2383
+ result["gadget-start-prefix"] = validateString(
2384
+ rawObj["gadget-start-prefix"],
2385
+ "gadget-start-prefix",
2386
+ section
2387
+ );
2388
+ }
2389
+ if ("gadget-end-prefix" in rawObj) {
2390
+ result["gadget-end-prefix"] = validateString(
2391
+ rawObj["gadget-end-prefix"],
2392
+ "gadget-end-prefix",
2393
+ section
2394
+ );
2395
+ }
2396
+ if ("gadget-arg-prefix" in rawObj) {
2397
+ result["gadget-arg-prefix"] = validateString(
2398
+ rawObj["gadget-arg-prefix"],
2399
+ "gadget-arg-prefix",
2400
+ section
2401
+ );
2402
+ }
2403
+ if ("gadget-approval" in rawObj) {
2404
+ result["gadget-approval"] = validateGadgetApproval(rawObj["gadget-approval"], section);
2405
+ }
2406
+ if ("max-tokens" in rawObj) {
2407
+ result["max-tokens"] = validateNumber(rawObj["max-tokens"], "max-tokens", section, {
2408
+ integer: true,
2409
+ min: 1
2410
+ });
2411
+ }
2412
+ if ("quiet" in rawObj) {
2413
+ result.quiet = validateBoolean(rawObj.quiet, "quiet", section);
2341
2414
  }
2415
+ Object.assign(result, validateLoggingConfig(rawObj, section));
2416
+ return result;
2342
2417
  }
2343
- function registerCompleteCommand(program, env, config) {
2344
- 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.");
2345
- addCompleteOptions(cmd, config);
2346
- cmd.action(
2347
- (prompt, options) => executeAction(() => executeComplete(prompt, options, env), env)
2348
- );
2418
+ function validatePromptsConfig(raw, section) {
2419
+ if (typeof raw !== "object" || raw === null) {
2420
+ throw new ConfigError(`[${section}] must be a table`);
2421
+ }
2422
+ const result = {};
2423
+ for (const [key, value] of Object.entries(raw)) {
2424
+ if (typeof value !== "string") {
2425
+ throw new ConfigError(`[${section}].${key} must be a string`);
2426
+ }
2427
+ result[key] = value;
2428
+ }
2429
+ return result;
2349
2430
  }
2350
-
2351
- // src/cli/config.ts
2352
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
2353
- import { homedir as homedir2 } from "node:os";
2354
- import { join as join2 } from "node:path";
2355
- import { load as parseToml } from "js-toml";
2356
-
2357
- // src/cli/templates.ts
2358
- import { Eta } from "eta";
2359
- var TemplateError = class extends Error {
2360
- constructor(message, promptName, configPath) {
2361
- super(promptName ? `[prompts.${promptName}]: ${message}` : message);
2362
- this.promptName = promptName;
2363
- this.configPath = configPath;
2364
- this.name = "TemplateError";
2431
+ function validateConfig(raw, configPath) {
2432
+ if (typeof raw !== "object" || raw === null) {
2433
+ throw new ConfigError("Config must be a TOML table", configPath);
2365
2434
  }
2366
- };
2367
- function createTemplateEngine(prompts, configPath) {
2368
- const eta = new Eta({
2369
- views: "/",
2370
- // Required but we use named templates
2371
- autoEscape: false,
2372
- // Don't escape - these are prompts, not HTML
2373
- autoTrim: false
2374
- // Preserve whitespace in prompts
2375
- });
2376
- for (const [name, template] of Object.entries(prompts)) {
2435
+ const rawObj = raw;
2436
+ const result = {};
2437
+ for (const [key, value] of Object.entries(rawObj)) {
2377
2438
  try {
2378
- eta.loadTemplate(`@${name}`, template);
2439
+ if (key === "global") {
2440
+ result.global = validateGlobalConfig(value, key);
2441
+ } else if (key === "complete") {
2442
+ result.complete = validateCompleteConfig(value, key);
2443
+ } else if (key === "agent") {
2444
+ result.agent = validateAgentConfig(value, key);
2445
+ } else if (key === "prompts") {
2446
+ result.prompts = validatePromptsConfig(value, key);
2447
+ } else if (key === "docker") {
2448
+ result.docker = validateDockerConfig(value, key);
2449
+ } else {
2450
+ result[key] = validateCustomConfig(value, key);
2451
+ }
2379
2452
  } catch (error) {
2380
- throw new TemplateError(
2381
- error instanceof Error ? error.message : String(error),
2382
- name,
2383
- configPath
2384
- );
2453
+ if (error instanceof ConfigError) {
2454
+ throw new ConfigError(error.message, configPath);
2455
+ }
2456
+ throw error;
2385
2457
  }
2386
2458
  }
2387
- return eta;
2459
+ return result;
2388
2460
  }
2389
- function resolveTemplate(eta, template, context = {}, configPath) {
2461
+ function loadConfig() {
2462
+ const configPath = getConfigPath();
2463
+ if (!existsSync2(configPath)) {
2464
+ return {};
2465
+ }
2466
+ let content;
2390
2467
  try {
2391
- const fullContext = {
2392
- ...context,
2393
- env: process.env,
2394
- date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
2395
- // "2025-12-01"
2396
- };
2397
- return eta.renderString(template, fullContext);
2468
+ content = readFileSync2(configPath, "utf-8");
2398
2469
  } catch (error) {
2399
- throw new TemplateError(
2400
- error instanceof Error ? error.message : String(error),
2401
- void 0,
2470
+ throw new ConfigError(
2471
+ `Failed to read config file: ${error instanceof Error ? error.message : "Unknown error"}`,
2402
2472
  configPath
2403
2473
  );
2404
2474
  }
2405
- }
2406
- function validatePrompts(prompts, configPath) {
2407
- const eta = createTemplateEngine(prompts, configPath);
2408
- for (const [name, template] of Object.entries(prompts)) {
2409
- try {
2410
- eta.renderString(template, { env: {} });
2411
- } catch (error) {
2412
- throw new TemplateError(
2413
- error instanceof Error ? error.message : String(error),
2414
- name,
2415
- configPath
2416
- );
2417
- }
2475
+ let raw;
2476
+ try {
2477
+ raw = parseToml(content);
2478
+ } catch (error) {
2479
+ throw new ConfigError(
2480
+ `Invalid TOML syntax: ${error instanceof Error ? error.message : "Unknown error"}`,
2481
+ configPath
2482
+ );
2418
2483
  }
2484
+ const validated = validateConfig(raw, configPath);
2485
+ const inherited = resolveInheritance(validated, configPath);
2486
+ return resolveTemplatesInConfig(inherited, configPath);
2419
2487
  }
2420
- function validateEnvVars(template, promptName, configPath) {
2421
- const envVarPattern = /<%=\s*it\.env\.(\w+)\s*%>/g;
2422
- const matches = template.matchAll(envVarPattern);
2423
- for (const match of matches) {
2424
- const varName = match[1];
2425
- if (process.env[varName] === void 0) {
2426
- throw new TemplateError(
2427
- `Environment variable '${varName}' is not set`,
2428
- promptName,
2429
- configPath
2430
- );
2431
- }
2432
- }
2433
- }
2434
- function hasTemplateSyntax(str) {
2435
- return str.includes("<%");
2436
- }
2437
-
2438
- // src/cli/config.ts
2439
- var VALID_APPROVAL_MODES = ["allowed", "denied", "approval-required"];
2440
- var GLOBAL_CONFIG_KEYS = /* @__PURE__ */ new Set(["log-level", "log-file", "log-reset"]);
2441
- var VALID_LOG_LEVELS = ["silly", "trace", "debug", "info", "warn", "error", "fatal"];
2442
- var COMPLETE_CONFIG_KEYS = /* @__PURE__ */ new Set([
2443
- "model",
2444
- "system",
2445
- "temperature",
2446
- "max-tokens",
2447
- "quiet",
2448
- "inherits",
2449
- "log-level",
2450
- "log-file",
2451
- "log-reset",
2452
- "log-llm-requests",
2453
- "log-llm-responses",
2454
- "type"
2455
- // Allowed for inheritance compatibility, ignored for built-in commands
2456
- ]);
2457
- var AGENT_CONFIG_KEYS = /* @__PURE__ */ new Set([
2458
- "model",
2459
- "system",
2460
- "temperature",
2461
- "max-iterations",
2462
- "gadgets",
2463
- // Full replacement (preferred)
2464
- "gadget-add",
2465
- // Add to inherited gadgets
2466
- "gadget-remove",
2467
- // Remove from inherited gadgets
2468
- "gadget",
2469
- // DEPRECATED: alias for gadgets
2470
- "builtins",
2471
- "builtin-interaction",
2472
- "gadget-start-prefix",
2473
- "gadget-end-prefix",
2474
- "gadget-arg-prefix",
2475
- "gadget-approval",
2476
- "quiet",
2477
- "inherits",
2478
- "log-level",
2479
- "log-file",
2480
- "log-reset",
2481
- "log-llm-requests",
2482
- "log-llm-responses",
2483
- "type"
2484
- // Allowed for inheritance compatibility, ignored for built-in commands
2485
- ]);
2486
- var CUSTOM_CONFIG_KEYS = /* @__PURE__ */ new Set([
2487
- ...COMPLETE_CONFIG_KEYS,
2488
- ...AGENT_CONFIG_KEYS,
2489
- "type",
2490
- "description"
2491
- ]);
2492
- function getConfigPath() {
2493
- return join2(homedir2(), ".llmist", "cli.toml");
2488
+ function getCustomCommandNames(config) {
2489
+ const reserved = /* @__PURE__ */ new Set(["global", "complete", "agent", "prompts", "docker"]);
2490
+ return Object.keys(config).filter((key) => !reserved.has(key));
2494
2491
  }
2495
- var ConfigError = class extends Error {
2496
- constructor(message, path5) {
2497
- super(path5 ? `${path5}: ${message}` : message);
2498
- this.path = path5;
2499
- this.name = "ConfigError";
2492
+ function resolveTemplatesInConfig(config, configPath) {
2493
+ const prompts = config.prompts ?? {};
2494
+ const hasPrompts = Object.keys(prompts).length > 0;
2495
+ let hasTemplates = false;
2496
+ for (const [sectionName, section] of Object.entries(config)) {
2497
+ if (sectionName === "global" || sectionName === "prompts") continue;
2498
+ if (!section || typeof section !== "object") continue;
2499
+ const sectionObj = section;
2500
+ if (typeof sectionObj.system === "string" && hasTemplateSyntax(sectionObj.system)) {
2501
+ hasTemplates = true;
2502
+ break;
2503
+ }
2500
2504
  }
2501
- };
2502
- function validateString(value, key, section) {
2503
- if (typeof value !== "string") {
2504
- throw new ConfigError(`[${section}].${key} must be a string`);
2505
+ for (const template of Object.values(prompts)) {
2506
+ if (hasTemplateSyntax(template)) {
2507
+ hasTemplates = true;
2508
+ break;
2509
+ }
2505
2510
  }
2506
- return value;
2511
+ if (!hasPrompts && !hasTemplates) {
2512
+ return config;
2513
+ }
2514
+ try {
2515
+ validatePrompts(prompts, configPath);
2516
+ } catch (error) {
2517
+ if (error instanceof TemplateError) {
2518
+ throw new ConfigError(error.message, configPath);
2519
+ }
2520
+ throw error;
2521
+ }
2522
+ for (const [name, template] of Object.entries(prompts)) {
2523
+ try {
2524
+ validateEnvVars(template, name, configPath);
2525
+ } catch (error) {
2526
+ if (error instanceof TemplateError) {
2527
+ throw new ConfigError(error.message, configPath);
2528
+ }
2529
+ throw error;
2530
+ }
2531
+ }
2532
+ const eta = createTemplateEngine(prompts, configPath);
2533
+ const result = { ...config };
2534
+ for (const [sectionName, section] of Object.entries(config)) {
2535
+ if (sectionName === "global" || sectionName === "prompts") continue;
2536
+ if (!section || typeof section !== "object") continue;
2537
+ const sectionObj = section;
2538
+ if (typeof sectionObj.system === "string" && hasTemplateSyntax(sectionObj.system)) {
2539
+ try {
2540
+ validateEnvVars(sectionObj.system, void 0, configPath);
2541
+ } catch (error) {
2542
+ if (error instanceof TemplateError) {
2543
+ throw new ConfigError(`[${sectionName}].system: ${error.message}`, configPath);
2544
+ }
2545
+ throw error;
2546
+ }
2547
+ try {
2548
+ const resolved = resolveTemplate(eta, sectionObj.system, {}, configPath);
2549
+ result[sectionName] = {
2550
+ ...sectionObj,
2551
+ system: resolved
2552
+ };
2553
+ } catch (error) {
2554
+ if (error instanceof TemplateError) {
2555
+ throw new ConfigError(`[${sectionName}].system: ${error.message}`, configPath);
2556
+ }
2557
+ throw error;
2558
+ }
2559
+ }
2560
+ }
2561
+ return result;
2507
2562
  }
2508
- function validateNumber(value, key, section, opts) {
2509
- if (typeof value !== "number") {
2510
- throw new ConfigError(`[${section}].${key} must be a number`);
2563
+ function resolveGadgets(section, inheritedGadgets, sectionName, configPath) {
2564
+ const hasGadgets = "gadgets" in section;
2565
+ const hasGadgetLegacy = "gadget" in section;
2566
+ const hasGadgetAdd = "gadget-add" in section;
2567
+ const hasGadgetRemove = "gadget-remove" in section;
2568
+ if (hasGadgetLegacy && !hasGadgets) {
2569
+ console.warn(
2570
+ `[config] Warning: [${sectionName}].gadget is deprecated, use 'gadgets' (plural) instead`
2571
+ );
2511
2572
  }
2512
- if (opts?.integer && !Number.isInteger(value)) {
2513
- throw new ConfigError(`[${section}].${key} must be an integer`);
2573
+ if ((hasGadgets || hasGadgetLegacy) && (hasGadgetAdd || hasGadgetRemove)) {
2574
+ throw new ConfigError(
2575
+ `[${sectionName}] Cannot use 'gadgets' with 'gadget-add'/'gadget-remove'. Use either full replacement (gadgets) OR modification (gadget-add/gadget-remove).`,
2576
+ configPath
2577
+ );
2514
2578
  }
2515
- if (opts?.min !== void 0 && value < opts.min) {
2516
- throw new ConfigError(`[${section}].${key} must be >= ${opts.min}`);
2579
+ if (hasGadgets) {
2580
+ return section.gadgets;
2517
2581
  }
2518
- if (opts?.max !== void 0 && value > opts.max) {
2519
- throw new ConfigError(`[${section}].${key} must be <= ${opts.max}`);
2582
+ if (hasGadgetLegacy) {
2583
+ return section.gadget;
2584
+ }
2585
+ let result = [...inheritedGadgets];
2586
+ if (hasGadgetRemove) {
2587
+ const toRemove = new Set(section["gadget-remove"]);
2588
+ result = result.filter((g) => !toRemove.has(g));
2589
+ }
2590
+ if (hasGadgetAdd) {
2591
+ const toAdd = section["gadget-add"];
2592
+ result.push(...toAdd);
2593
+ }
2594
+ return result;
2595
+ }
2596
+ function resolveInheritance(config, configPath) {
2597
+ const resolved = {};
2598
+ const resolving = /* @__PURE__ */ new Set();
2599
+ function resolveSection(name) {
2600
+ if (name in resolved) {
2601
+ return resolved[name];
2602
+ }
2603
+ if (resolving.has(name)) {
2604
+ throw new ConfigError(`Circular inheritance detected: ${name}`, configPath);
2605
+ }
2606
+ const section = config[name];
2607
+ if (section === void 0 || typeof section !== "object") {
2608
+ throw new ConfigError(`Cannot inherit from unknown section: ${name}`, configPath);
2609
+ }
2610
+ resolving.add(name);
2611
+ const sectionObj = section;
2612
+ const inheritsRaw = sectionObj.inherits;
2613
+ const inheritsList = inheritsRaw ? Array.isArray(inheritsRaw) ? inheritsRaw : [inheritsRaw] : [];
2614
+ let merged = {};
2615
+ for (const parent of inheritsList) {
2616
+ const parentResolved = resolveSection(parent);
2617
+ merged = { ...merged, ...parentResolved };
2618
+ }
2619
+ const inheritedGadgets = merged.gadgets ?? [];
2620
+ const {
2621
+ inherits: _inherits,
2622
+ gadgets: _gadgets,
2623
+ gadget: _gadget,
2624
+ "gadget-add": _gadgetAdd,
2625
+ "gadget-remove": _gadgetRemove,
2626
+ ...ownValues
2627
+ } = sectionObj;
2628
+ merged = { ...merged, ...ownValues };
2629
+ const resolvedGadgets = resolveGadgets(sectionObj, inheritedGadgets, name, configPath);
2630
+ if (resolvedGadgets.length > 0) {
2631
+ merged.gadgets = resolvedGadgets;
2632
+ }
2633
+ delete merged["gadget"];
2634
+ delete merged["gadget-add"];
2635
+ delete merged["gadget-remove"];
2636
+ resolving.delete(name);
2637
+ resolved[name] = merged;
2638
+ return merged;
2639
+ }
2640
+ for (const name of Object.keys(config)) {
2641
+ resolveSection(name);
2642
+ }
2643
+ return resolved;
2644
+ }
2645
+
2646
+ // src/cli/docker/docker-config.ts
2647
+ var MOUNT_CONFIG_KEYS = /* @__PURE__ */ new Set(["source", "target", "permission"]);
2648
+ function validateString2(value, key, section) {
2649
+ if (typeof value !== "string") {
2650
+ throw new ConfigError(`[${section}].${key} must be a string`);
2520
2651
  }
2521
2652
  return value;
2522
2653
  }
2523
- function validateBoolean(value, key, section) {
2654
+ function validateBoolean2(value, key, section) {
2524
2655
  if (typeof value !== "boolean") {
2525
2656
  throw new ConfigError(`[${section}].${key} must be a boolean`);
2526
2657
  }
2527
2658
  return value;
2528
2659
  }
2529
- function validateStringArray(value, key, section) {
2660
+ function validateStringArray2(value, key, section) {
2530
2661
  if (!Array.isArray(value)) {
2531
2662
  throw new ConfigError(`[${section}].${key} must be an array`);
2532
2663
  }
@@ -2537,535 +2668,949 @@ function validateStringArray(value, key, section) {
2537
2668
  }
2538
2669
  return value;
2539
2670
  }
2540
- function validateInherits(value, section) {
2541
- if (typeof value === "string") {
2542
- return value;
2543
- }
2544
- if (Array.isArray(value)) {
2545
- for (let i = 0; i < value.length; i++) {
2546
- if (typeof value[i] !== "string") {
2547
- throw new ConfigError(`[${section}].inherits[${i}] must be a string`);
2548
- }
2549
- }
2550
- return value;
2671
+ function validateMountPermission(value, key, section) {
2672
+ const str = validateString2(value, key, section);
2673
+ if (!VALID_MOUNT_PERMISSIONS.includes(str)) {
2674
+ throw new ConfigError(
2675
+ `[${section}].${key} must be one of: ${VALID_MOUNT_PERMISSIONS.join(", ")}`
2676
+ );
2551
2677
  }
2552
- throw new ConfigError(`[${section}].inherits must be a string or array of strings`);
2678
+ return str;
2553
2679
  }
2554
- function validateGadgetApproval(value, section) {
2680
+ function validateMountConfig(value, index, section) {
2555
2681
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
2556
- throw new ConfigError(
2557
- `[${section}].gadget-approval must be a table (e.g., { WriteFile = "approval-required" })`
2558
- );
2682
+ throw new ConfigError(`[${section}].mounts[${index}] must be a table`);
2559
2683
  }
2560
- const result = {};
2561
- for (const [gadgetName, mode] of Object.entries(value)) {
2562
- if (typeof mode !== "string") {
2563
- throw new ConfigError(
2564
- `[${section}].gadget-approval.${gadgetName} must be a string`
2565
- );
2566
- }
2567
- if (!VALID_APPROVAL_MODES.includes(mode)) {
2568
- throw new ConfigError(
2569
- `[${section}].gadget-approval.${gadgetName} must be one of: ${VALID_APPROVAL_MODES.join(", ")}`
2570
- );
2684
+ const rawObj = value;
2685
+ const mountSection = `${section}.mounts[${index}]`;
2686
+ for (const key of Object.keys(rawObj)) {
2687
+ if (!MOUNT_CONFIG_KEYS.has(key)) {
2688
+ throw new ConfigError(`[${mountSection}].${key} is not a valid mount option`);
2571
2689
  }
2572
- result[gadgetName] = mode;
2573
2690
  }
2574
- return result;
2575
- }
2576
- function validateLoggingConfig(raw, section) {
2577
- const result = {};
2578
- if ("log-level" in raw) {
2579
- const level = validateString(raw["log-level"], "log-level", section);
2580
- if (!VALID_LOG_LEVELS.includes(level)) {
2581
- throw new ConfigError(
2582
- `[${section}].log-level must be one of: ${VALID_LOG_LEVELS.join(", ")}`
2583
- );
2584
- }
2585
- result["log-level"] = level;
2691
+ if (!("source" in rawObj)) {
2692
+ throw new ConfigError(`[${mountSection}] missing required field 'source'`);
2586
2693
  }
2587
- if ("log-file" in raw) {
2588
- result["log-file"] = validateString(raw["log-file"], "log-file", section);
2694
+ if (!("target" in rawObj)) {
2695
+ throw new ConfigError(`[${mountSection}] missing required field 'target'`);
2589
2696
  }
2590
- if ("log-reset" in raw) {
2591
- result["log-reset"] = validateBoolean(raw["log-reset"], "log-reset", section);
2697
+ if (!("permission" in rawObj)) {
2698
+ throw new ConfigError(`[${mountSection}] missing required field 'permission'`);
2592
2699
  }
2593
- return result;
2700
+ return {
2701
+ source: validateString2(rawObj.source, "source", mountSection),
2702
+ target: validateString2(rawObj.target, "target", mountSection),
2703
+ permission: validateMountPermission(rawObj.permission, "permission", mountSection)
2704
+ };
2594
2705
  }
2595
- function validateBaseConfig(raw, section) {
2596
- const result = {};
2597
- if ("model" in raw) {
2598
- result.model = validateString(raw.model, "model", section);
2599
- }
2600
- if ("system" in raw) {
2601
- result.system = validateString(raw.system, "system", section);
2602
- }
2603
- if ("temperature" in raw) {
2604
- result.temperature = validateNumber(raw.temperature, "temperature", section, {
2605
- min: 0,
2606
- max: 2
2607
- });
2706
+ function validateMountsArray(value, section) {
2707
+ if (!Array.isArray(value)) {
2708
+ throw new ConfigError(`[${section}].mounts must be an array of tables`);
2608
2709
  }
2609
- if ("inherits" in raw) {
2610
- result.inherits = validateInherits(raw.inherits, section);
2710
+ const result = [];
2711
+ for (let i = 0; i < value.length; i++) {
2712
+ result.push(validateMountConfig(value[i], i, section));
2611
2713
  }
2612
2714
  return result;
2613
2715
  }
2614
- function validateGlobalConfig(raw, section) {
2615
- if (typeof raw !== "object" || raw === null) {
2616
- throw new ConfigError(`[${section}] must be a table`);
2617
- }
2618
- const rawObj = raw;
2619
- for (const key of Object.keys(rawObj)) {
2620
- if (!GLOBAL_CONFIG_KEYS.has(key)) {
2621
- throw new ConfigError(`[${section}].${key} is not a valid option`);
2622
- }
2623
- }
2624
- return validateLoggingConfig(rawObj, section);
2625
- }
2626
- function validateCompleteConfig(raw, section) {
2716
+ function validateDockerConfig(raw, section) {
2627
2717
  if (typeof raw !== "object" || raw === null) {
2628
2718
  throw new ConfigError(`[${section}] must be a table`);
2629
2719
  }
2630
2720
  const rawObj = raw;
2631
2721
  for (const key of Object.keys(rawObj)) {
2632
- if (!COMPLETE_CONFIG_KEYS.has(key)) {
2722
+ if (!DOCKER_CONFIG_KEYS.has(key)) {
2633
2723
  throw new ConfigError(`[${section}].${key} is not a valid option`);
2634
2724
  }
2635
2725
  }
2636
- const result = {
2637
- ...validateBaseConfig(rawObj, section),
2638
- ...validateLoggingConfig(rawObj, section)
2639
- };
2640
- if ("max-tokens" in rawObj) {
2641
- result["max-tokens"] = validateNumber(rawObj["max-tokens"], "max-tokens", section, {
2642
- integer: true,
2643
- min: 1
2644
- });
2726
+ const result = {};
2727
+ if ("enabled" in rawObj) {
2728
+ result.enabled = validateBoolean2(rawObj.enabled, "enabled", section);
2645
2729
  }
2646
- if ("quiet" in rawObj) {
2647
- result.quiet = validateBoolean(rawObj.quiet, "quiet", section);
2730
+ if ("dockerfile" in rawObj) {
2731
+ result.dockerfile = validateString2(rawObj.dockerfile, "dockerfile", section);
2648
2732
  }
2649
- if ("log-llm-requests" in rawObj) {
2650
- result["log-llm-requests"] = validateStringOrBoolean(
2651
- rawObj["log-llm-requests"],
2652
- "log-llm-requests",
2733
+ if ("cwd-permission" in rawObj) {
2734
+ result["cwd-permission"] = validateMountPermission(
2735
+ rawObj["cwd-permission"],
2736
+ "cwd-permission",
2653
2737
  section
2654
2738
  );
2655
2739
  }
2656
- if ("log-llm-responses" in rawObj) {
2657
- result["log-llm-responses"] = validateStringOrBoolean(
2658
- rawObj["log-llm-responses"],
2659
- "log-llm-responses",
2740
+ if ("config-permission" in rawObj) {
2741
+ result["config-permission"] = validateMountPermission(
2742
+ rawObj["config-permission"],
2743
+ "config-permission",
2660
2744
  section
2661
2745
  );
2662
2746
  }
2663
- return result;
2664
- }
2665
- function validateAgentConfig(raw, section) {
2666
- if (typeof raw !== "object" || raw === null) {
2667
- throw new ConfigError(`[${section}] must be a table`);
2747
+ if ("mounts" in rawObj) {
2748
+ result.mounts = validateMountsArray(rawObj.mounts, section);
2668
2749
  }
2669
- const rawObj = raw;
2670
- for (const key of Object.keys(rawObj)) {
2671
- if (!AGENT_CONFIG_KEYS.has(key)) {
2672
- throw new ConfigError(`[${section}].${key} is not a valid option`);
2673
- }
2750
+ if ("env-vars" in rawObj) {
2751
+ result["env-vars"] = validateStringArray2(rawObj["env-vars"], "env-vars", section);
2674
2752
  }
2675
- const result = {
2676
- ...validateBaseConfig(rawObj, section),
2677
- ...validateLoggingConfig(rawObj, section)
2678
- };
2679
- if ("max-iterations" in rawObj) {
2680
- result["max-iterations"] = validateNumber(rawObj["max-iterations"], "max-iterations", section, {
2681
- integer: true,
2682
- min: 1
2683
- });
2753
+ if ("image-name" in rawObj) {
2754
+ result["image-name"] = validateString2(rawObj["image-name"], "image-name", section);
2684
2755
  }
2685
- if ("gadgets" in rawObj) {
2686
- result.gadgets = validateStringArray(rawObj.gadgets, "gadgets", section);
2756
+ if ("dev-mode" in rawObj) {
2757
+ result["dev-mode"] = validateBoolean2(rawObj["dev-mode"], "dev-mode", section);
2687
2758
  }
2688
- if ("gadget-add" in rawObj) {
2689
- result["gadget-add"] = validateStringArray(rawObj["gadget-add"], "gadget-add", section);
2759
+ if ("dev-source" in rawObj) {
2760
+ result["dev-source"] = validateString2(rawObj["dev-source"], "dev-source", section);
2690
2761
  }
2691
- if ("gadget-remove" in rawObj) {
2692
- result["gadget-remove"] = validateStringArray(rawObj["gadget-remove"], "gadget-remove", section);
2762
+ return result;
2763
+ }
2764
+
2765
+ // src/cli/docker/dockerfile.ts
2766
+ var DEFAULT_DOCKERFILE = `# llmist sandbox image
2767
+ # Auto-generated - customize via [docker].dockerfile in cli.toml
2768
+
2769
+ FROM oven/bun:1-debian
2770
+
2771
+ # Install essential tools
2772
+ RUN apt-get update && apt-get install -y --no-install-recommends \\
2773
+ # ripgrep for fast file searching
2774
+ ripgrep \\
2775
+ # git for version control operations
2776
+ git \\
2777
+ # curl for downloads and API calls
2778
+ curl \\
2779
+ # ca-certificates for HTTPS
2780
+ ca-certificates \\
2781
+ && rm -rf /var/lib/apt/lists/*
2782
+
2783
+ # Install ast-grep for code search/refactoring
2784
+ # Using the official install script
2785
+ RUN curl -fsSL https://raw.githubusercontent.com/ast-grep/ast-grep/main/install.sh | bash \\
2786
+ && mv /root/.local/bin/ast-grep /usr/local/bin/ 2>/dev/null || true \\
2787
+ && mv /root/.local/bin/sg /usr/local/bin/ 2>/dev/null || true
2788
+
2789
+ # Install llmist globally via bun
2790
+ RUN bun add -g llmist
2791
+
2792
+ # Working directory (host CWD will be mounted here)
2793
+ WORKDIR /workspace
2794
+
2795
+ # Entry point - llmist with all arguments forwarded
2796
+ ENTRYPOINT ["llmist"]
2797
+ `;
2798
+ var DEV_DOCKERFILE = `# llmist DEV sandbox image
2799
+ # For development/testing with local source code
2800
+
2801
+ FROM oven/bun:1-debian
2802
+
2803
+ # Install essential tools (same as production)
2804
+ RUN apt-get update && apt-get install -y --no-install-recommends \\
2805
+ ripgrep \\
2806
+ git \\
2807
+ curl \\
2808
+ ca-certificates \\
2809
+ && rm -rf /var/lib/apt/lists/*
2810
+
2811
+ # Install ast-grep for code search/refactoring
2812
+ RUN curl -fsSL https://raw.githubusercontent.com/ast-grep/ast-grep/main/install.sh | bash \\
2813
+ && mv /root/.local/bin/ast-grep /usr/local/bin/ 2>/dev/null || true \\
2814
+ && mv /root/.local/bin/sg /usr/local/bin/ 2>/dev/null || true
2815
+
2816
+ # Working directory (host CWD will be mounted here)
2817
+ WORKDIR /workspace
2818
+
2819
+ # Entry point - run llmist from mounted source
2820
+ # Source is mounted at ${DEV_SOURCE_MOUNT_TARGET}
2821
+ ENTRYPOINT ["bun", "run", "${DEV_SOURCE_MOUNT_TARGET}/src/cli.ts"]
2822
+ `;
2823
+ function resolveDockerfile(config, devMode = false) {
2824
+ if (config.dockerfile) {
2825
+ return config.dockerfile;
2693
2826
  }
2694
- if ("gadget" in rawObj) {
2695
- result.gadget = validateStringArray(rawObj.gadget, "gadget", section);
2827
+ return devMode ? DEV_DOCKERFILE : DEFAULT_DOCKERFILE;
2828
+ }
2829
+ function computeDockerfileHash(dockerfile) {
2830
+ const encoder = new TextEncoder();
2831
+ const data = encoder.encode(dockerfile);
2832
+ return Bun.hash(data).toString(16);
2833
+ }
2834
+
2835
+ // src/cli/docker/image-manager.ts
2836
+ import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "node:fs";
2837
+ import { homedir as homedir3 } from "node:os";
2838
+ import { join as join3 } from "node:path";
2839
+ var CACHE_DIR = join3(homedir3(), ".llmist", "docker-cache");
2840
+ var HASH_FILE = "image-hash.json";
2841
+ function ensureCacheDir() {
2842
+ if (!existsSync3(CACHE_DIR)) {
2843
+ mkdirSync(CACHE_DIR, { recursive: true });
2696
2844
  }
2697
- if ("builtins" in rawObj) {
2698
- result.builtins = validateBoolean(rawObj.builtins, "builtins", section);
2845
+ }
2846
+ function getCachedHash(imageName) {
2847
+ const hashPath = join3(CACHE_DIR, HASH_FILE);
2848
+ if (!existsSync3(hashPath)) {
2849
+ return void 0;
2699
2850
  }
2700
- if ("builtin-interaction" in rawObj) {
2701
- result["builtin-interaction"] = validateBoolean(
2702
- rawObj["builtin-interaction"],
2703
- "builtin-interaction",
2704
- section
2705
- );
2851
+ try {
2852
+ const content = readFileSync3(hashPath, "utf-8");
2853
+ const cache = JSON.parse(content);
2854
+ return cache[imageName]?.dockerfileHash;
2855
+ } catch {
2856
+ return void 0;
2706
2857
  }
2707
- if ("gadget-start-prefix" in rawObj) {
2708
- result["gadget-start-prefix"] = validateString(
2709
- rawObj["gadget-start-prefix"],
2710
- "gadget-start-prefix",
2711
- section
2712
- );
2858
+ }
2859
+ function setCachedHash(imageName, hash) {
2860
+ ensureCacheDir();
2861
+ const hashPath = join3(CACHE_DIR, HASH_FILE);
2862
+ let cache = {};
2863
+ if (existsSync3(hashPath)) {
2864
+ try {
2865
+ const content = readFileSync3(hashPath, "utf-8");
2866
+ cache = JSON.parse(content);
2867
+ } catch {
2868
+ cache = {};
2869
+ }
2713
2870
  }
2714
- if ("gadget-end-prefix" in rawObj) {
2715
- result["gadget-end-prefix"] = validateString(
2716
- rawObj["gadget-end-prefix"],
2717
- "gadget-end-prefix",
2718
- section
2871
+ cache[imageName] = {
2872
+ imageName,
2873
+ dockerfileHash: hash,
2874
+ builtAt: (/* @__PURE__ */ new Date()).toISOString()
2875
+ };
2876
+ writeFileSync(hashPath, JSON.stringify(cache, null, 2));
2877
+ }
2878
+ var DockerBuildError = class extends Error {
2879
+ constructor(message, output) {
2880
+ super(message);
2881
+ this.output = output;
2882
+ this.name = "DockerBuildError";
2883
+ }
2884
+ };
2885
+ async function buildImage(imageName, dockerfile) {
2886
+ ensureCacheDir();
2887
+ const dockerfilePath = join3(CACHE_DIR, "Dockerfile");
2888
+ writeFileSync(dockerfilePath, dockerfile);
2889
+ const proc = Bun.spawn(
2890
+ ["docker", "build", "-t", imageName, "-f", dockerfilePath, CACHE_DIR],
2891
+ {
2892
+ stdout: "pipe",
2893
+ stderr: "pipe"
2894
+ }
2895
+ );
2896
+ const exitCode = await proc.exited;
2897
+ const stdout = await new Response(proc.stdout).text();
2898
+ const stderr = await new Response(proc.stderr).text();
2899
+ if (exitCode !== 0) {
2900
+ const output = [stdout, stderr].filter(Boolean).join("\n");
2901
+ throw new DockerBuildError(
2902
+ `Docker build failed with exit code ${exitCode}`,
2903
+ output
2719
2904
  );
2720
2905
  }
2721
- if ("gadget-arg-prefix" in rawObj) {
2722
- result["gadget-arg-prefix"] = validateString(
2723
- rawObj["gadget-arg-prefix"],
2724
- "gadget-arg-prefix",
2725
- section
2906
+ }
2907
+ async function ensureImage(imageName = DEFAULT_IMAGE_NAME, dockerfile) {
2908
+ const hash = computeDockerfileHash(dockerfile);
2909
+ const cachedHash = getCachedHash(imageName);
2910
+ if (cachedHash === hash) {
2911
+ return imageName;
2912
+ }
2913
+ console.error(`Building Docker image '${imageName}'...`);
2914
+ await buildImage(imageName, dockerfile);
2915
+ setCachedHash(imageName, hash);
2916
+ console.error(`Docker image '${imageName}' built successfully.`);
2917
+ return imageName;
2918
+ }
2919
+
2920
+ // src/cli/docker/docker-wrapper.ts
2921
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
2922
+ import { dirname, join as join4 } from "node:path";
2923
+ import { homedir as homedir4 } from "node:os";
2924
+ var DockerUnavailableError = class extends Error {
2925
+ constructor() {
2926
+ super(
2927
+ "Docker is required but not available. Install Docker or disable Docker sandboxing in your configuration."
2726
2928
  );
2929
+ this.name = "DockerUnavailableError";
2727
2930
  }
2728
- if ("gadget-approval" in rawObj) {
2729
- result["gadget-approval"] = validateGadgetApproval(rawObj["gadget-approval"], section);
2931
+ };
2932
+ var DockerSkipError = class extends Error {
2933
+ constructor() {
2934
+ super("Docker execution skipped - already inside container");
2935
+ this.name = "DockerSkipError";
2730
2936
  }
2731
- if ("quiet" in rawObj) {
2732
- result.quiet = validateBoolean(rawObj.quiet, "quiet", section);
2937
+ };
2938
+ async function checkDockerAvailable() {
2939
+ try {
2940
+ const proc = Bun.spawn(["docker", "info"], {
2941
+ stdout: "pipe",
2942
+ stderr: "pipe"
2943
+ });
2944
+ await proc.exited;
2945
+ return proc.exitCode === 0;
2946
+ } catch {
2947
+ return false;
2733
2948
  }
2734
- if ("log-llm-requests" in rawObj) {
2735
- result["log-llm-requests"] = validateStringOrBoolean(
2736
- rawObj["log-llm-requests"],
2737
- "log-llm-requests",
2738
- section
2739
- );
2949
+ }
2950
+ function isInsideContainer() {
2951
+ if (existsSync4("/.dockerenv")) {
2952
+ return true;
2740
2953
  }
2741
- if ("log-llm-responses" in rawObj) {
2742
- result["log-llm-responses"] = validateStringOrBoolean(
2743
- rawObj["log-llm-responses"],
2744
- "log-llm-responses",
2745
- section
2746
- );
2954
+ try {
2955
+ const cgroup = readFileSync4("/proc/1/cgroup", "utf-8");
2956
+ if (cgroup.includes("docker") || cgroup.includes("containerd")) {
2957
+ return true;
2958
+ }
2959
+ } catch {
2747
2960
  }
2748
- return result;
2961
+ return false;
2749
2962
  }
2750
- function validateStringOrBoolean(value, field, section) {
2751
- if (typeof value === "string" || typeof value === "boolean") {
2752
- return value;
2963
+ function autoDetectDevSource() {
2964
+ const scriptPath = process.argv[1];
2965
+ if (!scriptPath || !scriptPath.endsWith("src/cli.ts")) {
2966
+ return void 0;
2753
2967
  }
2754
- throw new ConfigError(`[${section}].${field} must be a string or boolean`);
2755
- }
2756
- function validateCustomConfig(raw, section) {
2757
- if (typeof raw !== "object" || raw === null) {
2758
- throw new ConfigError(`[${section}] must be a table`);
2968
+ const srcDir = dirname(scriptPath);
2969
+ const projectDir = dirname(srcDir);
2970
+ const packageJsonPath = join4(projectDir, "package.json");
2971
+ if (!existsSync4(packageJsonPath)) {
2972
+ return void 0;
2759
2973
  }
2760
- const rawObj = raw;
2761
- for (const key of Object.keys(rawObj)) {
2762
- if (!CUSTOM_CONFIG_KEYS.has(key)) {
2763
- throw new ConfigError(`[${section}].${key} is not a valid option`);
2974
+ try {
2975
+ const pkg = JSON.parse(readFileSync4(packageJsonPath, "utf-8"));
2976
+ if (pkg.name === "llmist") {
2977
+ return projectDir;
2764
2978
  }
2979
+ } catch {
2765
2980
  }
2766
- let type = "agent";
2767
- if ("type" in rawObj) {
2768
- const typeValue = validateString(rawObj.type, "type", section);
2769
- if (typeValue !== "agent" && typeValue !== "complete") {
2770
- throw new ConfigError(`[${section}].type must be "agent" or "complete"`);
2771
- }
2772
- type = typeValue;
2981
+ return void 0;
2982
+ }
2983
+ function resolveDevMode(config, cliDevMode) {
2984
+ const enabled = cliDevMode || config?.["dev-mode"] || process.env.LLMIST_DEV_MODE === "1";
2985
+ if (!enabled) {
2986
+ return { enabled: false, sourcePath: void 0 };
2773
2987
  }
2774
- const result = {
2775
- ...validateBaseConfig(rawObj, section),
2776
- type
2777
- };
2778
- if ("description" in rawObj) {
2779
- result.description = validateString(rawObj.description, "description", section);
2988
+ const sourcePath = config?.["dev-source"] || process.env.LLMIST_DEV_SOURCE || autoDetectDevSource();
2989
+ if (!sourcePath) {
2990
+ throw new Error(
2991
+ "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)."
2992
+ );
2780
2993
  }
2781
- if ("max-iterations" in rawObj) {
2782
- result["max-iterations"] = validateNumber(rawObj["max-iterations"], "max-iterations", section, {
2783
- integer: true,
2784
- min: 1
2785
- });
2994
+ return { enabled: true, sourcePath };
2995
+ }
2996
+ function expandHome(path5) {
2997
+ if (path5.startsWith("~")) {
2998
+ return path5.replace(/^~/, homedir4());
2786
2999
  }
2787
- if ("gadgets" in rawObj) {
2788
- result.gadgets = validateStringArray(rawObj.gadgets, "gadgets", section);
3000
+ return path5;
3001
+ }
3002
+ function buildDockerRunArgs(ctx, imageName, devMode) {
3003
+ const args = ["run", "--rm"];
3004
+ const timestamp = Date.now();
3005
+ const random = Math.random().toString(36).slice(2, 8);
3006
+ const containerName = `llmist-${timestamp}-${random}`;
3007
+ args.push("--name", containerName);
3008
+ if (process.stdin.isTTY) {
3009
+ args.push("-it");
3010
+ }
3011
+ const cwdPermission = ctx.options.dockerRo ? "ro" : ctx.profileCwdPermission ?? ctx.config["cwd-permission"] ?? DEFAULT_CWD_PERMISSION;
3012
+ args.push("-v", `${ctx.cwd}:/workspace:${cwdPermission}`);
3013
+ args.push("-w", "/workspace");
3014
+ const configPermission = ctx.config["config-permission"] ?? DEFAULT_CONFIG_PERMISSION;
3015
+ const llmistDir = expandHome("~/.llmist");
3016
+ args.push("-v", `${llmistDir}:/root/.llmist:${configPermission}`);
3017
+ if (devMode.enabled && devMode.sourcePath) {
3018
+ const expandedSource = expandHome(devMode.sourcePath);
3019
+ args.push("-v", `${expandedSource}:${DEV_SOURCE_MOUNT_TARGET}:ro`);
3020
+ }
3021
+ if (ctx.config.mounts) {
3022
+ for (const mount of ctx.config.mounts) {
3023
+ const source = expandHome(mount.source);
3024
+ args.push("-v", `${source}:${mount.target}:${mount.permission}`);
3025
+ }
3026
+ }
3027
+ for (const key of FORWARDED_API_KEYS) {
3028
+ if (process.env[key]) {
3029
+ args.push("-e", key);
3030
+ }
3031
+ }
3032
+ if (ctx.config["env-vars"]) {
3033
+ for (const key of ctx.config["env-vars"]) {
3034
+ if (process.env[key]) {
3035
+ args.push("-e", key);
3036
+ }
3037
+ }
2789
3038
  }
2790
- if ("gadget-add" in rawObj) {
2791
- result["gadget-add"] = validateStringArray(rawObj["gadget-add"], "gadget-add", section);
3039
+ args.push(imageName);
3040
+ args.push(...ctx.forwardArgs);
3041
+ return args;
3042
+ }
3043
+ function filterDockerArgs(argv) {
3044
+ const dockerFlags = /* @__PURE__ */ new Set(["--docker", "--docker-ro", "--no-docker", "--docker-dev"]);
3045
+ return argv.filter((arg) => !dockerFlags.has(arg));
3046
+ }
3047
+ function resolveDockerEnabled(config, options, profileDocker) {
3048
+ if (options.noDocker) {
3049
+ return false;
2792
3050
  }
2793
- if ("gadget-remove" in rawObj) {
2794
- result["gadget-remove"] = validateStringArray(rawObj["gadget-remove"], "gadget-remove", section);
3051
+ if (options.docker || options.dockerRo) {
3052
+ return true;
2795
3053
  }
2796
- if ("gadget" in rawObj) {
2797
- result.gadget = validateStringArray(rawObj.gadget, "gadget", section);
3054
+ if (profileDocker !== void 0) {
3055
+ return profileDocker;
2798
3056
  }
2799
- if ("builtins" in rawObj) {
2800
- result.builtins = validateBoolean(rawObj.builtins, "builtins", section);
3057
+ return config?.enabled ?? false;
3058
+ }
3059
+ async function executeInDocker(ctx, devMode) {
3060
+ if (isInsideContainer()) {
3061
+ console.error(
3062
+ "Warning: Docker mode requested but already inside a container. Proceeding without re-containerization."
3063
+ );
3064
+ throw new DockerSkipError();
2801
3065
  }
2802
- if ("builtin-interaction" in rawObj) {
2803
- result["builtin-interaction"] = validateBoolean(
2804
- rawObj["builtin-interaction"],
2805
- "builtin-interaction",
2806
- section
2807
- );
2808
- }
2809
- if ("gadget-start-prefix" in rawObj) {
2810
- result["gadget-start-prefix"] = validateString(
2811
- rawObj["gadget-start-prefix"],
2812
- "gadget-start-prefix",
2813
- section
2814
- );
2815
- }
2816
- if ("gadget-end-prefix" in rawObj) {
2817
- result["gadget-end-prefix"] = validateString(
2818
- rawObj["gadget-end-prefix"],
2819
- "gadget-end-prefix",
2820
- section
2821
- );
2822
- }
2823
- if ("gadget-arg-prefix" in rawObj) {
2824
- result["gadget-arg-prefix"] = validateString(
2825
- rawObj["gadget-arg-prefix"],
2826
- "gadget-arg-prefix",
2827
- section
2828
- );
3066
+ const available = await checkDockerAvailable();
3067
+ if (!available) {
3068
+ throw new DockerUnavailableError();
2829
3069
  }
2830
- if ("gadget-approval" in rawObj) {
2831
- result["gadget-approval"] = validateGadgetApproval(rawObj["gadget-approval"], section);
3070
+ const dockerfile = resolveDockerfile(ctx.config, devMode.enabled);
3071
+ const imageName = devMode.enabled ? DEV_IMAGE_NAME : ctx.config["image-name"] ?? DEFAULT_IMAGE_NAME;
3072
+ if (devMode.enabled) {
3073
+ console.error(`[dev mode] Mounting source from ${devMode.sourcePath}`);
2832
3074
  }
2833
- if ("max-tokens" in rawObj) {
2834
- result["max-tokens"] = validateNumber(rawObj["max-tokens"], "max-tokens", section, {
2835
- integer: true,
2836
- min: 1
2837
- });
2838
- }
2839
- if ("quiet" in rawObj) {
2840
- result.quiet = validateBoolean(rawObj.quiet, "quiet", section);
2841
- }
2842
- Object.assign(result, validateLoggingConfig(rawObj, section));
2843
- return result;
2844
- }
2845
- function validatePromptsConfig(raw, section) {
2846
- if (typeof raw !== "object" || raw === null) {
2847
- throw new ConfigError(`[${section}] must be a table`);
2848
- }
2849
- const result = {};
2850
- for (const [key, value] of Object.entries(raw)) {
2851
- if (typeof value !== "string") {
2852
- throw new ConfigError(`[${section}].${key} must be a string`);
3075
+ try {
3076
+ await ensureImage(imageName, dockerfile);
3077
+ } catch (error) {
3078
+ if (error instanceof DockerBuildError) {
3079
+ console.error("Docker build failed:");
3080
+ console.error(error.output);
3081
+ throw error;
2853
3082
  }
2854
- result[key] = value;
3083
+ throw error;
2855
3084
  }
2856
- return result;
3085
+ const dockerArgs = buildDockerRunArgs(ctx, imageName, devMode);
3086
+ const proc = Bun.spawn(["docker", ...dockerArgs], {
3087
+ stdin: "inherit",
3088
+ stdout: "inherit",
3089
+ stderr: "inherit"
3090
+ });
3091
+ const exitCode = await proc.exited;
3092
+ process.exit(exitCode);
2857
3093
  }
2858
- function validateConfig(raw, configPath) {
2859
- if (typeof raw !== "object" || raw === null) {
2860
- throw new ConfigError("Config must be a TOML table", configPath);
3094
+ function createDockerContext(config, options, argv, cwd, profileCwdPermission) {
3095
+ return {
3096
+ config: config ?? {},
3097
+ options,
3098
+ forwardArgs: filterDockerArgs(argv),
3099
+ cwd,
3100
+ profileCwdPermission
3101
+ };
3102
+ }
3103
+
3104
+ // src/cli/agent-command.ts
3105
+ function createHumanInputHandler(env, progress, keyboard) {
3106
+ const stdout = env.stdout;
3107
+ if (!isInteractive(env.stdin) || typeof stdout.isTTY !== "boolean" || !stdout.isTTY) {
3108
+ return void 0;
2861
3109
  }
2862
- const rawObj = raw;
2863
- const result = {};
2864
- for (const [key, value] of Object.entries(rawObj)) {
3110
+ return async (question) => {
3111
+ progress.pause();
3112
+ if (keyboard.cleanupEsc) {
3113
+ keyboard.cleanupEsc();
3114
+ keyboard.cleanupEsc = null;
3115
+ }
3116
+ const rl = createInterface2({ input: env.stdin, output: env.stdout });
2865
3117
  try {
2866
- if (key === "global") {
2867
- result.global = validateGlobalConfig(value, key);
2868
- } else if (key === "complete") {
2869
- result.complete = validateCompleteConfig(value, key);
2870
- } else if (key === "agent") {
2871
- result.agent = validateAgentConfig(value, key);
2872
- } else if (key === "prompts") {
2873
- result.prompts = validatePromptsConfig(value, key);
2874
- } else {
2875
- result[key] = validateCustomConfig(value, key);
2876
- }
2877
- } catch (error) {
2878
- if (error instanceof ConfigError) {
2879
- throw new ConfigError(error.message, configPath);
3118
+ const questionLine = question.trim() ? `
3119
+ ${renderMarkdownWithSeparators(question.trim())}` : "";
3120
+ let isFirst = true;
3121
+ while (true) {
3122
+ const statsPrompt = progress.formatPrompt();
3123
+ const prompt = isFirst ? `${questionLine}
3124
+ ${statsPrompt}` : statsPrompt;
3125
+ isFirst = false;
3126
+ const answer = await rl.question(prompt);
3127
+ const trimmed = answer.trim();
3128
+ if (trimmed) {
3129
+ return trimmed;
3130
+ }
2880
3131
  }
2881
- throw error;
3132
+ } finally {
3133
+ rl.close();
3134
+ keyboard.restore();
2882
3135
  }
2883
- }
2884
- return result;
3136
+ };
2885
3137
  }
2886
- function loadConfig() {
2887
- const configPath = getConfigPath();
2888
- if (!existsSync2(configPath)) {
2889
- return {};
2890
- }
2891
- let content;
2892
- try {
2893
- content = readFileSync2(configPath, "utf-8");
2894
- } catch (error) {
2895
- throw new ConfigError(
2896
- `Failed to read config file: ${error instanceof Error ? error.message : "Unknown error"}`,
2897
- configPath
2898
- );
2899
- }
2900
- let raw;
2901
- try {
2902
- raw = parseToml(content);
2903
- } catch (error) {
2904
- throw new ConfigError(
2905
- `Invalid TOML syntax: ${error instanceof Error ? error.message : "Unknown error"}`,
2906
- configPath
3138
+ async function executeAgent(promptArg, options, env) {
3139
+ const dockerOptions = {
3140
+ docker: options.docker ?? false,
3141
+ dockerRo: options.dockerRo ?? false,
3142
+ noDocker: options.noDocker ?? false,
3143
+ dockerDev: options.dockerDev ?? false
3144
+ };
3145
+ const dockerEnabled = resolveDockerEnabled(
3146
+ env.dockerConfig,
3147
+ dockerOptions,
3148
+ options.docker
3149
+ // Profile-level docker: true/false
3150
+ );
3151
+ if (dockerEnabled) {
3152
+ const devMode = resolveDevMode(env.dockerConfig, dockerOptions.dockerDev);
3153
+ const ctx = createDockerContext(
3154
+ env.dockerConfig,
3155
+ dockerOptions,
3156
+ env.argv.slice(2),
3157
+ // Remove 'node' and script path
3158
+ process.cwd(),
3159
+ options.dockerCwdPermission
3160
+ // Profile-level CWD permission override
2907
3161
  );
3162
+ try {
3163
+ await executeInDocker(ctx, devMode);
3164
+ } catch (error) {
3165
+ if (error instanceof Error && error.message === "SKIP_DOCKER") {
3166
+ } else {
3167
+ throw error;
3168
+ }
3169
+ }
2908
3170
  }
2909
- const validated = validateConfig(raw, configPath);
2910
- const inherited = resolveInheritance(validated, configPath);
2911
- return resolveTemplatesInConfig(inherited, configPath);
2912
- }
2913
- function getCustomCommandNames(config) {
2914
- const reserved = /* @__PURE__ */ new Set(["global", "complete", "agent", "prompts"]);
2915
- return Object.keys(config).filter((key) => !reserved.has(key));
2916
- }
2917
- function resolveTemplatesInConfig(config, configPath) {
2918
- const prompts = config.prompts ?? {};
2919
- const hasPrompts = Object.keys(prompts).length > 0;
2920
- let hasTemplates = false;
2921
- for (const [sectionName, section] of Object.entries(config)) {
2922
- if (sectionName === "global" || sectionName === "prompts") continue;
2923
- if (!section || typeof section !== "object") continue;
2924
- const sectionObj = section;
2925
- if (typeof sectionObj.system === "string" && hasTemplateSyntax(sectionObj.system)) {
2926
- hasTemplates = true;
2927
- break;
3171
+ const prompt = await resolvePrompt(promptArg, env);
3172
+ const client = env.createClient();
3173
+ const registry = new GadgetRegistry();
3174
+ const stdinIsInteractive = isInteractive(env.stdin);
3175
+ if (options.builtins !== false) {
3176
+ for (const gadget of builtinGadgets) {
3177
+ if (gadget.name === "AskUser" && (options.builtinInteraction === false || !stdinIsInteractive)) {
3178
+ continue;
3179
+ }
3180
+ registry.registerByClass(gadget);
2928
3181
  }
2929
3182
  }
2930
- for (const template of Object.values(prompts)) {
2931
- if (hasTemplateSyntax(template)) {
2932
- hasTemplates = true;
2933
- break;
3183
+ const gadgetSpecifiers = options.gadget ?? [];
3184
+ if (gadgetSpecifiers.length > 0) {
3185
+ const gadgets2 = await loadGadgets(gadgetSpecifiers, process.cwd());
3186
+ for (const gadget of gadgets2) {
3187
+ registry.registerByClass(gadget);
2934
3188
  }
2935
3189
  }
2936
- if (!hasPrompts && !hasTemplates) {
2937
- return config;
3190
+ const printer = new StreamPrinter(env.stdout);
3191
+ const stderrTTY = env.stderr.isTTY === true;
3192
+ const progress = new StreamProgress(env.stderr, stderrTTY, client.modelRegistry);
3193
+ const abortController = new AbortController();
3194
+ let wasCancelled = false;
3195
+ let isStreaming = false;
3196
+ const stdinStream = env.stdin;
3197
+ const handleCancel = () => {
3198
+ if (!abortController.signal.aborted) {
3199
+ wasCancelled = true;
3200
+ abortController.abort();
3201
+ progress.pause();
3202
+ env.stderr.write(chalk5.yellow(`
3203
+ [Cancelled] ${progress.formatStats()}
3204
+ `));
3205
+ }
3206
+ };
3207
+ const keyboard = {
3208
+ cleanupEsc: null,
3209
+ cleanupSigint: null,
3210
+ restore: () => {
3211
+ if (stdinIsInteractive && stdinStream.isTTY && !wasCancelled) {
3212
+ keyboard.cleanupEsc = createEscKeyListener(stdinStream, handleCancel);
3213
+ }
3214
+ }
3215
+ };
3216
+ const handleQuit = () => {
3217
+ keyboard.cleanupEsc?.();
3218
+ keyboard.cleanupSigint?.();
3219
+ progress.complete();
3220
+ printer.ensureNewline();
3221
+ const summary = renderOverallSummary({
3222
+ totalTokens: usage?.totalTokens,
3223
+ iterations,
3224
+ elapsedSeconds: progress.getTotalElapsedSeconds(),
3225
+ cost: progress.getTotalCost()
3226
+ });
3227
+ if (summary) {
3228
+ env.stderr.write(`${chalk5.dim("\u2500".repeat(40))}
3229
+ `);
3230
+ env.stderr.write(`${summary}
3231
+ `);
3232
+ }
3233
+ env.stderr.write(chalk5.dim("[Quit]\n"));
3234
+ process.exit(130);
3235
+ };
3236
+ if (stdinIsInteractive && stdinStream.isTTY) {
3237
+ keyboard.cleanupEsc = createEscKeyListener(stdinStream, handleCancel);
2938
3238
  }
2939
- try {
2940
- validatePrompts(prompts, configPath);
2941
- } catch (error) {
2942
- if (error instanceof TemplateError) {
2943
- throw new ConfigError(error.message, configPath);
3239
+ keyboard.cleanupSigint = createSigintListener(
3240
+ handleCancel,
3241
+ handleQuit,
3242
+ () => isStreaming && !abortController.signal.aborted,
3243
+ env.stderr
3244
+ );
3245
+ const DEFAULT_APPROVAL_REQUIRED = ["RunCommand", "WriteFile", "EditFile"];
3246
+ const userApprovals = options.gadgetApproval ?? {};
3247
+ const gadgetApprovals = {
3248
+ ...userApprovals
3249
+ };
3250
+ for (const gadget of DEFAULT_APPROVAL_REQUIRED) {
3251
+ const normalizedGadget = gadget.toLowerCase();
3252
+ const isConfigured = Object.keys(userApprovals).some(
3253
+ (key) => key.toLowerCase() === normalizedGadget
3254
+ );
3255
+ if (!isConfigured) {
3256
+ gadgetApprovals[gadget] = "approval-required";
2944
3257
  }
2945
- throw error;
2946
3258
  }
2947
- for (const [name, template] of Object.entries(prompts)) {
3259
+ const approvalConfig = {
3260
+ gadgetApprovals,
3261
+ defaultMode: "allowed"
3262
+ };
3263
+ const approvalManager = new ApprovalManager(approvalConfig, env, progress);
3264
+ let usage;
3265
+ let iterations = 0;
3266
+ const llmRequestsDir = resolveLogDir(options.logLlmRequests, "requests");
3267
+ const llmResponsesDir = resolveLogDir(options.logLlmResponses, "responses");
3268
+ let llmCallCounter = 0;
3269
+ const countMessagesTokens = async (model, messages) => {
2948
3270
  try {
2949
- validateEnvVars(template, name, configPath);
2950
- } catch (error) {
2951
- if (error instanceof TemplateError) {
2952
- throw new ConfigError(error.message, configPath);
2953
- }
2954
- throw error;
3271
+ return await client.countTokens(model, messages);
3272
+ } catch {
3273
+ const totalChars = messages.reduce((sum, m) => sum + (m.content?.length ?? 0), 0);
3274
+ return Math.round(totalChars / FALLBACK_CHARS_PER_TOKEN);
2955
3275
  }
2956
- }
2957
- const eta = createTemplateEngine(prompts, configPath);
2958
- const result = { ...config };
2959
- for (const [sectionName, section] of Object.entries(config)) {
2960
- if (sectionName === "global" || sectionName === "prompts") continue;
2961
- if (!section || typeof section !== "object") continue;
2962
- const sectionObj = section;
2963
- if (typeof sectionObj.system === "string" && hasTemplateSyntax(sectionObj.system)) {
2964
- try {
2965
- validateEnvVars(sectionObj.system, void 0, configPath);
2966
- } catch (error) {
2967
- if (error instanceof TemplateError) {
2968
- throw new ConfigError(`[${sectionName}].system: ${error.message}`, configPath);
3276
+ };
3277
+ const countGadgetOutputTokens = async (output) => {
3278
+ if (!output) return void 0;
3279
+ try {
3280
+ const messages = [{ role: "assistant", content: output }];
3281
+ return await client.countTokens(options.model, messages);
3282
+ } catch {
3283
+ return void 0;
3284
+ }
3285
+ };
3286
+ const builder = new AgentBuilder(client).withModel(options.model).withLogger(env.createLogger("llmist:cli:agent")).withHooks({
3287
+ observers: {
3288
+ // onLLMCallStart: Start progress indicator for each LLM call
3289
+ // This showcases how to react to agent lifecycle events
3290
+ onLLMCallStart: async (context) => {
3291
+ isStreaming = true;
3292
+ llmCallCounter++;
3293
+ const inputTokens = await countMessagesTokens(
3294
+ context.options.model,
3295
+ context.options.messages
3296
+ );
3297
+ progress.startCall(context.options.model, inputTokens);
3298
+ progress.setInputTokens(inputTokens, false);
3299
+ if (llmRequestsDir) {
3300
+ const filename = `${Date.now()}_call_${llmCallCounter}.request.txt`;
3301
+ const content = formatLlmRequest(context.options.messages);
3302
+ await writeLogFile(llmRequestsDir, filename, content);
3303
+ }
3304
+ },
3305
+ // onStreamChunk: Real-time updates as LLM generates tokens
3306
+ // This enables responsive UIs that show progress during generation
3307
+ onStreamChunk: async (context) => {
3308
+ progress.update(context.accumulatedText.length);
3309
+ if (context.usage) {
3310
+ if (context.usage.inputTokens) {
3311
+ progress.setInputTokens(context.usage.inputTokens, false);
3312
+ }
3313
+ if (context.usage.outputTokens) {
3314
+ progress.setOutputTokens(context.usage.outputTokens, false);
3315
+ }
3316
+ progress.setCachedTokens(
3317
+ context.usage.cachedInputTokens ?? 0,
3318
+ context.usage.cacheCreationInputTokens ?? 0
3319
+ );
3320
+ }
3321
+ },
3322
+ // onLLMCallComplete: Finalize metrics after each LLM call
3323
+ // This is where you'd typically log metrics or update dashboards
3324
+ onLLMCallComplete: async (context) => {
3325
+ isStreaming = false;
3326
+ usage = context.usage;
3327
+ iterations = Math.max(iterations, context.iteration + 1);
3328
+ if (context.usage) {
3329
+ if (context.usage.inputTokens) {
3330
+ progress.setInputTokens(context.usage.inputTokens, false);
3331
+ }
3332
+ if (context.usage.outputTokens) {
3333
+ progress.setOutputTokens(context.usage.outputTokens, false);
3334
+ }
3335
+ }
3336
+ let callCost;
3337
+ if (context.usage && client.modelRegistry) {
3338
+ try {
3339
+ const modelName = context.options.model.includes(":") ? context.options.model.split(":")[1] : context.options.model;
3340
+ const costResult = client.modelRegistry.estimateCost(
3341
+ modelName,
3342
+ context.usage.inputTokens,
3343
+ context.usage.outputTokens,
3344
+ context.usage.cachedInputTokens ?? 0,
3345
+ context.usage.cacheCreationInputTokens ?? 0
3346
+ );
3347
+ if (costResult) callCost = costResult.totalCost;
3348
+ } catch {
3349
+ }
3350
+ }
3351
+ const callElapsed = progress.getCallElapsedSeconds();
3352
+ progress.endCall(context.usage);
3353
+ if (!options.quiet) {
3354
+ const summary = renderSummary({
3355
+ iterations: context.iteration + 1,
3356
+ model: options.model,
3357
+ usage: context.usage,
3358
+ elapsedSeconds: callElapsed,
3359
+ cost: callCost,
3360
+ finishReason: context.finishReason
3361
+ });
3362
+ if (summary) {
3363
+ env.stderr.write(`${summary}
3364
+ `);
3365
+ }
3366
+ }
3367
+ if (llmResponsesDir) {
3368
+ const filename = `${Date.now()}_call_${llmCallCounter}.response.txt`;
3369
+ await writeLogFile(llmResponsesDir, filename, context.rawResponse);
2969
3370
  }
2970
- throw error;
2971
3371
  }
2972
- try {
2973
- const resolved = resolveTemplate(eta, sectionObj.system, {}, configPath);
2974
- result[sectionName] = {
2975
- ...sectionObj,
2976
- system: resolved
2977
- };
2978
- } catch (error) {
2979
- if (error instanceof TemplateError) {
2980
- throw new ConfigError(`[${sectionName}].system: ${error.message}`, configPath);
3372
+ },
3373
+ // SHOWCASE: Controller-based approval gating for gadgets
3374
+ //
3375
+ // This demonstrates how to add safety layers WITHOUT modifying gadgets.
3376
+ // The ApprovalManager handles approval flows externally via beforeGadgetExecution.
3377
+ // Approval modes are configurable via cli.toml:
3378
+ // - "allowed": auto-proceed
3379
+ // - "denied": auto-reject, return message to LLM
3380
+ // - "approval-required": prompt user interactively
3381
+ //
3382
+ // Default: RunCommand, WriteFile, EditFile require approval unless overridden.
3383
+ controllers: {
3384
+ beforeGadgetExecution: async (ctx) => {
3385
+ const mode = approvalManager.getApprovalMode(ctx.gadgetName);
3386
+ if (mode === "allowed") {
3387
+ return { action: "proceed" };
3388
+ }
3389
+ const stdinTTY = isInteractive(env.stdin);
3390
+ const stderrTTY2 = env.stderr.isTTY === true;
3391
+ const canPrompt = stdinTTY && stderrTTY2;
3392
+ if (!canPrompt) {
3393
+ if (mode === "approval-required") {
3394
+ return {
3395
+ action: "skip",
3396
+ syntheticResult: `status=denied
3397
+
3398
+ ${ctx.gadgetName} requires interactive approval. Run in a terminal to approve.`
3399
+ };
3400
+ }
3401
+ if (mode === "denied") {
3402
+ return {
3403
+ action: "skip",
3404
+ syntheticResult: `status=denied
3405
+
3406
+ ${ctx.gadgetName} is denied by configuration.`
3407
+ };
3408
+ }
3409
+ return { action: "proceed" };
3410
+ }
3411
+ const result = await approvalManager.requestApproval(ctx.gadgetName, ctx.parameters);
3412
+ if (!result.approved) {
3413
+ return {
3414
+ action: "skip",
3415
+ syntheticResult: `status=denied
3416
+
3417
+ Denied: ${result.reason ?? "by user"}`
3418
+ };
2981
3419
  }
2982
- throw error;
3420
+ return { action: "proceed" };
2983
3421
  }
2984
3422
  }
3423
+ });
3424
+ if (options.system) {
3425
+ builder.withSystem(options.system);
2985
3426
  }
2986
- return result;
2987
- }
2988
- function resolveGadgets(section, inheritedGadgets, sectionName, configPath) {
2989
- const hasGadgets = "gadgets" in section;
2990
- const hasGadgetLegacy = "gadget" in section;
2991
- const hasGadgetAdd = "gadget-add" in section;
2992
- const hasGadgetRemove = "gadget-remove" in section;
2993
- if (hasGadgetLegacy && !hasGadgets) {
2994
- console.warn(
2995
- `[config] Warning: [${sectionName}].gadget is deprecated, use 'gadgets' (plural) instead`
2996
- );
3427
+ if (options.maxIterations !== void 0) {
3428
+ builder.withMaxIterations(options.maxIterations);
2997
3429
  }
2998
- if ((hasGadgets || hasGadgetLegacy) && (hasGadgetAdd || hasGadgetRemove)) {
2999
- throw new ConfigError(
3000
- `[${sectionName}] Cannot use 'gadgets' with 'gadget-add'/'gadget-remove'. Use either full replacement (gadgets) OR modification (gadget-add/gadget-remove).`,
3001
- configPath
3002
- );
3430
+ if (options.temperature !== void 0) {
3431
+ builder.withTemperature(options.temperature);
3003
3432
  }
3004
- if (hasGadgets) {
3005
- return section.gadgets;
3433
+ const humanInputHandler = createHumanInputHandler(env, progress, keyboard);
3434
+ if (humanInputHandler) {
3435
+ builder.onHumanInput(humanInputHandler);
3006
3436
  }
3007
- if (hasGadgetLegacy) {
3008
- return section.gadget;
3437
+ builder.withSignal(abortController.signal);
3438
+ const gadgets = registry.getAll();
3439
+ if (gadgets.length > 0) {
3440
+ builder.withGadgets(...gadgets);
3009
3441
  }
3010
- let result = [...inheritedGadgets];
3011
- if (hasGadgetRemove) {
3012
- const toRemove = new Set(section["gadget-remove"]);
3013
- result = result.filter((g) => !toRemove.has(g));
3442
+ if (options.gadgetStartPrefix) {
3443
+ builder.withGadgetStartPrefix(options.gadgetStartPrefix);
3014
3444
  }
3015
- if (hasGadgetAdd) {
3016
- const toAdd = section["gadget-add"];
3017
- result.push(...toAdd);
3445
+ if (options.gadgetEndPrefix) {
3446
+ builder.withGadgetEndPrefix(options.gadgetEndPrefix);
3018
3447
  }
3019
- return result;
3020
- }
3021
- function resolveInheritance(config, configPath) {
3022
- const resolved = {};
3023
- const resolving = /* @__PURE__ */ new Set();
3024
- function resolveSection(name) {
3025
- if (name in resolved) {
3026
- return resolved[name];
3448
+ if (options.gadgetArgPrefix) {
3449
+ builder.withGadgetArgPrefix(options.gadgetArgPrefix);
3450
+ }
3451
+ builder.withSyntheticGadgetCall(
3452
+ "TellUser",
3453
+ {
3454
+ 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?",
3455
+ done: false,
3456
+ type: "info"
3457
+ },
3458
+ "\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?"
3459
+ );
3460
+ builder.withTextOnlyHandler("acknowledge");
3461
+ builder.withTextWithGadgetsHandler({
3462
+ gadgetName: "TellUser",
3463
+ parameterMapping: (text) => ({ message: text, done: false, type: "info" }),
3464
+ resultMapping: (text) => `\u2139\uFE0F ${text}`
3465
+ });
3466
+ const agent = builder.ask(prompt);
3467
+ let textBuffer = "";
3468
+ const flushTextBuffer = () => {
3469
+ if (textBuffer) {
3470
+ const output = options.quiet ? textBuffer : renderMarkdownWithSeparators(textBuffer);
3471
+ printer.write(output);
3472
+ textBuffer = "";
3027
3473
  }
3028
- if (resolving.has(name)) {
3029
- throw new ConfigError(`Circular inheritance detected: ${name}`, configPath);
3474
+ };
3475
+ try {
3476
+ for await (const event of agent.run()) {
3477
+ if (event.type === "text") {
3478
+ progress.pause();
3479
+ textBuffer += event.content;
3480
+ } else if (event.type === "gadget_result") {
3481
+ flushTextBuffer();
3482
+ progress.pause();
3483
+ if (options.quiet) {
3484
+ if (event.result.gadgetName === "TellUser" && event.result.parameters?.message) {
3485
+ const message = String(event.result.parameters.message);
3486
+ env.stdout.write(`${message}
3487
+ `);
3488
+ }
3489
+ } else {
3490
+ const tokenCount = await countGadgetOutputTokens(event.result.result);
3491
+ env.stderr.write(`${formatGadgetSummary({ ...event.result, tokenCount })}
3492
+ `);
3493
+ }
3494
+ }
3030
3495
  }
3031
- const section = config[name];
3032
- if (section === void 0 || typeof section !== "object") {
3033
- throw new ConfigError(`Cannot inherit from unknown section: ${name}`, configPath);
3496
+ } catch (error) {
3497
+ if (!isAbortError(error)) {
3498
+ throw error;
3034
3499
  }
3035
- resolving.add(name);
3036
- const sectionObj = section;
3037
- const inheritsRaw = sectionObj.inherits;
3038
- const inheritsList = inheritsRaw ? Array.isArray(inheritsRaw) ? inheritsRaw : [inheritsRaw] : [];
3039
- let merged = {};
3040
- for (const parent of inheritsList) {
3041
- const parentResolved = resolveSection(parent);
3042
- merged = { ...merged, ...parentResolved };
3500
+ } finally {
3501
+ isStreaming = false;
3502
+ keyboard.cleanupEsc?.();
3503
+ keyboard.cleanupSigint?.();
3504
+ }
3505
+ flushTextBuffer();
3506
+ progress.complete();
3507
+ printer.ensureNewline();
3508
+ if (!options.quiet && iterations > 1) {
3509
+ env.stderr.write(`${chalk5.dim("\u2500".repeat(40))}
3510
+ `);
3511
+ const summary = renderOverallSummary({
3512
+ totalTokens: usage?.totalTokens,
3513
+ iterations,
3514
+ elapsedSeconds: progress.getTotalElapsedSeconds(),
3515
+ cost: progress.getTotalCost()
3516
+ });
3517
+ if (summary) {
3518
+ env.stderr.write(`${summary}
3519
+ `);
3043
3520
  }
3044
- const inheritedGadgets = merged.gadgets ?? [];
3045
- const {
3046
- inherits: _inherits,
3047
- gadgets: _gadgets,
3048
- gadget: _gadget,
3049
- "gadget-add": _gadgetAdd,
3050
- "gadget-remove": _gadgetRemove,
3051
- ...ownValues
3052
- } = sectionObj;
3053
- merged = { ...merged, ...ownValues };
3054
- const resolvedGadgets = resolveGadgets(sectionObj, inheritedGadgets, name, configPath);
3055
- if (resolvedGadgets.length > 0) {
3056
- merged.gadgets = resolvedGadgets;
3521
+ }
3522
+ }
3523
+ function registerAgentCommand(program, env, config) {
3524
+ 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.");
3525
+ addAgentOptions(cmd, config);
3526
+ cmd.action(
3527
+ (prompt, options) => executeAction(() => {
3528
+ const mergedOptions = {
3529
+ ...options,
3530
+ gadgetApproval: config?.["gadget-approval"]
3531
+ };
3532
+ return executeAgent(prompt, mergedOptions, env);
3533
+ }, env)
3534
+ );
3535
+ }
3536
+
3537
+ // src/cli/complete-command.ts
3538
+ init_messages();
3539
+ init_model_shortcuts();
3540
+ init_constants();
3541
+ async function executeComplete(promptArg, options, env) {
3542
+ const prompt = await resolvePrompt(promptArg, env);
3543
+ const client = env.createClient();
3544
+ const model = resolveModel(options.model);
3545
+ const builder = new LLMMessageBuilder();
3546
+ if (options.system) {
3547
+ builder.addSystem(options.system);
3548
+ }
3549
+ builder.addUser(prompt);
3550
+ const messages = builder.build();
3551
+ const llmRequestsDir = resolveLogDir(options.logLlmRequests, "requests");
3552
+ const llmResponsesDir = resolveLogDir(options.logLlmResponses, "responses");
3553
+ const timestamp = Date.now();
3554
+ if (llmRequestsDir) {
3555
+ const filename = `${timestamp}_complete.request.txt`;
3556
+ const content = formatLlmRequest(messages);
3557
+ await writeLogFile(llmRequestsDir, filename, content);
3558
+ }
3559
+ const stream = client.stream({
3560
+ model,
3561
+ messages,
3562
+ temperature: options.temperature,
3563
+ maxTokens: options.maxTokens
3564
+ });
3565
+ const printer = new StreamPrinter(env.stdout);
3566
+ const stderrTTY = env.stderr.isTTY === true;
3567
+ const progress = new StreamProgress(env.stderr, stderrTTY, client.modelRegistry);
3568
+ const estimatedInputTokens = Math.round(prompt.length / FALLBACK_CHARS_PER_TOKEN);
3569
+ progress.startCall(model, estimatedInputTokens);
3570
+ let finishReason;
3571
+ let usage;
3572
+ let accumulatedResponse = "";
3573
+ for await (const chunk of stream) {
3574
+ if (chunk.usage) {
3575
+ usage = chunk.usage;
3576
+ if (chunk.usage.inputTokens) {
3577
+ progress.setInputTokens(chunk.usage.inputTokens, false);
3578
+ }
3579
+ if (chunk.usage.outputTokens) {
3580
+ progress.setOutputTokens(chunk.usage.outputTokens, false);
3581
+ }
3582
+ }
3583
+ if (chunk.text) {
3584
+ progress.pause();
3585
+ accumulatedResponse += chunk.text;
3586
+ progress.update(accumulatedResponse.length);
3587
+ printer.write(chunk.text);
3588
+ }
3589
+ if (chunk.finishReason !== void 0) {
3590
+ finishReason = chunk.finishReason;
3057
3591
  }
3058
- delete merged["gadget"];
3059
- delete merged["gadget-add"];
3060
- delete merged["gadget-remove"];
3061
- resolving.delete(name);
3062
- resolved[name] = merged;
3063
- return merged;
3064
3592
  }
3065
- for (const name of Object.keys(config)) {
3066
- resolveSection(name);
3593
+ progress.endCall(usage);
3594
+ progress.complete();
3595
+ printer.ensureNewline();
3596
+ if (llmResponsesDir) {
3597
+ const filename = `${timestamp}_complete.response.txt`;
3598
+ await writeLogFile(llmResponsesDir, filename, accumulatedResponse);
3067
3599
  }
3068
- return resolved;
3600
+ if (stderrTTY && !options.quiet) {
3601
+ const summary = renderSummary({ finishReason, usage, cost: progress.getTotalCost() });
3602
+ if (summary) {
3603
+ env.stderr.write(`${summary}
3604
+ `);
3605
+ }
3606
+ }
3607
+ }
3608
+ function registerCompleteCommand(program, env, config) {
3609
+ 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.");
3610
+ addCompleteOptions(cmd, config);
3611
+ cmd.action(
3612
+ (prompt, options) => executeAction(() => executeComplete(prompt, options, env), env)
3613
+ );
3069
3614
  }
3070
3615
 
3071
3616
  // src/cli/gadget-command.ts
@@ -3755,7 +4300,11 @@ function createCommandEnvironment(baseEnv, config) {
3755
4300
  logFile: config["log-file"] ?? baseEnv.loggerConfig?.logFile,
3756
4301
  logReset: config["log-reset"] ?? baseEnv.loggerConfig?.logReset
3757
4302
  };
3758
- return createDefaultEnvironment(loggerConfig);
4303
+ return {
4304
+ ...baseEnv,
4305
+ loggerConfig,
4306
+ createLogger: createLoggerFactory(loggerConfig)
4307
+ };
3759
4308
  }
3760
4309
  function registerCustomCommand(program, name, config, env) {
3761
4310
  const type = config.type ?? "agent";
@@ -3831,7 +4380,12 @@ async function runCLI(overrides = {}) {
3831
4380
  logReset: globalOpts.logReset ?? config.global?.["log-reset"]
3832
4381
  };
3833
4382
  const defaultEnv = createDefaultEnvironment(loggerConfig);
3834
- const env = { ...defaultEnv, ...envOverrides };
4383
+ const env = {
4384
+ ...defaultEnv,
4385
+ ...envOverrides,
4386
+ // Pass Docker config from [docker] section
4387
+ dockerConfig: config.docker
4388
+ };
3835
4389
  const program = createProgram(env, config);
3836
4390
  await program.parseAsync(env.argv);
3837
4391
  }