llmist 1.7.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-JGORHSHC.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-E52IO2NO.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.6.2",
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.
@@ -891,49 +885,77 @@ error: ${message}`;
891
885
  import { z as z6 } from "zod";
892
886
  var runCommand = createGadget({
893
887
  name: "RunCommand",
894
- description: "Execute a shell command and return its output. Returns both stdout and stderr combined with the exit status.",
888
+ description: "Execute a command with arguments and return its output. Uses argv array to bypass shell - arguments are passed directly without interpretation. Returns stdout/stderr combined with exit status.",
895
889
  schema: z6.object({
896
- command: z6.string().describe("The shell command to execute"),
890
+ argv: z6.array(z6.string()).describe("Command and arguments as array (e.g., ['git', 'commit', '-m', 'message'])"),
897
891
  cwd: z6.string().optional().describe("Working directory for the command (default: current directory)"),
898
892
  timeout: z6.number().default(3e4).describe("Timeout in milliseconds (default: 30000)")
899
893
  }),
900
894
  examples: [
901
895
  {
902
- params: { command: "ls -la", timeout: 3e4 },
896
+ params: { argv: ["ls", "-la"], timeout: 3e4 },
903
897
  output: "status=0\n\ntotal 24\ndrwxr-xr-x 5 user staff 160 Nov 27 10:00 .\ndrwxr-xr-x 3 user staff 96 Nov 27 09:00 ..\n-rw-r--r-- 1 user staff 1024 Nov 27 10:00 package.json",
904
898
  comment: "List directory contents with details"
905
899
  },
906
900
  {
907
- params: { command: "echo 'Hello World'", timeout: 3e4 },
901
+ params: { argv: ["echo", "Hello World"], timeout: 3e4 },
908
902
  output: "status=0\n\nHello World",
909
- comment: "Simple echo command"
903
+ comment: "Echo without shell - argument passed directly"
910
904
  },
911
905
  {
912
- params: { command: "cat nonexistent.txt", timeout: 3e4 },
906
+ params: { argv: ["cat", "nonexistent.txt"], timeout: 3e4 },
913
907
  output: "status=1\n\ncat: nonexistent.txt: No such file or directory",
914
908
  comment: "Command that fails returns non-zero status"
915
909
  },
916
910
  {
917
- params: { command: "pwd", cwd: "/tmp", timeout: 3e4 },
911
+ params: { argv: ["pwd"], cwd: "/tmp", timeout: 3e4 },
918
912
  output: "status=0\n\n/tmp",
919
913
  comment: "Execute command in a specific directory"
914
+ },
915
+ {
916
+ params: { argv: ["gh", "pr", "review", "123", "--comment", "--body", "Review with `backticks` and 'quotes'"], timeout: 3e4 },
917
+ output: "status=0\n\n(no output)",
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"
920
935
  }
921
936
  ],
922
- execute: async ({ command, cwd, timeout }) => {
937
+ execute: async ({ argv, cwd, timeout }) => {
923
938
  const workingDir = cwd ?? process.cwd();
939
+ if (argv.length === 0) {
940
+ return "status=1\n\nerror: argv array cannot be empty";
941
+ }
942
+ let timeoutId;
924
943
  try {
925
- const proc = Bun.spawn(["sh", "-c", command], {
944
+ const proc = Bun.spawn(argv, {
926
945
  cwd: workingDir,
927
946
  stdout: "pipe",
928
947
  stderr: "pipe"
929
948
  });
930
949
  const timeoutPromise = new Promise((_, reject) => {
931
- setTimeout(() => {
950
+ timeoutId = setTimeout(() => {
932
951
  proc.kill();
933
952
  reject(new Error(`Command timed out after ${timeout}ms`));
934
953
  }, timeout);
935
954
  });
936
955
  const exitCode = await Promise.race([proc.exited, timeoutPromise]);
956
+ if (timeoutId) {
957
+ clearTimeout(timeoutId);
958
+ }
937
959
  const stdout = await new Response(proc.stdout).text();
938
960
  const stderr = await new Response(proc.stderr).text();
939
961
  const output = [stdout, stderr].filter(Boolean).join("\n").trim();
@@ -941,6 +963,9 @@ var runCommand = createGadget({
941
963
 
942
964
  ${output || "(no output)"}`;
