llmist 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -1020,6 +1020,100 @@ var init_gadget = __esm({
1020
1020
  throw new AbortError();
1021
1021
  }
1022
1022
  }
1023
+ /**
1024
+ * Register a cleanup function to run when execution is aborted (timeout or cancellation).
1025
+ * The cleanup function is called immediately if the signal is already aborted.
1026
+ * Errors thrown by the cleanup function are silently ignored.
1027
+ *
1028
+ * Use this to clean up resources like browser instances, database connections,
1029
+ * or child processes when the gadget is cancelled due to timeout.
1030
+ *
1031
+ * @param ctx - The execution context containing the abort signal
1032
+ * @param cleanup - Function to run on abort (can be sync or async)
1033
+ *
1034
+ * @example
1035
+ * ```typescript
1036
+ * class BrowserGadget extends Gadget({
1037
+ * description: 'Fetches web page content',
1038
+ * schema: z.object({ url: z.string() }),
1039
+ * }) {
1040
+ * async execute(params: this['params'], ctx?: ExecutionContext): Promise<string> {
1041
+ * const browser = await chromium.launch();
1042
+ * this.onAbort(ctx, () => browser.close());
1043
+ *
1044
+ * const page = await browser.newPage();
1045
+ * this.onAbort(ctx, () => page.close());
1046
+ *
1047
+ * await page.goto(params.url);
1048
+ * const content = await page.content();
1049
+ *
1050
+ * await browser.close();
1051
+ * return content;
1052
+ * }
1053
+ * }
1054
+ * ```
1055
+ */
1056
+ onAbort(ctx, cleanup) {
1057
+ if (!ctx?.signal) return;
1058
+ const safeCleanup = () => {
1059
+ try {
1060
+ const result = cleanup();
1061
+ if (result && typeof result === "object" && "catch" in result) {
1062
+ result.catch(() => {
1063
+ });
1064
+ }
1065
+ } catch {
1066
+ }
1067
+ };
1068
+ if (ctx.signal.aborted) {
1069
+ safeCleanup();
1070
+ return;
1071
+ }
1072
+ ctx.signal.addEventListener("abort", safeCleanup, { once: true });
1073
+ }
1074
+ /**
1075
+ * Create an AbortController linked to the execution context's signal.
1076
+ * When the parent signal aborts, the returned controller also aborts with the same reason.
1077
+ *
1078
+ * Useful for passing abort signals to child operations like fetch() while still
1079
+ * being able to abort them independently if needed.
1080
+ *
1081
+ * @param ctx - The execution context containing the parent abort signal
1082
+ * @returns A new AbortController linked to the parent signal
1083
+ *
1084
+ * @example
1085
+ * ```typescript
1086
+ * class FetchGadget extends Gadget({
1087
+ * description: 'Fetches data from URL',
1088
+ * schema: z.object({ url: z.string() }),
1089
+ * }) {
1090
+ * async execute(params: this['params'], ctx?: ExecutionContext): Promise<string> {
1091
+ * const controller = this.createLinkedAbortController(ctx);
1092
+ *
1093
+ * // fetch() will automatically abort when parent times out
1094
+ * const response = await fetch(params.url, { signal: controller.signal });
1095
+ * return response.text();
1096
+ * }
1097
+ * }
1098
+ * ```
1099
+ */
1100
+ createLinkedAbortController(ctx) {
1101
+ const controller = new AbortController();
1102
+ if (ctx?.signal) {
1103
+ if (ctx.signal.aborted) {
1104
+ controller.abort(ctx.signal.reason);
1105
+ } else {
1106
+ ctx.signal.addEventListener(
1107
+ "abort",
1108
+ () => {
1109
+ controller.abort(ctx.signal.reason);
1110
+ },
1111
+ { once: true }
1112
+ );
1113
+ }
1114
+ }
1115
+ return controller;
1116
+ }
1023
1117
  /**
1024
1118
  * Auto-generated instruction text for the LLM.
1025
1119
  * Combines name, description, and parameter schema into a formatted instruction.
@@ -3802,6 +3896,17 @@ var init_agent = __esm({
3802
3896
  llmOptions = { ...llmOptions, ...action.modifiedOptions };
3803
3897
  }
3804
3898
  }
3899
+ await this.safeObserve(async () => {
3900
+ if (this.hooks.observers?.onLLMCallReady) {
3901
+ const context = {
3902
+ iteration: currentIteration,
3903
+ maxIterations: this.maxIterations,
3904
+ options: llmOptions,
3905
+ logger: this.logger
3906
+ };
3907
+ await this.hooks.observers.onLLMCallReady(context);
3908
+ }
3909
+ });
3805
3910
  this.logger.info("Calling LLM", { model: this.model });
3806
3911
  this.logger.silly("LLM request details", {
3807
3912
  model: llmOptions.model,
@@ -6888,7 +6993,6 @@ var OPTION_FLAGS = {
6888
6993
  logFile: "--log-file <path>",
6889
6994
  logReset: "--log-reset",
6890
6995
  logLlmRequests: "--log-llm-requests [dir]",
6891
- logLlmResponses: "--log-llm-responses [dir]",
6892
6996
  noBuiltins: "--no-builtins",
6893
6997
  noBuiltinInteraction: "--no-builtin-interaction",
6894
6998
  quiet: "-q, --quiet",
@@ -6907,8 +7011,7 @@ var OPTION_DESCRIPTIONS = {
6907
7011
  logLevel: "Log level: silly, trace, debug, info, warn, error, fatal.",
6908
7012
  logFile: "Path to log file. When set, logs are written to file instead of stderr.",
6909
7013
  logReset: "Reset (truncate) the log file at session start instead of appending.",
6910
- logLlmRequests: "Save raw LLM requests as plain text. Optional dir, defaults to ~/.llmist/logs/requests/",
6911
- logLlmResponses: "Save raw LLM responses as plain text. Optional dir, defaults to ~/.llmist/logs/responses/",
7014
+ logLlmRequests: "Save LLM requests/responses to session directories. Optional dir, defaults to ~/.llmist/logs/requests/",
6912
7015
  noBuiltins: "Disable built-in gadgets (AskUser, TellUser).",
6913
7016
  noBuiltinInteraction: "Disable interactive gadgets (AskUser) while keeping TellUser.",
6914
7017
  quiet: "Suppress all output except content (text and TellUser messages).",
@@ -6925,7 +7028,7 @@ var import_commander2 = require("commander");
6925
7028
  // package.json
6926
7029
  var package_default = {
6927
7030
  name: "llmist",
6928
- version: "1.7.0",
7031
+ version: "2.0.0",
6929
7032
  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.",
6930
7033
  type: "module",
6931
7034
  main: "dist/index.cjs",
@@ -7098,6 +7201,14 @@ ${addedLines}`;
7098
7201
  }
7099
7202
 
7100
7203
  // src/cli/approval/context-providers.ts
7204
+ function formatGadgetSummary(gadgetName, params) {
7205
+ const paramEntries = Object.entries(params);
7206
+ if (paramEntries.length === 0) {
7207
+ return `${gadgetName}()`;
7208
+ }
7209
+ const paramStr = paramEntries.map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(", ");
7210
+ return `${gadgetName}(${paramStr})`;
7211
+ }
7101
7212
  var WriteFileContextProvider = class {
7102
7213
  gadgetName = "WriteFile";
7103
7214
  async getContext(params) {
@@ -7106,14 +7217,14 @@ var WriteFileContextProvider = class {
7106
7217
  const resolvedPath = (0, import_node_path2.resolve)(process.cwd(), filePath);
7107
7218
  if (!(0, import_node_fs2.existsSync)(resolvedPath)) {
7108
7219
  return {
7109
- summary: `Create new file: ${filePath}`,
7220
+ summary: formatGadgetSummary(this.gadgetName, params),
7110
7221
  details: formatNewFileDiff(filePath, newContent)
7111
7222
  };
7112
7223
  }
7113
7224
  const oldContent = (0, import_node_fs2.readFileSync)(resolvedPath, "utf-8");
7114
7225
  const diff = (0, import_diff.createPatch)(filePath, oldContent, newContent, "original", "modified");
7115
7226
  return {
7116
- summary: `Modify: ${filePath}`,
7227
+ summary: formatGadgetSummary(this.gadgetName, params),
7117
7228
  details: diff
7118
7229
  };
7119
7230
  }
@@ -7127,37 +7238,27 @@ var EditFileContextProvider = class {
7127
7238
  const newContent = String(params.content);
7128
7239
  if (!(0, import_node_fs2.existsSync)(resolvedPath)) {
7129
7240
  return {
7130
- summary: `Create new file: ${filePath}`,
7241
+ summary: formatGadgetSummary(this.gadgetName, params),
7131
7242
  details: formatNewFileDiff(filePath, newContent)
7132
7243
  };
7133
7244
  }
7134
7245
  const oldContent = (0, import_node_fs2.readFileSync)(resolvedPath, "utf-8");
7135
7246
  const diff = (0, import_diff.createPatch)(filePath, oldContent, newContent, "original", "modified");
7136
7247
  return {
7137
- summary: `Modify: ${filePath}`,
7248
+ summary: formatGadgetSummary(this.gadgetName, params),
7138
7249
  details: diff
7139
7250
  };
7140
7251
  }
7141
7252
  if ("commands" in params) {
7142
7253
  const commands = String(params.commands);
7143
7254
  return {
7144
- summary: `Edit: ${filePath}`,
7255
+ summary: formatGadgetSummary(this.gadgetName, params),
7145
7256
  details: `Commands:
7146
7257
  ${commands}`
7147
7258
  };
7148
7259
  }
7149
7260
  return {
7150
- summary: `Edit: ${filePath}`
7151
- };
7152
- }
7153
- };
7154
- var RunCommandContextProvider = class {
7155
- gadgetName = "RunCommand";
7156
- async getContext(params) {
7157
- const command = String(params.command ?? "");
7158
- const cwd = params.cwd ? ` (in ${params.cwd})` : "";
7159
- return {
7160
- summary: `Execute: ${command}${cwd}`
7261
+ summary: formatGadgetSummary(this.gadgetName, params)
7161
7262
  };
7162
7263
  }
7163
7264
  };
@@ -7166,27 +7267,15 @@ var DefaultContextProvider = class {
7166
7267
  this.gadgetName = gadgetName;
7167
7268
  }
7168
7269
  async getContext(params) {
7169
- const paramEntries = Object.entries(params);
7170
- if (paramEntries.length === 0) {
7171
- return {
7172
- summary: `${this.gadgetName}()`
7173
- };
7174
- }
7175
- const formatValue = (value) => {
7176
- const MAX_LEN = 50;
7177
- const str = JSON.stringify(value);
7178
- return str.length > MAX_LEN ? `${str.slice(0, MAX_LEN - 3)}...` : str;
7179
- };
7180
- const paramStr = paramEntries.map(([k, v]) => `${k}=${formatValue(v)}`).join(", ");
7181
7270
  return {
7182
- summary: `${this.gadgetName}(${paramStr})`
7271
+ summary: formatGadgetSummary(this.gadgetName, params)
7183
7272
  };
7184
7273
  }
7185
7274
  };
7186
7275
  var builtinContextProviders = [
7187
7276
  new WriteFileContextProvider(),
7188
- new EditFileContextProvider(),
7189
- new RunCommandContextProvider()
7277
+ new EditFileContextProvider()
7278
+ // Note: RunCommand uses DefaultContextProvider - no custom details needed
7190
7279
  ];
7191
7280
 
7192
7281
  // src/cli/approval/manager.ts
@@ -7197,11 +7286,13 @@ var ApprovalManager = class {
7197
7286
  * @param config - Approval configuration with per-gadget modes
7198
7287
  * @param env - CLI environment for I/O operations
7199
7288
  * @param progress - Optional progress indicator to pause during prompts
7289
+ * @param keyboard - Optional keyboard coordinator to disable ESC listener during prompts
7200
7290
  */
