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.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import "./chunk-LFSIEPAE.js";
2
+ import "./chunk-LSCCBXS7.js";
3
3
  import {
4
4
  AgentBuilder,
5
5
  BaseGadget,
@@ -27,7 +27,7 @@ import {
27
27
  resolveModel,
28
28
  schemaToJSONSchema,
29
29
  validateGadgetSchema
30
- } from "./chunk-LBHWVCZ2.js";
30
+ } from "./chunk-PDYVT3FI.js";
31
31
 
32
32
  // src/cli/constants.ts
33
33
  var CLI_NAME = "llmist";
@@ -51,7 +51,6 @@ var OPTION_FLAGS = {
51
51
  logFile: "--log-file <path>",
52
52
  logReset: "--log-reset",
53
53
  logLlmRequests: "--log-llm-requests [dir]",
54
- logLlmResponses: "--log-llm-responses [dir]",
55
54
  noBuiltins: "--no-builtins",
56
55
  noBuiltinInteraction: "--no-builtin-interaction",
57
56
  quiet: "-q, --quiet",
@@ -70,8 +69,7 @@ var OPTION_DESCRIPTIONS = {
70
69
  logLevel: "Log level: silly, trace, debug, info, warn, error, fatal.",
71
70
  logFile: "Path to log file. When set, logs are written to file instead of stderr.",
72
71
  logReset: "Reset (truncate) the log file at session start instead of appending.",
73
- logLlmRequests: "Save raw LLM requests as plain text. Optional dir, defaults to ~/.llmist/logs/requests/",
74
- logLlmResponses: "Save raw LLM responses as plain text. Optional dir, defaults to ~/.llmist/logs/responses/",
72
+ logLlmRequests: "Save LLM requests/responses to session directories. Optional dir, defaults to ~/.llmist/logs/requests/",
75
73
  noBuiltins: "Disable built-in gadgets (AskUser, TellUser).",
76
74
  noBuiltinInteraction: "Disable interactive gadgets (AskUser) while keeping TellUser.",
77
75
  quiet: "Suppress all output except content (text and TellUser messages).",
@@ -88,7 +86,7 @@ import { Command, InvalidArgumentError as InvalidArgumentError2 } from "commande
88
86
  // package.json
89
87
  var package_default = {
90
88
  name: "llmist",
91
- version: "1.7.0",
89
+ version: "2.0.0",
92
90
  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.",
93
91
  type: "module",
94
92
  main: "dist/index.cjs",
@@ -261,6 +259,14 @@ ${addedLines}`;
261
259
  }
262
260
 
263
261
  // src/cli/approval/context-providers.ts
262
+ function formatGadgetSummary(gadgetName, params) {
263
+ const paramEntries = Object.entries(params);
264
+ if (paramEntries.length === 0) {
265
+ return `${gadgetName}()`;
266
+ }
267
+ const paramStr = paramEntries.map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(", ");
268
+ return `${gadgetName}(${paramStr})`;
269
+ }
264
270
  var WriteFileContextProvider = class {
265
271
  gadgetName = "WriteFile";
266
272
  async getContext(params) {
@@ -269,14 +275,14 @@ var WriteFileContextProvider = class {
269
275
  const resolvedPath = resolve(process.cwd(), filePath);
270
276
  if (!existsSync(resolvedPath)) {
271
277
  return {
272
- summary: `Create new file: ${filePath}`,
278
+ summary: formatGadgetSummary(this.gadgetName, params),
273
279
  details: formatNewFileDiff(filePath, newContent)
274
280
  };
275
281
  }
276
282
  const oldContent = readFileSync(resolvedPath, "utf-8");
277
283
  const diff = createPatch(filePath, oldContent, newContent, "original", "modified");
278
284
  return {
279
- summary: `Modify: ${filePath}`,
285
+ summary: formatGadgetSummary(this.gadgetName, params),
280
286
  details: diff
281
287
  };
282
288
  }
@@ -290,37 +296,27 @@ var EditFileContextProvider = class {
290
296
  const newContent = String(params.content);
291
297
  if (!existsSync(resolvedPath)) {
292
298
  return {
293
- summary: `Create new file: ${filePath}`,
299
+ summary: formatGadgetSummary(this.gadgetName, params),
294
300
  details: formatNewFileDiff(filePath, newContent)
295
301
  };
296
302
  }
297
303
  const oldContent = readFileSync(resolvedPath, "utf-8");
298
304
  const diff = createPatch(filePath, oldContent, newContent, "original", "modified");
299
305
  return {
300
- summary: `Modify: ${filePath}`,
306
+ summary: formatGadgetSummary(this.gadgetName, params),
301
307
  details: diff
302
308
  };
303
309
  }
304
310
  if ("commands" in params) {
305
311
  const commands = String(params.commands);
306
312
  return {
307
- summary: `Edit: ${filePath}`,
313
+ summary: formatGadgetSummary(this.gadgetName, params),
308
314
  details: `Commands:
309
315
  ${commands}`
310
316
  };
311
317
  }
312
318
  return {
313
- summary: `Edit: ${filePath}`
314
- };
315
- }
316
- };
317
- var RunCommandContextProvider = class {
318
- gadgetName = "RunCommand";
319
- async getContext(params) {
320
- const command = String(params.command ?? "");
321
- const cwd = params.cwd ? ` (in ${params.cwd})` : "";
322
- return {
323
- summary: `Execute: ${command}${cwd}`
319
+ summary: formatGadgetSummary(this.gadgetName, params)
324
320
  };
325
321
  }
326
322
  };
@@ -329,27 +325,15 @@ var DefaultContextProvider = class {
329
325
  this.gadgetName = gadgetName;
330
326
  }
331
327
  async getContext(params) {
332
- const paramEntries = Object.entries(params);
333
- if (paramEntries.length === 0) {
334
- return {
335
- summary: `${this.gadgetName}()`
336
- };
337
- }
338
- const formatValue = (value) => {
339
- const MAX_LEN = 50;
340
- const str = JSON.stringify(value);
341
- return str.length > MAX_LEN ? `${str.slice(0, MAX_LEN - 3)}...` : str;
342
- };
343
- const paramStr = paramEntries.map(([k, v]) => `${k}=${formatValue(v)}`).join(", ");
344
328
  return {
345
- summary: `${this.gadgetName}(${paramStr})`
329
+ summary: formatGadgetSummary(this.gadgetName, params)
346
330
  };
347
331
  }
348
332
  };
349
333
  var builtinContextProviders = [
350
334
  new WriteFileContextProvider(),
351
- new EditFileContextProvider(),
352
- new RunCommandContextProvider()
335
+ new EditFileContextProvider()
336
+ // Note: RunCommand uses DefaultContextProvider - no custom details needed
353
337
  ];
354
338
 
355
339
  // src/cli/approval/manager.ts
@@ -360,11 +344,13 @@ var ApprovalManager = class {
360
344
  * @param config - Approval configuration with per-gadget modes
361
345
  * @param env - CLI environment for I/O operations
362
346
  * @param progress - Optional progress indicator to pause during prompts
347
+ * @param keyboard - Optional keyboard coordinator to disable ESC listener during prompts
363
348
  */
364
- constructor(config, env, progress) {
349
+ constructor(config, env, progress, keyboard) {
365
350
  this.config = config;
366
351
  this.env = env;
367
352
  this.progress = progress;
353
+ this.keyboard = keyboard;
368
354
  for (const provider of builtinContextProviders) {
369
355
  this.registerProvider(provider);
370
356
  }
@@ -433,26 +419,34 @@ var ApprovalManager = class {
433
419
  const provider = this.providers.get(gadgetName.toLowerCase()) ?? new DefaultContextProvider(gadgetName);
434
420
  const context = await provider.getContext(params);
435
421
  this.progress?.pause();
436
- this.env.stderr.write(`
422
+ if (this.keyboard?.cleanupEsc) {
423
+ this.keyboard.cleanupEsc();
424
+ this.keyboard.cleanupEsc = null;
425
+ }
426
+ try {
427
+ this.env.stderr.write(`
437
428
  ${chalk2.yellow("\u{1F512} Approval required:")} ${context.summary}
438
429
  `);
439
- if (context.details) {
440
- this.env.stderr.write(`
430
+ if (context.details) {
431
+ this.env.stderr.write(`
441
432
  ${renderColoredDiff(context.details)}
442
433
  `);
443
- }
444
- const response = await this.prompt(" \u23CE approve, or type to reject: ");
445
- const isApproved = response === "" || response.toLowerCase() === "y";
446
- if (isApproved) {
447
- this.env.stderr.write(` ${chalk2.green("\u2713 Approved")}
434
+ }
435
+ const response = await this.prompt(" \u23CE approve, or type to reject: ");
436
+ const isApproved = response === "" || response.toLowerCase() === "y";
437
+ if (isApproved) {
438
+ this.env.stderr.write(` ${chalk2.green("\u2713 Approved")}
448
439
 
449
440
  `);
450
- return { approved: true };
451
- }
452
- this.env.stderr.write(` ${chalk2.red("\u2717 Denied")}
441
+ return { approved: true };
442
+ }
443
+ this.env.stderr.write(` ${chalk2.red("\u2717 Denied")}
453
444
 
454
445
  `);
455
- return { approved: false, reason: response || "Rejected by user" };
446
+ return { approved: false, reason: response || "Rejected by user" };
447
+ } finally {
448
+ this.keyboard?.restore();
449
+ }
456
450
  }
457
451
  /**
458
452
  * Prompts for user input.
@@ -922,6 +916,22 @@ var runCommand = createGadget({
922
916
  params: { argv: ["gh", "pr", "review", "123", "--comment", "--body", "Review with `backticks` and 'quotes'"], timeout: 3e4 },
923
917
  output: "status=0\n\n(no output)",
924
918
  comment: "Complex arguments with special characters - no escaping needed"
919
+ },
920
+ {
921
+ params: {
922
+ argv: [
923
+ "gh",
924
+ "pr",
925
+ "review",
926
+ "123",
927
+ "--approve",
928
+ "--body",
929
+ "## Review Summary\n\n**Looks good!**\n\n- Clean code\n- Tests pass"
930
+ ],
931
+ timeout: 3e4
932
+ },
933
+ output: "status=0\n\nApproving pull request #123",
934
+ comment: "Multiline body: --body flag and content must be SEPARATE array elements"
925
935
  }
926
936
  ],
927
937
  execute: async ({ argv, cwd, timeout }) => {
@@ -929,6 +939,7 @@ var runCommand = createGadget({
929
939
  if (argv.length === 0) {
930
940
  return "status=1\n\nerror: argv array cannot be empty";
931
941
  }
942
+ let timeoutId;
932
943
  try {
933
944
  const proc = Bun.spawn(argv, {
934
945
  cwd: workingDir,
@@ -936,12 +947,15 @@ var runCommand = createGadget({
936
947
  stderr: "pipe"
937
948
  });
938
949
  const timeoutPromise = new Promise((_, reject) => {
939
- setTimeout(() => {
950
+ timeoutId = setTimeout(() => {
940
951
  proc.kill();
941
952
  reject(new Error(`Command timed out after ${timeout}ms`));
942
953
  }, timeout);
943
954
  });
944
955
  const exitCode = await Promise.race([proc.exited, timeoutPromise]);
956
+ if (timeoutId) {
957
+ clearTimeout(timeoutId);
958
+ }
945
959
  const stdout = await new Response(proc.stdout).text();
946
960
  const stderr = await new Response(proc.stderr).text();
947
961
  const output = [stdout, stderr].filter(Boolean).join("\n").trim();
@@ -949,6 +963,9 @@ var runCommand = createGadget({
949
963
 
950
964
  ${output || "(no output)"}`;
951
965
  } catch (error) {
966
+ if (timeoutId) {
967
+ clearTimeout(timeoutId);
968
+ }
952
969
  const message = error instanceof Error ? error.message : String(error);
953
970
  return `status=1
954
971
 
@@ -1121,6 +1138,30 @@ async function writeLogFile(dir, filename, content) {
1121
1138
  await mkdir(dir, { recursive: true });
1122
1139
  await writeFile2(join(dir, filename), content, "utf-8");
1123
1140
  }
1141
+ function formatSessionTimestamp(date = /* @__PURE__ */ new Date()) {
1142
+ const pad = (n) => n.toString().padStart(2, "0");
1143
+ const year = date.getFullYear();
1144
+ const month = pad(date.getMonth() + 1);
1145
+ const day = pad(date.getDate());
1146
+ const hours = pad(date.getHours());
1147
+ const minutes = pad(date.getMinutes());
1148
+ const seconds = pad(date.getSeconds());
1149
+ return `${year}-${month}-${day}_${hours}-${minutes}-${seconds}`;
1150
+ }
1151
+ async function createSessionDir(baseDir) {
1152
+ const timestamp = formatSessionTimestamp();
1153
+ const sessionDir = join(baseDir, timestamp);
1154
+ try {
1155
+ await mkdir(sessionDir, { recursive: true });
1156
+ return sessionDir;
1157
+ } catch (error) {
1158
+ console.warn(`[llmist] Failed to create log session directory: ${sessionDir}`, error);
1159
+ return void 0;
1160
+ }
1161
+ }
1162
+ function formatCallNumber(n) {
1163
+ return n.toString().padStart(4, "0");
1164
+ }
1124
1165
 
1125
1166
  // src/cli/utils.ts
1126
1167
  init_constants();
@@ -1284,7 +1325,7 @@ function formatBytes(bytes) {
1284
1325
  }
1285
1326
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1286
1327
  }
1287
- function formatGadgetSummary(result) {
1328
+ function formatGadgetSummary2(result) {
1288
1329
  const gadgetLabel = chalk3.magenta.bold(result.gadgetName);
1289
1330
  const timeLabel = chalk3.dim(`${Math.round(result.executionTimeMs)}ms`);
1290
1331
  const paramsStr = formatParametersInline(result.parameters);
@@ -1369,12 +1410,21 @@ function isInteractive(stream) {
1369
1410
  }
1370
1411
  var ESC_KEY = 27;
1371
1412
  var ESC_TIMEOUT_MS = 50;
1372
- function createEscKeyListener(stdin, onEsc) {
1413
+ var CTRL_C = 3;
1414
+ function createEscKeyListener(stdin, onEsc, onCtrlC) {
1373
1415
  if (!stdin.isTTY || typeof stdin.setRawMode !== "function") {
1374
1416
  return null;
1375
1417
  }
1376
1418
  let escTimeout = null;
1377
1419
  const handleData = (data) => {
1420
+ if (data[0] === CTRL_C && onCtrlC) {
1421
+ if (escTimeout) {
1422
+ clearTimeout(escTimeout);
1423
+ escTimeout = null;
1424
+ }
1425
+ onCtrlC();
1426
+ return;
1427
+ }
1378
1428
  if (data[0] === ESC_KEY) {
1379
1429
  if (data.length === 1) {
1380
1430
  escTimeout = setTimeout(() => {
@@ -1822,7 +1872,7 @@ function addCompleteOptions(cmd, defaults) {
1822
1872
  OPTION_DESCRIPTIONS.maxTokens,
1823
1873
  createNumericParser({ label: "Max tokens", integer: true, min: 1 }),
1824
1874
  defaults?.["max-tokens"]
1825
- ).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"]);
1875
+ ).option(OPTION_FLAGS.quiet, OPTION_DESCRIPTIONS.quiet, defaults?.quiet).option(OPTION_FLAGS.logLlmRequests, OPTION_DESCRIPTIONS.logLlmRequests, defaults?.["log-llm-requests"]);
1826
1876
  }
1827
1877
  function addAgentOptions(cmd, defaults) {
1828
1878
  const gadgetAccumulator = (value, previous = []) => [
@@ -1846,7 +1896,7 @@ function addAgentOptions(cmd, defaults) {
1846
1896
  OPTION_FLAGS.noBuiltinInteraction,
1847
1897
  OPTION_DESCRIPTIONS.noBuiltinInteraction,
1848
1898
  defaults?.["builtin-interaction"] !== false
1849
- ).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);
1899
+ ).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);
1850
1900
  }
1851
1901
  function configToCompleteOptions(config) {
1852
1902
  const result = {};
@@ -1856,7 +1906,6 @@ function configToCompleteOptions(config) {
1856
1906
  if (config["max-tokens"] !== void 0) result.maxTokens = config["max-tokens"];
1857
1907
  if (config.quiet !== void 0) result.quiet = config.quiet;
1858
1908
  if (config["log-llm-requests"] !== void 0) result.logLlmRequests = config["log-llm-requests"];
1859
- if (config["log-llm-responses"] !== void 0) result.logLlmResponses = config["log-llm-responses"];
1860
1909
  return result;
1861
1910
  }
1862
1911
  function configToAgentOptions(config) {
@@ -1880,7 +1929,6 @@ function configToAgentOptions(config) {
1880
1929
  result.gadgetApproval = config["gadget-approval"];
1881
1930
  if (config.quiet !== void 0) result.quiet = config.quiet;
1882
1931
  if (config["log-llm-requests"] !== void 0) result.logLlmRequests = config["log-llm-requests"];
1883
- if (config["log-llm-responses"] !== void 0) result.logLlmResponses = config["log-llm-responses"];
1884
1932
  if (config.docker !== void 0) result.docker = config.docker;
1885
1933
  if (config["docker-cwd-permission"] !== void 0)
1886
1934
  result.dockerCwdPermission = config["docker-cwd-permission"];
@@ -1898,7 +1946,8 @@ var DOCKER_CONFIG_KEYS = /* @__PURE__ */ new Set([
1898
1946
  "env-vars",
1899
1947
  "image-name",
1900
1948
  "dev-mode",
1901
- "dev-source"
1949
+ "dev-source",
1950
+ "docker-args"
1902
1951
  ]);
1903
1952
  var DEFAULT_IMAGE_NAME = "llmist-sandbox";
1904
1953
  var DEFAULT_CWD_PERMISSION = "rw";
@@ -2013,7 +2062,6 @@ var COMPLETE_CONFIG_KEYS = /* @__PURE__ */ new Set([
2013
2062
  "log-file",
2014
2063
  "log-reset",
2015
2064
  "log-llm-requests",
2016
- "log-llm-responses",
2017
2065
  "type",
2018
2066
  // Allowed for inheritance compatibility, ignored for built-in commands
2019
2067
  "docker",
@@ -2046,7 +2094,6 @@ var AGENT_CONFIG_KEYS = /* @__PURE__ */ new Set([
2046
2094
  "log-file",
2047
2095
  "log-reset",
2048
2096
  "log-llm-requests",
2049
- "log-llm-responses",
2050
2097
  "type",
2051
2098
  // Allowed for inheritance compatibility, ignored for built-in commands
2052
2099
  "docker",
@@ -2234,13 +2281,6 @@ function validateCompleteConfig(raw, section) {
2234
2281
  section
2235
2282
  );
2236
2283
  }
2237
- if ("log-llm-responses" in rawObj) {
2238
- result["log-llm-responses"] = validateStringOrBoolean(
2239
- rawObj["log-llm-responses"],
2240
- "log-llm-responses",
2241
- section
2242
- );
2243
- }
2244
2284
  return result;
2245
2285
  }
2246
2286
  function validateAgentConfig(raw, section) {
@@ -2319,13 +2359,6 @@ function validateAgentConfig(raw, section) {
2319
2359
  section
2320
2360
  );
2321
2361
  }
2322
- if ("log-llm-responses" in rawObj) {
2323
- result["log-llm-responses"] = validateStringOrBoolean(
2324
- rawObj["log-llm-responses"],
2325
- "log-llm-responses",
2326
- section
2327
- );
2328
- }
2329
2362
  return result;
2330
2363
  }
2331
2364
  function validateStringOrBoolean(value, field, section) {
@@ -2767,6 +2800,9 @@ function validateDockerConfig(raw, section) {
2767
2800
  if ("dev-source" in rawObj) {
2768
2801
  result["dev-source"] = validateString2(rawObj["dev-source"], "dev-source", section);
2769
2802
  }
2803
+ if ("docker-args" in rawObj) {
2804
+ result["docker-args"] = validateStringArray2(rawObj["docker-args"], "docker-args", section);
2805
+ }
2770
2806
  return result;
2771
2807
  }
2772
2808
 
@@ -2778,6 +2814,8 @@ FROM oven/bun:1-debian
2778
2814
 
2779
2815
  # Install essential tools
2780
2816
  RUN apt-get update && apt-get install -y --no-install-recommends \\
2817
+ # ed for EditFile gadget (line-oriented editor)
2818
+ ed \\
2781
2819
  # ripgrep for fast file searching
2782
2820
  ripgrep \\
2783
2821
  # git for version control operations
@@ -2810,6 +2848,7 @@ FROM oven/bun:1-debian
2810
2848
 
2811
2849
  # Install essential tools (same as production)
2812
2850
  RUN apt-get update && apt-get install -y --no-install-recommends \\
2851
+ ed \\
2813
2852
  ripgrep \\
2814
2853
  git \\
2815
2854
  curl \\
@@ -3044,6 +3083,9 @@ function buildDockerRunArgs(ctx, imageName, devMode) {
3044
3083
  }
3045
3084
  }
3046
3085
  }
3086
+ if (ctx.config["docker-args"]) {
3087
+ args.push(...ctx.config["docker-args"]);
3088
+ }
3047
3089
  args.push(imageName);
3048
3090
  args.push(...ctx.forwardArgs);
3049
3091
  return args;
@@ -3210,6 +3252,8 @@ async function executeAgent(promptArg, options, env) {
3210
3252
  env.stderr.write(chalk5.yellow(`
3211
3253
  [Cancelled] ${progress.formatStats()}
3212
3254
  `));
3255
+ } else {
3256
+ handleQuit();
3213
3257
  }
3214
3258
  };
3215
3259
  const keyboard = {
@@ -3217,7 +3261,7 @@ async function executeAgent(promptArg, options, env) {
3217
3261
  cleanupSigint: null,
3218
3262
  restore: () => {
3219
3263
  if (stdinIsInteractive && stdinStream.isTTY && !wasCancelled) {
3220
- keyboard.cleanupEsc = createEscKeyListener(stdinStream, handleCancel);
3264
+ keyboard.cleanupEsc = createEscKeyListener(stdinStream, handleCancel, handleCancel);
3221
3265
  }
3222
3266
  }
3223
3267
  };
@@ -3242,7 +3286,7 @@ async function executeAgent(promptArg, options, env) {
3242
3286
  process.exit(130);
3243
3287
  };
3244
3288
  if (stdinIsInteractive && stdinStream.isTTY) {
3245
- keyboard.cleanupEsc = createEscKeyListener(stdinStream, handleCancel);
3289
+ keyboard.cleanupEsc = createEscKeyListener(stdinStream, handleCancel, handleCancel);
3246
3290
  }
3247
3291
  keyboard.cleanupSigint = createSigintListener(
3248
3292
  handleCancel,
@@ -3268,11 +3312,11 @@ async function executeAgent(promptArg, options, env) {
3268
3312
  gadgetApprovals,
3269
3313
  defaultMode: "allowed"
3270
3314
  };
3271
- const approvalManager = new ApprovalManager(approvalConfig, env, progress);
3315
+ const approvalManager = new ApprovalManager(approvalConfig, env, progress, keyboard);
3272
3316
  let usage;
3273
3317
  let iterations = 0;
3274
- const llmRequestsDir = resolveLogDir(options.logLlmRequests, "requests");
3275
- const llmResponsesDir = resolveLogDir(options.logLlmResponses, "responses");
3318
+ const llmLogsBaseDir = resolveLogDir(options.logLlmRequests, "requests");
3319
+ let llmSessionDir;
3276
3320
  let llmCallCounter = 0;
3277
3321
  const countMessagesTokens = async (model, messages) => {
3278
3322
  try {
@@ -3304,10 +3348,19 @@ async function executeAgent(promptArg, options, env) {
3304
3348
  );
3305
3349
  progress.startCall(context.options.model, inputTokens);
3306
3350
  progress.setInputTokens(inputTokens, false);
3307
- if (llmRequestsDir) {
3308
- const filename = `${Date.now()}_call_${llmCallCounter}.request.txt`;
3309
- const content = formatLlmRequest(context.options.messages);
3310
- await writeLogFile(llmRequestsDir, filename, content);
3351
+ },
3352
+ // onLLMCallReady: Log the exact request being sent to the LLM
3353
+ // This fires AFTER controller modifications (e.g., trailing messages)
3354
+ onLLMCallReady: async (context) => {
3355
+ if (llmLogsBaseDir) {
3356
+ if (!llmSessionDir) {
3357
+ llmSessionDir = await createSessionDir(llmLogsBaseDir);
3358
+ }
3359
+ if (llmSessionDir) {
3360
+ const filename = `${formatCallNumber(llmCallCounter)}.request`;
3361
+ const content = formatLlmRequest(context.options.messages);
3362
+ await writeLogFile(llmSessionDir, filename, content);
3363
+ }
3311
3364
  }
3312
3365
  },
3313
3366
  // onStreamChunk: Real-time updates as LLM generates tokens
@@ -3372,9 +3425,9 @@ async function executeAgent(promptArg, options, env) {
3372
3425
  `);
3373
3426
  }
3374
3427
  }
3375
- if (llmResponsesDir) {
3376
- const filename = `${Date.now()}_call_${llmCallCounter}.response.txt`;
3377
- await writeLogFile(llmResponsesDir, filename, context.rawResponse);
3428
+ if (llmSessionDir) {
3429
+ const filename = `${formatCallNumber(llmCallCounter)}.response`;
3430
+ await writeLogFile(llmSessionDir, filename, context.rawResponse);
3378
3431
  }
3379
3432
  }
3380
3433
  },
@@ -3471,6 +3524,13 @@ Denied: ${result.reason ?? "by user"}`
3471
3524
  parameterMapping: (text) => ({ message: text, done: false, type: "info" }),
3472
3525
  resultMapping: (text) => `\u2139\uFE0F ${text}`
3473
3526
  });
3527
+ builder.withTrailingMessage(
3528
+ (ctx) => [
3529
+ `[Iteration ${ctx.iteration + 1}/${ctx.maxIterations}]`,
3530
+ "Think carefully: what gadget invocations can you make in parallel right now?",
3531
+ "Maximize efficiency by batching independent operations in a single response."
3532
+ ].join(" ")
3533
+ );
3474
3534
  const agent = builder.ask(prompt);
3475
3535
  let textBuffer = "";
3476
3536
  const flushTextBuffer = () => {
@@ -3496,7 +3556,7 @@ Denied: ${result.reason ?? "by user"}`
3496
3556
  }
3497
3557
  } else {
3498
3558
  const tokenCount = await countGadgetOutputTokens(event.result.result);
3499
- env.stderr.write(`${formatGadgetSummary({ ...event.result, tokenCount })}
3559
+ env.stderr.write(`${formatGadgetSummary2({ ...event.result, tokenCount })}
3500
3560
  `);
3501
3561
  }
3502
3562
  }
@@ -3508,7 +3568,10 @@ Denied: ${result.reason ?? "by user"}`
3508
3568
  } finally {
3509
3569
  isStreaming = false;
3510
3570
  keyboard.cleanupEsc?.();
3511
- keyboard.cleanupSigint?.();
3571
+ if (keyboard.cleanupSigint) {
3572
+ keyboard.cleanupSigint();
3573
+ process.once("SIGINT", () => process.exit(130));
3574
+ }
3512
3575
  }
3513
3576
  flushTextBuffer();
3514
3577
  progress.complete();
@@ -3556,13 +3619,15 @@ async function executeComplete(promptArg, options, env) {
3556
3619
  }
3557
3620
  builder.addUser(prompt);
3558
3621
  const messages = builder.build();
3559
- const llmRequestsDir = resolveLogDir(options.logLlmRequests, "requests");
3560
- const llmResponsesDir = resolveLogDir(options.logLlmResponses, "responses");
3561
- const timestamp = Date.now();
3562
- if (llmRequestsDir) {
3563
- const filename = `${timestamp}_complete.request.txt`;
3564
- const content = formatLlmRequest(messages);
3565
- await writeLogFile(llmRequestsDir, filename, content);
3622
+ const llmLogsBaseDir = resolveLogDir(options.logLlmRequests, "requests");
3623
+ let llmSessionDir;
3624
+ if (llmLogsBaseDir) {
3625
+ llmSessionDir = await createSessionDir(llmLogsBaseDir);
3626
+ if (llmSessionDir) {
3627
+ const filename = "0001.request";
3628
+ const content = formatLlmRequest(messages);
3629
+ await writeLogFile(llmSessionDir, filename, content);
3630
+ }
3566
3631
  }
3567
3632
  const stream = client.stream({
3568
3633
  model,
@@ -3601,9 +3666,9 @@ async function executeComplete(promptArg, options, env) {
3601
3666
  progress.endCall(usage);
3602
3667
  progress.complete();
3603
3668
  printer.ensureNewline();
3604
- if (llmResponsesDir) {
3605
- const filename = `${timestamp}_complete.response.txt`;
3606
- await writeLogFile(llmResponsesDir, filename, accumulatedResponse);
3669
+ if (llmSessionDir) {
3670
+ const filename = "0001.response";
3671
+ await writeLogFile(llmSessionDir, filename, accumulatedResponse);
3607
3672
  }
3608
3673
  if (stderrTTY && !options.quiet) {
3609
3674
  const summary = renderSummary({ finishReason, usage, cost: progress.getTotalCost() });