943
965
  } catch (error) {
966
+ if (timeoutId) {
967
+ clearTimeout(timeoutId);
968
+ }
944
969
  const message = error instanceof Error ? error.message : String(error);
945
970
  return `status=1
946
971
 
@@ -1113,6 +1138,30 @@ async function writeLogFile(dir, filename, content) {
1113
1138
  await mkdir(dir, { recursive: true });
1114
1139
  await writeFile2(join(dir, filename), content, "utf-8");
1115
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
+ }
1116
1165
 
1117
1166
  // src/cli/utils.ts
1118
1167
  init_constants();
@@ -1276,7 +1325,7 @@ function formatBytes(bytes) {
1276
1325
  }
1277
1326
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1278
1327
  }
1279
- function formatGadgetSummary(result) {
1328
+ function formatGadgetSummary2(result) {
1280
1329
  const gadgetLabel = chalk3.magenta.bold(result.gadgetName);
1281
1330
  const timeLabel = chalk3.dim(`${Math.round(result.executionTimeMs)}ms`);
1282
1331
  const paramsStr = formatParametersInline(result.parameters);
@@ -1361,12 +1410,21 @@ function isInteractive(stream) {
1361
1410
  }
1362
1411
  var ESC_KEY = 27;
1363
1412
  var ESC_TIMEOUT_MS = 50;
1364
- function createEscKeyListener(stdin, onEsc) {
1413
+ var CTRL_C = 3;
1414
+ function createEscKeyListener(stdin, onEsc, onCtrlC) {
1365
1415
  if (!stdin.isTTY || typeof stdin.setRawMode !== "function") {
1366
1416
  return null;
1367
1417
  }
1368
1418
  let escTimeout = null;
1369
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
+ }
1370
1428
  if (data[0] === ESC_KEY) {
1371
1429
  if (data.length === 1) {
1372
1430
  escTimeout = setTimeout(() => {
@@ -1814,7 +1872,7 @@ function addCompleteOptions(cmd, defaults) {
1814
1872
  OPTION_DESCRIPTIONS.maxTokens,
1815
1873
  createNumericParser({ label: "Max tokens", integer: true, min: 1 }),
1816
1874
  defaults?.["max-tokens"]
1817
- ).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"]);
1818
1876
  }
1819
1877
  function addAgentOptions(cmd, defaults) {
1820
1878
  const gadgetAccumulator = (value, previous = []) => [
@@ -1838,7 +1896,7 @@ function addAgentOptions(cmd, defaults) {
1838
1896
  OPTION_FLAGS.noBuiltinInteraction,
1839
1897
  OPTION_DESCRIPTIONS.noBuiltinInteraction,
1840
1898
  defaults?.["builtin-interaction"] !== false
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);
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);
1842
1900
  }
1843
1901
  function configToCompleteOptions(config) {
1844
1902
  const result = {};
@@ -1848,7 +1906,6 @@ function configToCompleteOptions(config) {
1848
1906
  if (config["max-tokens"] !== void 0) result.maxTokens = config["max-tokens"];
1849
1907
  if (config.quiet !== void 0) result.quiet = config.quiet;
1850
1908
  if (config["log-llm-requests"] !== void 0) result.logLlmRequests = config["log-llm-requests"];
1851
- if (config["log-llm-responses"] !== void 0) result.logLlmResponses = config["log-llm-responses"];
1852
1909
  return result;
1853
1910
  }
1854
1911
  function configToAgentOptions(config) {
@@ -1872,7 +1929,6 @@ function configToAgentOptions(config) {
1872
1929
  result.gadgetApproval = config["gadget-approval"];
1873
1930
  if (config.quiet !== void 0) result.quiet = config.quiet;
1874
1931
  if (config["log-llm-requests"] !== void 0) result.logLlmRequests = config["log-llm-requests"];
1875
- if (config["log-llm-responses"] !== void 0) result.logLlmResponses = config["log-llm-responses"];
1876
1932
  if (config.docker !== void 0) result.docker = config.docker;
1877
1933
  if (config["docker-cwd-permission"] !== void 0)
1878
1934
  result.dockerCwdPermission = config["docker-cwd-permission"];
@@ -1890,7 +1946,8 @@ var DOCKER_CONFIG_KEYS = /* @__PURE__ */ new Set([
1890
1946
  "env-vars",
1891
1947
  "image-name",
1892
1948
  "dev-mode",
1893
- "dev-source"
1949
+ "dev-source",
1950
+ "docker-args"
1894
1951
  ]);
1895
1952
  var DEFAULT_IMAGE_NAME = "llmist-sandbox";
1896
1953
  var DEFAULT_CWD_PERMISSION = "rw";
@@ -2005,7 +2062,6 @@ var COMPLETE_CONFIG_KEYS = /* @__PURE__ */ new Set([
2005
2062
  "log-file",
2006
2063
  "log-reset",
2007
2064
  "log-llm-requests",
2008
- "log-llm-responses",
2009
2065
  "type",
2010
2066
  // Allowed for inheritance compatibility, ignored for built-in commands
2011
2067
  "docker",
@@ -2038,7 +2094,6 @@ var AGENT_CONFIG_KEYS = /* @__PURE__ */ new Set([
2038
2094
  "log-file",
2039
2095
  "log-reset",
2040
2096
  "log-llm-requests",
2041
- "log-llm-responses",
2042
2097
  "type",
2043
2098
  // Allowed for inheritance compatibility, ignored for built-in commands
2044
2099
  "docker",
@@ -2226,13 +2281,6 @@ function validateCompleteConfig(raw, section) {
2226
2281
  section
2227
2282
  );
2228
2283
  }
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
2284
  return result;
2237
2285
  }
2238
2286
  function validateAgentConfig(raw, section) {
@@ -2311,13 +2359,6 @@ function validateAgentConfig(raw, section) {
2311
2359
  section
2312
2360
  );
2313
2361
  }
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
- );
2320
- }
2321
2362
  return result;
2322
2363
  }
2323
2364
  function validateStringOrBoolean(value, field, section) {
@@ -2759,6 +2800,9 @@ function validateDockerConfig(raw, section) {
2759
2800
  if ("dev-source" in rawObj) {
2760
2801
  result["dev-source"] = validateString2(rawObj["dev-source"], "dev-source", section);
2761
2802
  }
2803
+ if ("docker-args" in rawObj) {
2804
+ result["docker-args"] = validateStringArray2(rawObj["docker-args"], "docker-args", section);
2805
+ }
2762
2806
  return result;
2763
2807
  }
2764
2808
 
@@ -2770,6 +2814,8 @@ FROM oven/bun:1-debian
2770
2814
 
2771
2815
  # Install essential tools
2772
2816
  RUN apt-get update && apt-get install -y --no-install-recommends \\
2817
+ # ed for EditFile gadget (line-oriented editor)
2818
+ ed \\
2773
2819
  # ripgrep for fast file searching
2774
2820
  ripgrep \\
2775
2821
  # git for version control operations
@@ -2802,6 +2848,7 @@ FROM oven/bun:1-debian
2802
2848
 
2803
2849
  # Install essential tools (same as production)
2804
2850
  RUN apt-get update && apt-get install -y --no-install-recommends \\
2851
+ ed \\
2805
2852
  ripgrep \\
2806
2853
  git \\
2807
2854
  curl \\
@@ -3036,6 +3083,9 @@ function buildDockerRunArgs(ctx, imageName, devMode) {
3036
3083
  }
3037
3084
  }
3038
3085
  }
3086
+ if (ctx.config["docker-args"]) {
3087
+ args.push(...ctx.config["docker-args"]);
3088
+ }
3039
3089
  args.push(imageName);
3040
3090
  args.push(...ctx.forwardArgs);
3041
3091
  return args;
@@ -3202,6 +3252,8 @@ async function executeAgent(promptArg, options, env) {
3202
3252
  env.stderr.write(chalk5.yellow(`
3203
3253
  [Cancelled] ${progress.formatStats()}
3204
3254
  `));
3255
+ } else {
3256
+ handleQuit();
3205
3257
  }
3206
3258
  };
3207
3259
  const keyboard = {
@@ -3209,7 +3261,7 @@ async function executeAgent(promptArg, options, env) {
3209
3261
  cleanupSigint: null,
3210
3262
  restore: () => {
3211
3263
  if (stdinIsInteractive && stdinStream.isTTY && !wasCancelled) {
3212
- keyboard.cleanupEsc = createEscKeyListener(stdinStream, handleCancel);
3264
+ keyboard.cleanupEsc = createEscKeyListener(stdinStream, handleCancel, handleCancel);
3213
3265
  }
3214
3266
  }
3215
3267
  };
@@ -3234,7 +3286,7 @@ async function executeAgent(promptArg, options, env) {
3234
3286
  process.exit(130);
3235
3287
  };
3236
3288
  if (stdinIsInteractive && stdinStream.isTTY) {
3237
- keyboard.cleanupEsc = createEscKeyListener(stdinStream, handleCancel);
3289
+ keyboard.cleanupEsc = createEscKeyListener(stdinStream, handleCancel, handleCancel);
3238
3290
  }
3239
3291
  keyboard.cleanupSigint = createSigintListener(
3240
3292
  handleCancel,
@@ -3260,11 +3312,11 @@ async function executeAgent(promptArg, options, env) {
3260
3312
  gadgetApprovals,
3261
3313
  defaultMode: "allowed"
3262
3314
  };
3263
- const approvalManager = new ApprovalManager(approvalConfig, env, progress);
3315
+ const approvalManager = new ApprovalManager(approvalConfig, env, progress, keyboard);
3264
3316
  let usage;
3265
3317
  let iterations = 0;
3266
- const llmRequestsDir = resolveLogDir(options.logLlmRequests, "requests");
3267
- const llmResponsesDir = resolveLogDir(options.logLlmResponses, "responses");
3318
+ const llmLogsBaseDir = resolveLogDir(options.logLlmRequests, "requests");
3319
+ let llmSessionDir;
3268
3320
  let llmCallCounter = 0;
3269
3321
  const countMessagesTokens = async (model, messages) => {
3270
3322
  try {
@@ -3296,10 +3348,19 @@ async function executeAgent(promptArg, options, env) {
3296
3348
  );
3297
3349
  progress.startCall(context.options.model, inputTokens);
3298
3350
  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);
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
+ }
3303
3364
  }
3304
3365
  },
3305
3366
  // onStreamChunk: Real-time updates as LLM generates tokens
@@ -3364,9 +3425,9 @@ async function executeAgent(promptArg, options, env) {
3364
3425
  `);