7201
- constructor(config, env, progress) {
7291
+ constructor(config, env, progress, keyboard) {
7202
7292
  this.config = config;
7203
7293
  this.env = env;
7204
7294
  this.progress = progress;
7295
+ this.keyboard = keyboard;
7205
7296
  for (const provider of builtinContextProviders) {
7206
7297
  this.registerProvider(provider);
7207
7298
  }
@@ -7270,26 +7361,34 @@ var ApprovalManager = class {
7270
7361
  const provider = this.providers.get(gadgetName.toLowerCase()) ?? new DefaultContextProvider(gadgetName);
7271
7362
  const context = await provider.getContext(params);
7272
7363
  this.progress?.pause();
7273
- this.env.stderr.write(`
7364
+ if (this.keyboard?.cleanupEsc) {
7365
+ this.keyboard.cleanupEsc();
7366
+ this.keyboard.cleanupEsc = null;
7367
+ }
7368
+ try {
7369
+ this.env.stderr.write(`
7274
7370
  ${import_chalk2.default.yellow("\u{1F512} Approval required:")} ${context.summary}
7275
7371
  `);
7276
- if (context.details) {
7277
- this.env.stderr.write(`
7372
+ if (context.details) {
7373
+ this.env.stderr.write(`
7278
7374
  ${renderColoredDiff(context.details)}
7279
7375
  `);
7280
- }
7281
- const response = await this.prompt(" \u23CE approve, or type to reject: ");
7282
- const isApproved = response === "" || response.toLowerCase() === "y";
7283
- if (isApproved) {
7284
- this.env.stderr.write(` ${import_chalk2.default.green("\u2713 Approved")}
7376
+ }
7377
+ const response = await this.prompt(" \u23CE approve, or type to reject: ");
7378
+ const isApproved = response === "" || response.toLowerCase() === "y";
7379
+ if (isApproved) {
7380
+ this.env.stderr.write(` ${import_chalk2.default.green("\u2713 Approved")}
7285
7381
 
7286
7382
  `);
7287
- return { approved: true };
7288
- }
7289
- this.env.stderr.write(` ${import_chalk2.default.red("\u2717 Denied")}
7383
+ return { approved: true };
7384
+ }
7385
+ this.env.stderr.write(` ${import_chalk2.default.red("\u2717 Denied")}
7290
7386
 
7291
7387
  `);
7292
- return { approved: false, reason: response || "Rejected by user" };
7388
+ return { approved: false, reason: response || "Rejected by user" };
7389
+ } finally {
7390
+ this.keyboard?.restore();
7391
+ }
7293
7392
  }
7294
7393
  /**
7295
7394
  * Prompts for user input.
@@ -7819,6 +7918,22 @@ var runCommand = createGadget({
7819
7918
  params: { argv: ["gh", "pr", "review", "123", "--comment", "--body", "Review with `backticks` and 'quotes'"], timeout: 3e4 },
7820
7919
  output: "status=0\n\n(no output)",
7821
7920
  comment: "Complex arguments with special characters - no escaping needed"
7921
+ },
7922
+ {
7923
+ params: {
7924
+ argv: [
7925
+ "gh",
7926
+ "pr",
7927
+ "review",
7928
+ "123",
7929
+ "--approve",
7930
+ "--body",
7931
+ "## Review Summary\n\n**Looks good!**\n\n- Clean code\n- Tests pass"
7932
+ ],
7933
+ timeout: 3e4
7934
+ },
7935
+ output: "status=0\n\nApproving pull request #123",
7936
+ comment: "Multiline body: --body flag and content must be SEPARATE array elements"
7822
7937
  }
7823
7938
  ],
7824
7939
  execute: async ({ argv, cwd, timeout }) => {
@@ -7826,6 +7941,7 @@ var runCommand = createGadget({
7826
7941
  if (argv.length === 0) {
7827
7942
  return "status=1\n\nerror: argv array cannot be empty";
7828
7943
  }
7944
+ let timeoutId;
7829
7945
  try {
7830
7946
  const proc = Bun.spawn(argv, {
7831
7947
  cwd: workingDir,
@@ -7833,12 +7949,15 @@ var runCommand = createGadget({
7833
7949
  stderr: "pipe"
7834
7950
  });
7835
7951
  const timeoutPromise = new Promise((_, reject) => {
7836
- setTimeout(() => {
7952
+ timeoutId = setTimeout(() => {
7837
7953
  proc.kill();
7838
7954
  reject(new Error(`Command timed out after ${timeout}ms`));
7839
7955
  }, timeout);
7840
7956
  });
7841
7957
  const exitCode = await Promise.race([proc.exited, timeoutPromise]);
7958
+ if (timeoutId) {
7959
+ clearTimeout(timeoutId);
7960
+ }
7842
7961
  const stdout = await new Response(proc.stdout).text();
7843
7962
  const stderr = await new Response(proc.stderr).text();
7844
7963
  const output = [stdout, stderr].filter(Boolean).join("\n").trim();
@@ -7846,6 +7965,9 @@ var runCommand = createGadget({
7846
7965
 
7847
7966
  ${output || "(no output)"}`;
7848
7967
  } catch (error) {
7968
+ if (timeoutId) {
7969
+ clearTimeout(timeoutId);
7970
+ }
7849
7971
  const message = error instanceof Error ? error.message : String(error);
7850
7972
  return `status=1
7851
7973
 
@@ -8018,6 +8140,30 @@ async function writeLogFile(dir, filename, content) {
8018
8140
  await (0, import_promises2.mkdir)(dir, { recursive: true });
8019
8141
  await (0, import_promises2.writeFile)((0, import_node_path7.join)(dir, filename), content, "utf-8");
8020
8142
  }
8143
+ function formatSessionTimestamp(date = /* @__PURE__ */ new Date()) {
8144
+ const pad = (n) => n.toString().padStart(2, "0");
8145
+ const year = date.getFullYear();
8146
+ const month = pad(date.getMonth() + 1);
8147
+ const day = pad(date.getDate());
8148
+ const hours = pad(date.getHours());
8149
+ const minutes = pad(date.getMinutes());
8150
+ const seconds = pad(date.getSeconds());
8151
+ return `${year}-${month}-${day}_${hours}-${minutes}-${seconds}`;
8152
+ }
8153
+ async function createSessionDir(baseDir) {
8154
+ const timestamp = formatSessionTimestamp();
8155
+ const sessionDir = (0, import_node_path7.join)(baseDir, timestamp);
8156
+ try {
8157
+ await (0, import_promises2.mkdir)(sessionDir, { recursive: true });
8158
+ return sessionDir;
8159
+ } catch (error) {
8160
+ console.warn(`[llmist] Failed to create log session directory: ${sessionDir}`, error);
8161
+ return void 0;
8162
+ }
8163
+ }
8164
+ function formatCallNumber(n) {
8165
+ return n.toString().padStart(4, "0");
8166
+ }
8021
8167
 
8022
8168
  // src/cli/utils.ts
8023
8169
  var import_chalk4 = __toESM(require("chalk"), 1);
@@ -8181,7 +8327,7 @@ function formatBytes(bytes) {
8181
8327
  }
8182
8328
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
8183
8329
  }
8184
- function formatGadgetSummary(result) {
8330
+ function formatGadgetSummary2(result) {
8185
8331
  const gadgetLabel = import_chalk3.default.magenta.bold(result.gadgetName);
8186
8332
  const timeLabel = import_chalk3.default.dim(`${Math.round(result.executionTimeMs)}ms`);
8187
8333
  const paramsStr = formatParametersInline(result.parameters);
@@ -8266,12 +8412,21 @@ function isInteractive(stream2) {
8266
8412
  }
8267
8413
  var ESC_KEY = 27;
8268
8414
  var ESC_TIMEOUT_MS = 50;
8269
- function createEscKeyListener(stdin, onEsc) {
8415
+ var CTRL_C = 3;
8416
+ function createEscKeyListener(stdin, onEsc, onCtrlC) {
8270
8417
  if (!stdin.isTTY || typeof stdin.setRawMode !== "function") {
8271
8418
  return null;
8272
8419
  }
8273
8420
  let escTimeout = null;
8274
8421
  const handleData = (data) => {
8422
+ if (data[0] === CTRL_C && onCtrlC) {
8423
+ if (escTimeout) {
8424
+ clearTimeout(escTimeout);
8425
+ escTimeout = null;
8426
+ }
8427
+ onCtrlC();
8428
+ return;
8429
+ }
8275
8430
  if (data[0] === ESC_KEY) {
8276
8431
  if (data.length === 1) {
8277
8432
  escTimeout = setTimeout(() => {
@@ -8719,7 +8874,7 @@ function addCompleteOptions(cmd, defaults) {
8719
8874
  OPTION_DESCRIPTIONS.maxTokens,
8720
8875
  createNumericParser({ label: "Max tokens", integer: true, min: 1 }),
8721
8876
  defaults?.["max-tokens"]
8722
- ).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"]);
8877
+ ).option(OPTION_FLAGS.quiet, OPTION_DESCRIPTIONS.quiet, defaults?.quiet).option(OPTION_FLAGS.logLlmRequests, OPTION_DESCRIPTIONS.logLlmRequests, defaults?.["log-llm-requests"]);
8723
8878
  }
8724
8879
  function addAgentOptions(cmd, defaults) {
8725
8880
  const gadgetAccumulator = (value, previous = []) => [
@@ -8743,7 +8898,7 @@ function addAgentOptions(cmd, defaults) {
8743
8898
  OPTION_FLAGS.noBuiltinInteraction,
8744
8899
  OPTION_DESCRIPTIONS.noBuiltinInteraction,
8745
8900
  defaults?.["builtin-interaction"] !== false
8746
- ).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);
8901
+ ).option(OPTION_FLAGS.quiet, OPTION_DESCRIPTIONS.quiet, defaults?.quiet).option(OPTION_FLAGS.logLlmRequests, OPTION_DESCRIPTIONS.logLlmRequests, defaults?.["log-llm-requests"]).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);
8747
8902
  }
8748
8903
  function configToCompleteOptions(config) {
8749
8904
  const result = {};
@@ -8753,7 +8908,6 @@ function configToCompleteOptions(config) {
8753
8908
  if (config["max-tokens"] !== void 0) result.maxTokens = config["max-tokens"];
8754
8909
  if (config.quiet !== void 0) result.quiet = config.quiet;
8755
8910
  if (config["log-llm-requests"] !== void 0) result.logLlmRequests = config["log-llm-requests"];
8756
- if (config["log-llm-responses"] !== void 0) result.logLlmResponses = config["log-llm-responses"];
8757
8911
  return result;
8758
8912
  }
8759
8913
  function configToAgentOptions(config) {
@@ -8777,7 +8931,6 @@ function configToAgentOptions(config) {
8777
8931
  result.gadgetApproval = config["gadget-approval"];
8778
8932
  if (config.quiet !== void 0) result.quiet = config.quiet;
8779
8933
  if (config["log-llm-requests"] !== void 0) result.logLlmRequests = config["log-llm-requests"];
8780
- if (config["log-llm-responses"] !== void 0) result.logLlmResponses = config["log-llm-responses"];
8781
8934
  if (config.docker !== void 0) result.docker = config.docker;
8782
8935
  if (config["docker-cwd-permission"] !== void 0)
8783
8936
  result.dockerCwdPermission = config["docker-cwd-permission"];
@@ -8795,7 +8948,8 @@ var DOCKER_CONFIG_KEYS = /* @__PURE__ */ new Set([
8795
8948
  "env-vars",
8796
8949
  "image-name",
8797
8950
  "dev-mode",
8798
- "dev-source"
8951
+ "dev-source",
8952
+ "docker-args"
8799
8953
  ]);
8800
8954
  var DEFAULT_IMAGE_NAME = "llmist-sandbox";
8801
8955
  var DEFAULT_CWD_PERMISSION = "rw";
@@ -8910,7 +9064,6 @@ var COMPLETE_CONFIG_KEYS = /* @__PURE__ */ new Set([
8910
9064
  "log-file",
8911
9065
  "log-reset",
8912
9066
  "log-llm-requests",
8913
- "log-llm-responses",
8914
9067
  "type",
8915
9068
  // Allowed for inheritance compatibility, ignored for built-in commands
8916
9069
  "docker",
@@ -8943,7 +9096,6 @@ var AGENT_CONFIG_KEYS = /* @__PURE__ */ new Set([
8943
9096
  "log-file",
8944
9097
  "log-reset",
8945
9098
  "log-llm-requests",
8946
- "log-llm-responses",
8947
9099
  "type",
8948
9100
  // Allowed for inheritance compatibility, ignored for built-in commands
8949
9101
  "docker",
@@ -9131,13 +9283,6 @@ function validateCompleteConfig(raw, section) {
9131
9283
  section
9132
9284
  );
9133
9285
  }
9134
- if ("log-llm-responses" in rawObj) {
9135
- result["log-llm-responses"] = validateStringOrBoolean(
9136
- rawObj["log-llm-responses"],
9137
- "log-llm-responses",
9138
- section
9139
- );
9140
- }
9141
9286
  return result;
9142
9287
  }
9143
9288
  function validateAgentConfig(raw, section) {
@@ -9216,13 +9361,6 @@ function validateAgentConfig(raw, section) {
9216
9361
  section
9217
9362
  );
9218
9363
  }
9219
- if ("log-llm-responses" in rawObj) {
9220
- result["log-llm-responses"] = validateStringOrBoolean(
9221
- rawObj["log-llm-responses"],
9222
- "log-llm-responses",
9223
- section
9224
- );
9225
- }
9226
9364
  return result;
9227
9365
  }
9228
9366
  function validateStringOrBoolean(value, field, section) {
@@ -9664,6 +9802,9 @@ function validateDockerConfig(raw, section) {
9664
9802
  if ("dev-source" in rawObj) {
9665
9803
  result["dev-source"] = validateString2(rawObj["dev-source"], "dev-source", section);
9666
9804
  }
9805
+ if ("docker-args" in rawObj) {
9806
+ result["docker-args"] = validateStringArray2(rawObj["docker-args"], "docker-args", section);
9807
+ }
9667
9808
  return result;
9668
9809
  }
9669
9810
 
@@ -9675,6 +9816,8 @@ FROM oven/bun:1-debian
9675
9816
 
9676
9817
  # Install essential tools
9677
9818
  RUN apt-get update && apt-get install -y --no-install-recommends \\
9819
+ # ed for EditFile gadget (line-oriented editor)
9820
+ ed \\
9678
9821
  # ripgrep for fast file searching
9679
9822
  ripgrep \\
9680
9823
  # git for version control operations
@@ -9707,6 +9850,7 @@ FROM oven/bun:1-debian
9707
9850
 
9708
9851
  # Install essential tools (same as production)
9709
9852
  RUN apt-get update && apt-get install -y --no-install-recommends \\
9853
+ ed \\
9710
9854
  ripgrep \\
9711
9855
  git \\
9712
9856
  curl \\
@@ -9941,6 +10085,9 @@ function buildDockerRunArgs(ctx, imageName, devMode) {
9941
10085
  }
9942
10086
  }
9943
10087
  }
10088
+ if (ctx.config["docker-args"]) {
10089
+ args.push(...ctx.config["docker-args"]);
10090
+ }
9944
10091
  args.push(imageName);
9945
10092
  args.push(...ctx.forwardArgs);
9946
10093
  return args;
@@ -10107,6 +10254,8 @@ async function executeAgent(promptArg, options, env) {
10107
10254
  env.stderr.write(import_chalk5.default.yellow(`
10108
10255
  [Cancelled] ${progress.formatStats()}
10109
10256
  `));
10257
+ } else {
10258
+ handleQuit();
10110
10259
  }
10111
10260
  };
10112
10261
  const keyboard = {
@@ -10114,7 +10263,7 @@ async function executeAgent(promptArg, options, env) {
10114
10263
  cleanupSigint: null,
10115
10264
  restore: () => {
10116
10265
  if (stdinIsInteractive && stdinStream.isTTY && !wasCancelled) {
10117
- keyboard.cleanupEsc = createEscKeyListener(stdinStream, handleCancel);
10266
+ keyboard.cleanupEsc = createEscKeyListener(stdinStream, handleCancel, handleCancel);
10118
10267
  }
10119
10268
  }
10120
10269
  };
@@ -10139,7 +10288,7 @@ async function executeAgent(promptArg, options, env) {
10139
10288
  process.exit(130);
10140
10289
  };
10141
10290
  if (stdinIsInteractive && stdinStream.isTTY) {
10142
- keyboard.cleanupEsc = createEscKeyListener(stdinStream, handleCancel);
10291
+ keyboard.cleanupEsc = createEscKeyListener(stdinStream, handleCancel, handleCancel);
10143
10292
  }
10144
10293
  keyboard.cleanupSigint = createSigintListener(
10145
10294
  handleCancel,
@@ -10165,11 +10314,11 @@ async function executeAgent(promptArg, options, env) {
10165
10314
  gadgetApprovals,
10166
10315
  defaultMode: "allowed"
10167
10316
  };
10168
- const approvalManager = new ApprovalManager(approvalConfig, env, progress);
10317
+ const approvalManager = new ApprovalManager(approvalConfig, env, progress, keyboard);
10169
10318
  let usage;
10170
10319
  let iterations = 0;
10171
- const llmRequestsDir = resolveLogDir(options.logLlmRequests, "requests");
10172
- const llmResponsesDir = resolveLogDir(options.logLlmResponses, "responses");
10320
+ const llmLogsBaseDir = resolveLogDir(options.logLlmRequests, "requests");
10321
+ let llmSessionDir;
10173
10322
  let llmCallCounter = 0;
10174
10323
  const countMessagesTokens = async (model, messages) => {
10175
10324
  try {
@@ -10201,10 +10350,19 @@ async function executeAgent(promptArg, options, env) {
10201
10350
  );
10202
10351
  progress.startCall(context.options.model, inputTokens);
10203
10352
  progress.setInputTokens(inputTokens, false);
10204
- if (llmRequestsDir) {
10205
- const filename = `${Date.now()}_call_${llmCallCounter}.request.txt`;
10206
- const content = formatLlmRequest(context.options.messages);
10207
- await writeLogFile(llmRequestsDir, filename, content);
10353
+ },
10354
+ // onLLMCallReady: Log the exact request being sent to the LLM
10355
+ // This fires AFTER controller modifications (e.g., trailing messages)
10356
+ onLLMCallReady: async (context) => {
10357
+ if (llmLogsBaseDir) {
10358
+ if (!llmSessionDir) {
10359
+ llmSessionDir = await createSessionDir(llmLogsBaseDir);
10360
+ }
10361
+ if (llmSessionDir) {
10362
+ const filename = `${formatCallNumber(llmCallCounter)}.request`;
10363
+ const content = formatLlmRequest(context.options.messages);
10364
+ await writeLogFile(llmSessionDir, filename, content);
10365
+ }
10208
10366
  }
10209
10367
  },
10210
10368
  // onStreamChunk: Real-time updates as LLM generates tokens
@@ -10269,9 +10427,9 @@ async function executeAgent(promptArg, options, env) {
10269
10427
  `);
10270
10428
  }
10271
10429
  }
10272
- if (llmResponsesDir) {
10273
- const filename = `${Date.now()}_call_${llmCallCounter}.response.txt`;
10274
- await writeLogFile(llmResponsesDir, filename, context.rawResponse);
10430
+ if (llmSessionDir) {
10431
+ const filename = `${formatCallNumber(llmCallCounter)}.response`;
10432
+ await writeLogFile(llmSessionDir, filename, context.rawResponse);
10275
10433
  }
10276
10434
  }
10277
10435
  },
@@ -10368,6 +10526,13 @@ Denied: ${result.reason ?? "by user"}`
10368
10526
  parameterMapping: (text) => ({ message: text, done: false, type: "info" }),
10369
10527
  resultMapping: (text) => `\u2139\uFE0F ${text}`
10370
10528
  });
10529
+ builder.withTrailingMessage(
10530
+ (ctx) => [
10531
+ `[Iteration ${ctx.iteration + 1}/${ctx.maxIterations}]`,
10532
+ "Think carefully: what gadget invocations can you make in parallel right now?",
10533
+ "Maximize efficiency by batching independent operations in a single response."
10534
+ ].join(" ")
10535
+ );
10371
10536
  const agent = builder.ask(prompt);
10372
10537
  let textBuffer = "";
10373
10538
  const flushTextBuffer = () => {
@@ -10393,7 +10558,7 @@ Denied: ${result.reason ?? "by user"}`
10393
10558
  }
10394
10559
  } else {
10395
10560
  const tokenCount = await countGadgetOutputTokens(event.result.result);
10396
- env.stderr.write(`${formatGadgetSummary({ ...event.result, tokenCount })}
10561
+ env.stderr.write(`${formatGadgetSummary2({ ...event.result, tokenCount })}
10397
10562
  `);
10398
10563
  }
10399
10564
  }
@@ -10405,7 +10570,10 @@ Denied: ${result.reason ?? "by user"}`
10405
10570
  } finally {
10406
10571
  isStreaming = false;
10407
10572
  keyboard.cleanupEsc?.();
10408
- keyboard.cleanupSigint?.();
10573
+ if (keyboard.cleanupSigint) {
10574
+ keyboard.cleanupSigint();
10575
+ process.once("SIGINT", () => process.exit(130));
10576
+ }
10409
10577
  }
10410
10578
  flushTextBuffer();
10411
10579
  progress.complete();
@@ -10453,13 +10621,15 @@ async function executeComplete(promptArg, options, env) {
10453
10621
  }
10454
10622
  builder.addUser(prompt);
10455
10623
  const messages = builder.build();
10456
- const llmRequestsDir = resolveLogDir(options.logLlmRequests, "requests");
10457
- const llmResponsesDir = resolveLogDir(options.logLlmResponses, "responses");
10458
- const timestamp = Date.now();
10459
- if (llmRequestsDir) {
10460
- const filename = `${timestamp}_complete.request.txt`;
10461
- const content = formatLlmRequest(messages);
10462
- await writeLogFile(llmRequestsDir, filename, content);
10624
+ const llmLogsBaseDir = resolveLogDir(options.logLlmRequests, "requests");
10625
+ let llmSessionDir;
10626
+ if (llmLogsBaseDir) {
10627
+ llmSessionDir = await createSessionDir(llmLogsBaseDir);
10628
+ if (llmSessionDir) {
10629
+ const filename = "0001.request";
10630
+ const content = formatLlmRequest(messages);
10631
+ await writeLogFile(llmSessionDir, filename, content);
10632
+ }
10463
10633
  }
10464
10634
  const stream2 = client.stream({
10465
10635
  model,
@@ -10498,9 +10668,9 @@ async function executeComplete(promptArg, options, env) {
10498
10668
  progress.endCall(usage);
10499
10669
  progress.complete();
10500
10670
  printer.ensureNewline();
10501
- if (llmResponsesDir) {
10502
- const filename = `${timestamp}_complete.response.txt`;
10503
- await writeLogFile(llmResponsesDir, filename, accumulatedResponse);
10671
+ if (llmSessionDir) {
10672
+ const filename = "0001.response";
10673
+ await writeLogFile(llmSessionDir, filename, accumulatedResponse);
10504
10674
  }
10505
10675
  if (stderrTTY && !options.quiet) {
10506
10676
  const summary = renderSummary({ finishReason, usage, cost: progress.getTotalCost() });