3365
3426
  }
3366
3427
  }
3367
- if (llmResponsesDir) {
3368
- const filename = `${Date.now()}_call_${llmCallCounter}.response.txt`;
3369
- await writeLogFile(llmResponsesDir, filename, context.rawResponse);
3428
+ if (llmSessionDir) {
3429
+ const filename = `${formatCallNumber(llmCallCounter)}.response`;
3430
+ await writeLogFile(llmSessionDir, filename, context.rawResponse);
3370
3431
  }
3371
3432
  }
3372
3433
  },
@@ -3463,6 +3524,13 @@ Denied: ${result.reason ?? "by user"}`
3463
3524
  parameterMapping: (text) => ({ message: text, done: false, type: "info" }),
3464
3525
  resultMapping: (text) => `\u2139\uFE0F ${text}`
3465
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
+ );
3466
3534
  const agent = builder.ask(prompt);
3467
3535
  let textBuffer = "";
3468
3536
  const flushTextBuffer = () => {
@@ -3488,7 +3556,7 @@ Denied: ${result.reason ?? "by user"}`
3488
3556
  }
3489
3557
  } else {
3490
3558
  const tokenCount = await countGadgetOutputTokens(event.result.result);
3491
- env.stderr.write(`${formatGadgetSummary({ ...event.result, tokenCount })}
3559
+ env.stderr.write(`${formatGadgetSummary2({ ...event.result, tokenCount })}
3492
3560
  `);
3493
3561
  }
3494
3562
  }
@@ -3500,7 +3568,10 @@ Denied: ${result.reason ?? "by user"}`
3500
3568
  } finally {
3501
3569
  isStreaming = false;
3502
3570
  keyboard.cleanupEsc?.();
3503
- keyboard.cleanupSigint?.();
3571
+ if (keyboard.cleanupSigint) {
3572
+ keyboard.cleanupSigint();
3573
+ process.once("SIGINT", () => process.exit(130));
3574
+ }
3504
3575
  }
3505
3576
  flushTextBuffer();
3506
3577
  progress.complete();
@@ -3548,13 +3619,15 @@ async function executeComplete(promptArg, options, env) {
3548
3619
  }
3549
3620
  builder.addUser(prompt);
3550
3621
  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);
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
+ }
3558
3631
  }
3559
3632
  const stream = client.stream({
3560
3633
  model,
@@ -3593,9 +3666,9 @@ async function executeComplete(promptArg, options, env) {
3593
3666
  progress.endCall(usage);
3594
3667
  progress.complete();
3595
3668
  printer.ensureNewline();
3596
- if (llmResponsesDir) {
3597
- const filename = `${timestamp}_complete.response.txt`;
3598
- await writeLogFile(llmResponsesDir, filename, accumulatedResponse);
3669
+ if (llmSessionDir) {
3670
+ const filename = "0001.response";
3671
+ await writeLogFile(llmSessionDir, filename, accumulatedResponse);
3599
3672
  }
3600
3673
  if (stderrTTY && !options.quiet) {
3601
3674
  const summary = renderSummary({ finishReason, usage, cost: progress.getTotalCost() });
@@ -3830,9 +3903,11 @@ ${issues}`);
3830
3903
  env.stderr.write(chalk7.dim("\nExecuting...\n"));
3831
3904
  const startTime = Date.now();
3832
3905
  let result;
3906
+ let cost;
3833
3907
  try {
3908
+ let rawResult;
3834
3909
  if (gadget.timeoutMs && gadget.timeoutMs > 0) {
3835
- result = await Promise.race([
3910
+ rawResult = await Promise.race([
3836
3911
  Promise.resolve(gadget.execute(params)),
3837
3912
  new Promise(
3838
3913
  (_, reject) => setTimeout(
@@ -3842,15 +3917,18 @@ ${issues}`);
3842
3917
  )
3843
3918
  ]);
3844
3919
  } else {
3845
- result = await Promise.resolve(gadget.execute(params));
3920
+ rawResult = await Promise.resolve(gadget.execute(params));
3846
3921
  }
3922
+ result = typeof rawResult === "string" ? rawResult : rawResult.result;
3923
+ cost = typeof rawResult === "object" ? rawResult.cost : void 0;
3847
3924
  } catch (error) {
3848
3925
  const message = error instanceof Error ? error.message : String(error);
3849
3926
  throw new Error(`Execution failed: ${message}`);
3850
3927
  }
3851
3928
  const elapsed = Date.now() - startTime;
3929
+ const costInfo = cost !== void 0 && cost > 0 ? ` (Cost: $${cost.toFixed(6)})` : "";
3852
3930
  env.stderr.write(chalk7.green(`
3853
- \u2713 Completed in ${elapsed}ms
3931
+ \u2713 Completed in ${elapsed}ms${costInfo}
3854
3932
 
3855
3933
  `));
3856
3934
  formatOutput(result, options, env.stdout);