llmist 4.0.0 → 5.0.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-Q6NQRMYD.js";
2
+ import "./chunk-UBPZUVIN.js";
3
3
  import {
4
4
  AbstractGadget,
5
5
  AgentBuilder,
@@ -34,7 +34,7 @@ import {
34
34
  schemaToJSONSchema,
35
35
  text,
36
36
  validateGadgetSchema
37
- } from "./chunk-RHR2M6T6.js";
37
+ } from "./chunk-3SZIQI45.js";
38
38
 
39
39
  // src/cli/constants.ts
40
40
  var CLI_NAME = "llmist";
@@ -68,7 +68,6 @@ var OPTION_FLAGS = {
68
68
  docker: "--docker",
69
69
  dockerRo: "--docker-ro",
70
70
  noDocker: "--no-docker",
71
- dockerDev: "--docker-dev",
72
71
  // Multimodal input options
73
72
  inputImage: "--image <path>",
74
73
  inputAudio: "--audio <path>",
@@ -103,7 +102,6 @@ var OPTION_DESCRIPTIONS = {
103
102
  docker: "Run agent in a Docker sandbox container for security isolation.",
104
103
  dockerRo: "Run in Docker with current directory mounted read-only.",
105
104
  noDocker: "Disable Docker sandboxing (override config).",
106
- dockerDev: "Run in Docker dev mode (mount local source instead of npm install).",
107
105
  // Image generation descriptions
108
106
  imageSize: "Image size/aspect ratio, e.g. '1024x1024', '1:1', '16:9'.",
109
107
  imageQuality: "Image quality: 'standard', 'hd', 'low', 'medium', 'high'.",
@@ -123,7 +121,7 @@ import { Command, InvalidArgumentError as InvalidArgumentError2 } from "commande
123
121
  // package.json
124
122
  var package_default = {
125
123
  name: "llmist",
126
- version: "3.1.0",
124
+ version: "5.0.0",
127
125
  description: "TypeScript LLM client with streaming tool execution. Tools fire mid-stream. Built-in function calling works with any model\u2014no structured outputs or native tool support required.",
128
126
  type: "module",
129
127
  main: "dist/index.cjs",
@@ -166,7 +164,8 @@ var package_default = {
166
164
  "test:all": "bun run test && bun run test:e2e",
167
165
  clean: "rimraf dist",
168
166
  prepare: "node scripts/install-hooks.js || true",
169
- "release:dry": "bunx semantic-release --dry-run"
167
+ "release:dry": "bunx semantic-release --dry-run",
168
+ "release:publish": 'test "$(git branch --show-current)" = "main" && git pull origin main && bun run build && npm publish'
170
169
  },
171
170
  bin: {
172
171
  llmist: "dist/cli.js"
@@ -1525,20 +1524,17 @@ var DOCKER_CONFIG_KEYS = /* @__PURE__ */ new Set([
1525
1524
  "mounts",
1526
1525
  "env-vars",
1527
1526
  "image-name",
1528
- "dev-mode",
1529
- "dev-source",
1530
1527
  "docker-args"
1531
1528
  ]);
1532
1529
  var DEFAULT_IMAGE_NAME = "llmist-sandbox";
1533
1530
  var DEFAULT_CWD_PERMISSION = "rw";
1534
1531
  var DEFAULT_CONFIG_PERMISSION = "ro";
1532
+ var GADGET_CACHE_VOLUME = "llmist-gadget-cache";
1535
1533
  var FORWARDED_API_KEYS = [
1536
1534
  "ANTHROPIC_API_KEY",
1537
1535
  "OPENAI_API_KEY",
1538
1536
  "GEMINI_API_KEY"
1539
1537
  ];
1540
- var DEV_IMAGE_NAME = "llmist-dev-sandbox";
1541
- var DEV_SOURCE_MOUNT_TARGET = "/llmist-src";
1542
1538
 
1543
1539
  // src/cli/docker/docker-config.ts
1544
1540
  var MOUNT_CONFIG_KEYS = /* @__PURE__ */ new Set(["source", "target", "permission"]);
@@ -1650,12 +1646,6 @@ function validateDockerConfig(raw, section) {
1650
1646
  if ("image-name" in rawObj) {
1651
1647
  result["image-name"] = validateString2(rawObj["image-name"], "image-name", section);
1652
1648
  }
1653
- if ("dev-mode" in rawObj) {
1654
- result["dev-mode"] = validateBoolean2(rawObj["dev-mode"], "dev-mode", section);
1655
- }
1656
- if ("dev-source" in rawObj) {
1657
- result["dev-source"] = validateString2(rawObj["dev-source"], "dev-source", section);
1658
- }
1659
1649
  if ("docker-args" in rawObj) {
1660
1650
  result["docker-args"] = validateStringArray2(rawObj["docker-args"], "docker-args", section);
1661
1651
  }
@@ -1665,7 +1655,6 @@ function validateDockerConfig(raw, section) {
1665
1655
  // src/cli/docker/docker-wrapper.ts
1666
1656
  import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
1667
1657
  import { homedir as homedir3 } from "node:os";
1668
- import { dirname, join as join3 } from "node:path";
1669
1658
 
1670
1659
  // src/cli/docker/dockerfile.ts
1671
1660
  var DEFAULT_DOCKERFILE = `# llmist sandbox image
@@ -1685,6 +1674,10 @@ RUN apt-get update && apt-get install -y --no-install-recommends \\
1685
1674
  curl \\
1686
1675
  # ca-certificates for HTTPS
1687
1676
  ca-certificates \\
1677
+ # python3 for native module compilation (node-gyp)
1678
+ python3 \\
1679
+ # build-essential for compiling native modules
1680
+ build-essential \\
1688
1681
  && rm -rf /var/lib/apt/lists/*
1689
1682
 
1690
1683
  # Install ast-grep for code search/refactoring
@@ -1702,37 +1695,8 @@ WORKDIR /workspace
1702
1695
  # Entry point - llmist with all arguments forwarded
1703
1696
  ENTRYPOINT ["llmist"]
1704
1697
  `;
1705
- var DEV_DOCKERFILE = `# llmist DEV sandbox image
1706
- # For development/testing with local source code
1707
-
1708
- FROM oven/bun:1-debian
1709
-
1710
- # Install essential tools (same as production)
1711
- RUN apt-get update && apt-get install -y --no-install-recommends \\
1712
- ed \\
1713
- ripgrep \\
1714
- git \\
1715
- curl \\
1716
- ca-certificates \\
1717
- && rm -rf /var/lib/apt/lists/*
1718
-
1719
- # Install ast-grep for code search/refactoring
1720
- RUN curl -fsSL https://raw.githubusercontent.com/ast-grep/ast-grep/main/install.sh | bash \\
1721
- && mv /root/.local/bin/ast-grep /usr/local/bin/ 2>/dev/null || true \\
1722
- && mv /root/.local/bin/sg /usr/local/bin/ 2>/dev/null || true
1723
-
1724
- # Working directory (host CWD will be mounted here)
1725
- WORKDIR /workspace
1726
-
1727
- # Entry point - run llmist from mounted source
1728
- # Source is mounted at ${DEV_SOURCE_MOUNT_TARGET}
1729
- ENTRYPOINT ["bun", "run", "${DEV_SOURCE_MOUNT_TARGET}/src/cli.ts"]
1730
- `;
1731
- function resolveDockerfile(config, devMode = false) {
1732
- if (config.dockerfile) {
1733
- return config.dockerfile;
1734
- }
1735
- return devMode ? DEV_DOCKERFILE : DEFAULT_DOCKERFILE;
1698
+ function resolveDockerfile(config) {
1699
+ return config.dockerfile ?? DEFAULT_DOCKERFILE;
1736
1700
  }
1737
1701
  function computeDockerfileHash(dockerfile) {
1738
1702
  const encoder = new TextEncoder();
@@ -1794,10 +1758,13 @@ async function buildImage(imageName, dockerfile) {
1794
1758
  ensureCacheDir();
1795
1759
  const dockerfilePath = join2(CACHE_DIR, "Dockerfile");
1796
1760
  writeFileSync(dockerfilePath, dockerfile);
1797
- const proc = Bun.spawn(["docker", "build", "-t", imageName, "-f", dockerfilePath, CACHE_DIR], {
1798
- stdout: "pipe",
1799
- stderr: "pipe"
1800
- });
1761
+ const proc = Bun.spawn(
1762
+ ["docker", "build", "--no-cache", "-t", imageName, "-f", dockerfilePath, CACHE_DIR],
1763
+ {
1764
+ stdout: "pipe",
1765
+ stderr: "pipe"
1766
+ }
1767
+ );
1801
1768
  const exitCode = await proc.exited;
1802
1769
  const stdout = await new Response(proc.stdout).text();
1803
1770
  const stderr = await new Response(proc.stderr).text();
@@ -1859,46 +1826,13 @@ function isInsideContainer() {
1859
1826
  }
1860
1827
  return false;
1861
1828
  }
1862
- function autoDetectDevSource() {
1863
- const scriptPath = process.argv[1];
1864
- if (!scriptPath || !scriptPath.endsWith("src/cli.ts")) {
1865
- return void 0;
1866
- }
1867
- const srcDir = dirname(scriptPath);
1868
- const projectDir = dirname(srcDir);
1869
- const packageJsonPath = join3(projectDir, "package.json");
1870
- if (!existsSync4(packageJsonPath)) {
1871
- return void 0;
1872
- }
1873
- try {
1874
- const pkg = JSON.parse(readFileSync4(packageJsonPath, "utf-8"));
1875
- if (pkg.name === "llmist") {
1876
- return projectDir;
1877
- }
1878
- } catch {
1879
- }
1880
- return void 0;
1881
- }
1882
- function resolveDevMode(config, cliDevMode) {
1883
- const enabled = cliDevMode || config?.["dev-mode"] || process.env.LLMIST_DEV_MODE === "1";
1884
- if (!enabled) {
1885
- return { enabled: false, sourcePath: void 0 };
1886
- }
1887
- const sourcePath = config?.["dev-source"] || process.env.LLMIST_DEV_SOURCE || autoDetectDevSource();
1888
- if (!sourcePath) {
1889
- throw new Error(
1890
- "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)."
1891
- );
1892
- }
1893
- return { enabled: true, sourcePath };
1894
- }
1895
1829
  function expandHome(path6) {
1896
1830
  if (path6.startsWith("~")) {
1897
1831
  return path6.replace(/^~/, homedir3());
1898
1832
  }
1899
1833
  return path6;
1900
1834
  }
1901
- function buildDockerRunArgs(ctx, imageName, devMode) {
1835
+ function buildDockerRunArgs(ctx, imageName) {
1902
1836
  const args = ["run", "--rm"];
1903
1837
  const timestamp = Date.now();
1904
1838
  const random = Math.random().toString(36).slice(2, 8);
@@ -1912,11 +1846,15 @@ function buildDockerRunArgs(ctx, imageName, devMode) {
1912
1846
  args.push("-w", "/workspace");
1913
1847
  const configPermission = ctx.config["config-permission"] ?? DEFAULT_CONFIG_PERMISSION;
1914
1848
  const llmistDir = expandHome("~/.llmist");
1915
- args.push("-v", `${llmistDir}:/root/.llmist:${configPermission}`);
1916
- if (devMode.enabled && devMode.sourcePath) {
1917
- const expandedSource = expandHome(devMode.sourcePath);
1918
- args.push("-v", `${expandedSource}:${DEV_SOURCE_MOUNT_TARGET}:ro`);
1849
+ const cliTomlPath = `${llmistDir}/cli.toml`;
1850
+ if (existsSync4(cliTomlPath)) {
1851
+ args.push("-v", `${cliTomlPath}:/root/.llmist/cli.toml:${configPermission}`);
1919
1852
  }
1853
+ const gadgetsDir = `${llmistDir}/gadgets`;
1854
+ if (existsSync4(gadgetsDir)) {
1855
+ args.push("-v", `${gadgetsDir}:/root/.llmist/gadgets:${configPermission}`);
1856
+ }
1857
+ args.push("-v", `${GADGET_CACHE_VOLUME}:/root/.llmist/gadget-cache`);
1920
1858
  if (ctx.config.mounts) {
1921
1859
  for (const mount of ctx.config.mounts) {
1922
1860
  const source = expandHome(mount.source);
@@ -1943,7 +1881,7 @@ function buildDockerRunArgs(ctx, imageName, devMode) {
1943
1881
  return args;
1944
1882
  }
1945
1883
  function filterDockerArgs(argv) {
1946
- const dockerFlags = /* @__PURE__ */ new Set(["--docker", "--docker-ro", "--no-docker", "--docker-dev"]);
1884
+ const dockerFlags = /* @__PURE__ */ new Set(["--docker", "--docker-ro", "--no-docker"]);
1947
1885
  return argv.filter((arg) => !dockerFlags.has(arg));
1948
1886
  }
1949
1887
  function resolveDockerEnabled(config, options, profileDocker) {
@@ -1958,22 +1896,16 @@ function resolveDockerEnabled(config, options, profileDocker) {
1958
1896
  }
1959
1897
  return config?.enabled ?? false;
1960
1898
  }
1961
- async function executeInDocker(ctx, devMode) {
1899
+ async function executeInDocker(ctx) {
1962
1900
  if (isInsideContainer()) {
1963
- console.error(
1964
- "Warning: Docker mode requested but already inside a container. Proceeding without re-containerization."
1965
- );
1966
1901
  throw new DockerSkipError();
1967
1902
  }
1968
1903
  const available = await checkDockerAvailable();
1969
1904
  if (!available) {
1970
1905
  throw new DockerUnavailableError();
1971
1906
  }
1972
- const dockerfile = resolveDockerfile(ctx.config, devMode.enabled);
1973
- const imageName = devMode.enabled ? DEV_IMAGE_NAME : ctx.config["image-name"] ?? DEFAULT_IMAGE_NAME;
1974
- if (devMode.enabled) {
1975
- console.error(`[dev mode] Mounting source from ${devMode.sourcePath}`);
1976
- }
1907
+ const dockerfile = resolveDockerfile(ctx.config);
1908
+ const imageName = ctx.config["image-name"] ?? DEFAULT_IMAGE_NAME;
1977
1909
  try {
1978
1910
  await ensureImage(imageName, dockerfile);
1979
1911
  } catch (error) {
@@ -1984,7 +1916,7 @@ async function executeInDocker(ctx, devMode) {
1984
1916
  }
1985
1917
  throw error;
1986
1918
  }
1987
- const dockerArgs = buildDockerRunArgs(ctx, imageName, devMode);
1919
+ const dockerArgs = buildDockerRunArgs(ctx, imageName);
1988
1920
  const proc = Bun.spawn(["docker", ...dockerArgs], {
1989
1921
  stdin: "inherit",
1990
1922
  stdout: "inherit",
@@ -2592,7 +2524,7 @@ async function installNpmPackage(spec, cacheDir) {
2592
2524
  fs5.writeFileSync(path4.join(cacheDir, "package.json"), JSON.stringify(packageJson, null, 2));
2593
2525
  const packageSpec = spec.version ? `${spec.package}@${spec.version}` : spec.package;
2594
2526
  try {
2595
- execSync(`npm install --prefix "${cacheDir}" "${packageSpec}" --save`, {
2527
+ execSync(`bun add "${packageSpec}"`, {
2596
2528
  stdio: "pipe",
2597
2529
  cwd: cacheDir
2598
2530
  });
@@ -2623,7 +2555,7 @@ async function installGitPackage(spec, cacheDir) {
2623
2555
  }
2624
2556
  if (fs5.existsSync(path4.join(cacheDir, "package.json"))) {
2625
2557
  try {
2626
- execSync("npm install", { cwd: cacheDir, stdio: "pipe" });
2558
+ execSync("bun install", { cwd: cacheDir, stdio: "inherit" });
2627
2559
  } catch (error) {
2628
2560
  const message = error instanceof Error ? error.message : String(error);
2629
2561
  throw new Error(`Failed to install dependencies for '${spec.package}': ${message}`);
@@ -2631,7 +2563,7 @@ async function installGitPackage(spec, cacheDir) {
2631
2563
  try {
2632
2564
  const packageJson = JSON.parse(fs5.readFileSync(path4.join(cacheDir, "package.json"), "utf-8"));
2633
2565
  if (packageJson.scripts?.build) {
2634
- execSync("npm run build", { cwd: cacheDir, stdio: "pipe" });
2566
+ execSync("bun run build", { cwd: cacheDir, stdio: "inherit" });
2635
2567
  }
2636
2568
  } catch (error) {
2637
2569
  const message = error instanceof Error ? error.message : String(error);
@@ -2878,11 +2810,11 @@ async function loadGadgets(specifiers, cwd, importer = (specifier) => import(spe
2878
2810
  init_messages();
2879
2811
  import { mkdir, writeFile as writeFile2 } from "node:fs/promises";
2880
2812
  import { homedir as homedir4 } from "node:os";
2881
- import { join as join4 } from "node:path";
2882
- var DEFAULT_LLM_LOG_DIR = join4(homedir4(), ".llmist", "logs");
2813
+ import { join as join3 } from "node:path";
2814
+ var DEFAULT_LLM_LOG_DIR = join3(homedir4(), ".llmist", "logs");
2883
2815
  function resolveLogDir(option, subdir) {
2884
2816
  if (option === true) {
2885
- return join4(DEFAULT_LLM_LOG_DIR, subdir);
2817
+ return join3(DEFAULT_LLM_LOG_DIR, subdir);
2886
2818
  }
2887
2819
  if (typeof option === "string") {
2888
2820
  return option;
@@ -2900,7 +2832,7 @@ function formatLlmRequest(messages) {
2900
2832
  }
2901
2833
  async function writeLogFile(dir, filename, content) {
2902
2834
  await mkdir(dir, { recursive: true });
2903
- await writeFile2(join4(dir, filename), content, "utf-8");
2835
+ await writeFile2(join3(dir, filename), content, "utf-8");
2904
2836
  }
2905
2837
  function formatSessionTimestamp(date = /* @__PURE__ */ new Date()) {
2906
2838
  const pad = (n) => n.toString().padStart(2, "0");
@@ -2914,7 +2846,7 @@ function formatSessionTimestamp(date = /* @__PURE__ */ new Date()) {
2914
2846
  }
2915
2847
  async function createSessionDir(baseDir) {
2916
2848
  const timestamp = formatSessionTimestamp();
2917
- const sessionDir = join4(baseDir, timestamp);
2849
+ const sessionDir = join3(baseDir, timestamp);
2918
2850
  try {
2919
2851
  await mkdir(sessionDir, { recursive: true });
2920
2852
  return sessionDir;
@@ -3007,6 +2939,45 @@ function formatCost(cost) {
3007
2939
  }
3008
2940
  return cost.toFixed(2);
3009
2941
  }
2942
+ function formatLLMCallLine(info) {
2943
+ const parts = [];
2944
+ parts.push(`${chalk3.cyan(`#${info.iteration}`)} ${chalk3.magenta(info.model)}`);
2945
+ if (info.contextPercent !== void 0 && info.contextPercent !== null) {
2946
+ const formatted = `${Math.round(info.contextPercent)}%`;
2947
+ if (info.contextPercent >= 80) {
2948
+ parts.push(chalk3.red(formatted));
2949
+ } else if (info.contextPercent >= 50) {
2950
+ parts.push(chalk3.yellow(formatted));
2951
+ } else {
2952
+ parts.push(chalk3.green(formatted));
2953
+ }
2954
+ }
2955
+ if (info.inputTokens && info.inputTokens > 0) {
2956
+ const prefix = info.estimated?.input ? "~" : "";
2957
+ parts.push(chalk3.dim("\u2191") + chalk3.yellow(` ${prefix}${formatTokens(info.inputTokens)}`));
2958
+ }
2959
+ if (info.cachedInputTokens && info.cachedInputTokens > 0) {
2960
+ parts.push(chalk3.dim("\u27F3") + chalk3.blue(` ${formatTokens(info.cachedInputTokens)}`));
2961
+ }
2962
+ if (info.outputTokens !== void 0 && info.outputTokens > 0 || info.isStreaming) {
2963
+ const prefix = info.estimated?.output ? "~" : "";
2964
+ parts.push(chalk3.dim("\u2193") + chalk3.green(` ${prefix}${formatTokens(info.outputTokens ?? 0)}`));
2965
+ }
2966
+ parts.push(chalk3.dim(`${info.elapsedSeconds.toFixed(1)}s`));
2967
+ if (info.cost !== void 0 && info.cost > 0) {
2968
+ parts.push(chalk3.cyan(`$${formatCost(info.cost)}`));
2969
+ }
2970
+ if (info.isStreaming && info.spinner) {
2971
+ parts.push(chalk3.cyan(info.spinner));
2972
+ } else if (info.finishReason !== void 0) {
2973
+ if (!info.finishReason || info.finishReason === "stop" || info.finishReason === "end_turn") {
2974
+ parts.push(chalk3.green("\u2713"));
2975
+ } else {
2976
+ parts.push(chalk3.yellow(info.finishReason));
2977
+ }
2978
+ }
2979
+ return parts.join(chalk3.dim(" | "));
2980
+ }
3010
2981
  function renderSummary(metadata) {
3011
2982
  const parts = [];
3012
2983
  if (metadata.iterations !== void 0) {
@@ -3113,6 +3084,34 @@ function formatParametersInline(params, maxWidth) {
3113
3084
  return `${chalk3.dim(key)}${chalk3.dim("=")}${chalk3.cyan(formatted)}`;
3114
3085
  }).join(chalk3.dim(", "));
3115
3086
  }
3087
+ function formatGadgetLine(info, maxWidth) {
3088
+ const terminalWidth = maxWidth ?? process.stdout.columns ?? 80;
3089
+ const gadgetLabel = chalk3.magenta.bold(info.name);
3090
+ const timeStr = `${info.elapsedSeconds.toFixed(1)}s`;
3091
+ const timeLabel = chalk3.dim(timeStr);
3092
+ const fixedLength = 2 + info.name.length + 2 + 1 + timeStr.length;
3093
+ const availableForParams = Math.max(40, terminalWidth - fixedLength - 2);
3094
+ const paramsStr = formatParametersInline(info.parameters, availableForParams);
3095
+ const paramsLabel = paramsStr ? `${chalk3.dim("(")}${paramsStr}${chalk3.dim(")")}` : "";
3096
+ if (info.error) {
3097
+ const errorMsg = info.error.length > 50 ? `${info.error.slice(0, 50)}\u2026` : info.error;
3098
+ return `${chalk3.red("\u2717")} ${gadgetLabel}${paramsLabel} ${chalk3.red("error:")} ${errorMsg} ${timeLabel}`;
3099
+ }
3100
+ if (!info.isComplete) {
3101
+ return `${chalk3.blue("\u23F5")} ${gadgetLabel}${paramsLabel} ${timeLabel}`;
3102
+ }
3103
+ let outputStr;
3104
+ if (info.tokenCount !== void 0 && info.tokenCount > 0) {
3105
+ outputStr = `${formatTokens(info.tokenCount)} tokens`;
3106
+ } else if (info.outputBytes !== void 0 && info.outputBytes > 0) {
3107
+ outputStr = formatBytes(info.outputBytes);
3108
+ } else {
3109
+ outputStr = "";
3110
+ }
3111
+ const icon = info.breaksLoop ? chalk3.yellow("\u23F9") : chalk3.green("\u2713");
3112
+ const outputLabel = outputStr ? ` ${chalk3.dim("\u2192")} ${chalk3.green(outputStr)}` : "";
3113
+ return `${icon} ${gadgetLabel}${paramsLabel}${outputLabel} ${timeLabel}`;
3114
+ }
3116
3115
  function formatBytes(bytes) {
3117
3116
  if (bytes < 1024) {
3118
3117
  return `${bytes} bytes`;
@@ -3391,11 +3390,35 @@ var StreamProgress = class {
3391
3390
  }
3392
3391
  /**
3393
3392
  * Update a nested agent with completion info (called when nested llm_call_end event received).
3393
+ * Records completion time to freeze the elapsed timer.
3394
+ * @param info - Full LLM call info including tokens, cache details, and cost
3394
3395
  */
3395
- updateNestedAgent(id, outputTokens) {
3396
+ updateNestedAgent(id, info) {
3396
3397
  const agent = this.nestedAgents.get(id);
3397
3398
  if (agent) {
3398
- agent.outputTokens = outputTokens;
3399
+ agent.inputTokens = info.inputTokens;
3400
+ agent.outputTokens = info.outputTokens;
3401
+ agent.cachedInputTokens = info.cachedInputTokens;
3402
+ agent.cacheCreationInputTokens = info.cacheCreationInputTokens;
3403
+ agent.finishReason = info.finishReason;
3404
+ if (info.cost !== void 0) {
3405
+ agent.cost = info.cost;
3406
+ } else if (this.modelRegistry && agent.model && info.outputTokens) {
3407
+ try {
3408
+ const modelName = agent.model.includes(":") ? agent.model.split(":")[1] : agent.model;
3409
+ const costResult = this.modelRegistry.estimateCost(
3410
+ modelName,
3411
+ info.inputTokens ?? 0,
3412
+ info.outputTokens,
3413
+ info.cachedInputTokens,
3414
+ info.cacheCreationInputTokens
3415
+ );
3416
+ agent.cost = costResult?.totalCost;
3417
+ } catch {
3418
+ }
3419
+ }
3420
+ agent.completed = true;
3421
+ agent.completedTime = Date.now();
3399
3422
  if (this.isRunning && this.isTTY) {
3400
3423
  this.render();
3401
3424
  }
@@ -3413,11 +3436,12 @@ var StreamProgress = class {
3413
3436
  /**
3414
3437
  * Add a nested gadget call (called when nested gadget_call event received).
3415
3438
  */
3416
- addNestedGadget(id, depth, parentInvocationId, name) {
3439
+ addNestedGadget(id, depth, parentInvocationId, name, parameters) {
3417
3440
  this.nestedGadgets.set(id, {
3418
3441
  depth,
3419
3442
  parentInvocationId,
3420
3443
  name,
3444
+ parameters,
3421
3445
  startTime: Date.now()
3422
3446
  });
3423
3447
  if (this.isRunning && this.isTTY) {
@@ -3435,11 +3459,13 @@ var StreamProgress = class {
3435
3459
  }
3436
3460
  /**
3437
3461
  * Mark a nested gadget as completed (keeps it visible with ✓ indicator).
3462
+ * Records completion time to freeze the elapsed timer.
3438
3463
  */
3439
3464
  completeNestedGadget(id) {
3440
3465
  const gadget = this.nestedGadgets.get(id);
3441
3466
  if (gadget) {
3442
3467
  gadget.completed = true;
3468
+ gadget.completedTime = Date.now();
3443
3469
  if (this.isRunning && this.isTTY) {
3444
3470
  this.render();
3445
3471
  }
@@ -3581,25 +3607,73 @@ var StreamProgress = class {
3581
3607
  }
3582
3608
  if (this.isTTY) {
3583
3609
  for (const [gadgetId, gadget] of this.inFlightGadgets) {
3584
- const elapsed = ((Date.now() - gadget.startTime) / 1e3).toFixed(1);
3585
- const gadgetLine = ` ${chalk4.blue("\u23F5")} ${chalk4.magenta.bold(gadget.name)}${chalk4.dim("(...)")} ${chalk4.dim(elapsed + "s")}`;
3610
+ const elapsedSeconds = (Date.now() - gadget.startTime) / 1e3;
3611
+ const gadgetLine = ` ${formatGadgetLine({
3612
+ name: gadget.name,
3613
+ parameters: gadget.params,
3614
+ elapsedSeconds,
3615
+ isComplete: false
3616
+ })}`;
3586
3617
  lines.push(gadgetLine);
3618
+ const nestedOps = [];
3587
3619
  for (const [_agentId, nested] of this.nestedAgents) {
3588
- if (nested.parentInvocationId !== gadgetId) continue;
3589
- const indent = " ".repeat(nested.depth + 1);
3590
- const nestedElapsed = ((Date.now() - nested.startTime) / 1e3).toFixed(1);
3591
- const tokens = nested.inputTokens ? ` ${chalk4.dim("\u2191")}${chalk4.yellow(formatTokens(nested.inputTokens))}` : "";
3592
- const outTokens = nested.outputTokens ? ` ${chalk4.dim("\u2193")}${chalk4.green(formatTokens(nested.outputTokens))}` : "";
3593
- const nestedLine = `${indent}${chalk4.cyan(`#${nested.iteration}`)} ${chalk4.dim(nested.model)}${tokens}${outTokens} ${chalk4.dim(nestedElapsed + "s")} ${chalk4.cyan(spinner)}`;
3594
- lines.push(nestedLine);
3620
+ if (nested.parentInvocationId === gadgetId) {
3621
+ nestedOps.push({
3622
+ type: "agent",
3623
+ startTime: nested.startTime,
3624
+ depth: nested.depth,
3625
+ iteration: nested.iteration,
3626
+ model: nested.model,
3627
+ inputTokens: nested.inputTokens,
3628
+ cachedInputTokens: nested.cachedInputTokens,
3629
+ outputTokens: nested.outputTokens,
3630
+ cost: nested.cost,
3631
+ finishReason: nested.finishReason,
3632
+ completed: nested.completed,
3633
+ completedTime: nested.completedTime
3634
+ });
3635
+ }
3595
3636
  }
3596
- for (const [nestedId, nestedGadget] of this.nestedGadgets) {
3637
+ for (const [_nestedId, nestedGadget] of this.nestedGadgets) {
3597
3638
  if (nestedGadget.parentInvocationId === gadgetId) {
3598
- const indent = " ".repeat(nestedGadget.depth + 1);
3599
- const nestedElapsed = ((Date.now() - nestedGadget.startTime) / 1e3).toFixed(1);
3600
- const icon = nestedGadget.completed ? chalk4.green("\u2713") : chalk4.blue("\u23F5");
3601
- const nestedGadgetLine = `${indent}${icon} ${chalk4.dim(nestedGadget.name + "(...)")} ${chalk4.dim(nestedElapsed + "s")}`;
3602
- lines.push(nestedGadgetLine);
3639
+ nestedOps.push({
3640
+ type: "gadget",
3641
+ startTime: nestedGadget.startTime,
3642
+ depth: nestedGadget.depth,
3643
+ name: nestedGadget.name,
3644
+ parameters: nestedGadget.parameters,
3645
+ completed: nestedGadget.completed,
3646
+ completedTime: nestedGadget.completedTime
3647
+ });
3648
+ }
3649
+ }
3650
+ nestedOps.sort((a, b) => a.startTime - b.startTime);
3651
+ for (const op of nestedOps) {
3652
+ const indent = " ".repeat(op.depth + 1);
3653
+ const endTime = op.completedTime ?? Date.now();
3654
+ const elapsedSeconds2 = (endTime - op.startTime) / 1e3;
3655
+ if (op.type === "agent") {
3656
+ const line = formatLLMCallLine({
3657
+ iteration: op.iteration ?? 0,
3658
+ model: op.model ?? "",
3659
+ inputTokens: op.inputTokens,
3660
+ cachedInputTokens: op.cachedInputTokens,
3661
+ outputTokens: op.outputTokens,
3662
+ elapsedSeconds: elapsedSeconds2,
3663
+ cost: op.cost,
3664
+ finishReason: op.completed ? op.finishReason ?? "stop" : void 0,
3665
+ isStreaming: !op.completed,
3666
+ spinner
3667
+ });
3668
+ lines.push(`${indent}${line}`);
3669
+ } else {
3670
+ const line = formatGadgetLine({
3671
+ name: op.name ?? "",
3672
+ parameters: op.parameters,
3673
+ elapsedSeconds: elapsedSeconds2,
3674
+ isComplete: op.completed ?? false
3675
+ });
3676
+ lines.push(`${indent}${line}`);
3603
3677
  }
3604
3678
  }
3605
3679
  }
@@ -3621,42 +3695,27 @@ var StreamProgress = class {
3621
3695
  }
3622
3696
  /**
3623
3697
  * Format the streaming mode progress line (returns string, doesn't write).
3698
+ * Uses the shared formatLLMCallLine() function for consistent formatting
3699
+ * between main agent and nested subagent displays.
3624
3700
  */
3625
3701
  formatStreamingLine(spinner) {
3626
- const elapsed = ((Date.now() - this.callStartTime) / 1e3).toFixed(1);
3627
3702
  const outTokens = this.callOutputTokensEstimated ? Math.round(this.callOutputChars / FALLBACK_CHARS_PER_TOKEN) : this.callOutputTokens;
3628
- const parts = [];
3629
- const iterPart = chalk4.cyan(`#${this.currentIteration}`);
3630
- if (this.model) {
3631
- parts.push(`${iterPart} ${chalk4.magenta(this.model)}`);
3632
- } else {
3633
- parts.push(iterPart);
3634
- }
3635
- const usagePercent = this.getContextUsagePercent();
3636
- if (usagePercent !== null) {
3637
- const formatted = `${Math.round(usagePercent)}%`;
3638
- if (usagePercent >= 80) {
3639
- parts.push(chalk4.red(formatted));
3640
- } else if (usagePercent >= 50) {
3641
- parts.push(chalk4.yellow(formatted));
3642
- } else {
3643
- parts.push(chalk4.green(formatted));
3703
+ return formatLLMCallLine({
3704
+ iteration: this.currentIteration,
3705
+ model: this.model ?? "",
3706
+ inputTokens: this.callInputTokens,
3707
+ cachedInputTokens: this.callCachedInputTokens,
3708
+ outputTokens: outTokens,
3709
+ elapsedSeconds: (Date.now() - this.callStartTime) / 1e3,
3710
+ cost: this.calculateCurrentCallCost(outTokens),
3711
+ isStreaming: true,
3712
+ spinner,
3713
+ contextPercent: this.getContextUsagePercent(),
3714
+ estimated: {
3715
+ input: this.callInputTokensEstimated,
3716
+ output: this.callOutputTokensEstimated
3644
3717
  }
3645
- }
3646
- if (this.callInputTokens > 0) {
3647
- const prefix = this.callInputTokensEstimated ? "~" : "";
3648
- parts.push(chalk4.dim("\u2191") + chalk4.yellow(` ${prefix}${formatTokens(this.callInputTokens)}`));
3649
- }
3650
- if (this.isStreaming || outTokens > 0) {
3651
- const prefix = this.callOutputTokensEstimated ? "~" : "";
3652
- parts.push(chalk4.dim("\u2193") + chalk4.green(` ${prefix}${formatTokens(outTokens)}`));
3653
- }
3654
- parts.push(chalk4.dim(`${elapsed}s`));
3655
- const callCost = this.calculateCurrentCallCost(outTokens);
3656
- if (callCost > 0) {
3657
- parts.push(chalk4.cyan(`$${formatCost(callCost)}`));
3658
- }
3659
- return `${parts.join(chalk4.dim(" | "))} ${chalk4.cyan(spinner)}`;
3718
+ });
3660
3719
  }
3661
3720
  /**
3662
3721
  * Calculates live cost estimate for the current streaming call.
@@ -3884,7 +3943,7 @@ function addAgentOptions(cmd, defaults) {
3884
3943
  OPTION_FLAGS.logLlmRequests,
3885
3944
  OPTION_DESCRIPTIONS.logLlmRequests,
3886
3945
  defaults?.["log-llm-requests"]
3887
- ).option(OPTION_FLAGS.inputImage, OPTION_DESCRIPTIONS.inputImage).option(OPTION_FLAGS.inputAudio, OPTION_DESCRIPTIONS.inputAudio).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);
3946
+ ).option(OPTION_FLAGS.inputImage, OPTION_DESCRIPTIONS.inputImage).option(OPTION_FLAGS.inputAudio, OPTION_DESCRIPTIONS.inputAudio).option(OPTION_FLAGS.docker, OPTION_DESCRIPTIONS.docker).option(OPTION_FLAGS.dockerRo, OPTION_DESCRIPTIONS.dockerRo).option(OPTION_FLAGS.noDocker, OPTION_DESCRIPTIONS.noDocker);
3888
3947
  }
3889
3948
  function configToCompleteOptions(config) {
3890
3949
  const result = {};
@@ -3961,8 +4020,7 @@ async function executeAgent(promptArg, options, env) {
3961
4020
  const dockerOptions = {
3962
4021
  docker: options.docker ?? false,
3963
4022
  dockerRo: options.dockerRo ?? false,
3964
- noDocker: options.noDocker ?? false,
3965
- dockerDev: options.dockerDev ?? false
4023
+ noDocker: options.noDocker ?? false
3966
4024
  };
3967
4025
  const dockerEnabled = resolveDockerEnabled(
3968
4026
  env.dockerConfig,
@@ -3971,7 +4029,6 @@ async function executeAgent(promptArg, options, env) {
3971
4029
  // Profile-level docker: true/false
3972
4030
  );
3973
4031
  if (dockerEnabled) {
3974
- const devMode = resolveDevMode(env.dockerConfig, dockerOptions.dockerDev);
3975
4032
  const ctx = createDockerContext(
3976
4033
  env.dockerConfig,
3977
4034
  dockerOptions,
@@ -3982,7 +4039,7 @@ async function executeAgent(promptArg, options, env) {
3982
4039
  // Profile-level CWD permission override
3983
4040
  );
3984
4041
  try {
3985
- await executeInDocker(ctx, devMode);
4042
+ await executeInDocker(ctx);
3986
4043
  } catch (error) {
3987
4044
  if (error instanceof DockerSkipError) {
3988
4045
  } else {
@@ -4009,9 +4066,18 @@ async function executeAgent(promptArg, options, env) {
4009
4066
  registry.registerByClass(gadget);
4010
4067
  }
4011
4068
  }
4069
+ if (!options.quiet) {
4070
+ const allNames = registry.getAll().map((g) => g.name).join(", ");
4071
+ env.stderr.write(chalk5.dim(`Gadgets: ${allNames}
4072
+ `));
4073
+ }
4012
4074
  const printer = new StreamPrinter(env.stdout);
4013
4075
  const stderrTTY = env.stderr.isTTY === true;
4014
- const progress = new StreamProgress(env.stderr, stderrTTY, client.modelRegistry);
4076
+ const progress = new StreamProgress(
4077
+ env.stderr,
4078
+ stderrTTY,
4079
+ client.modelRegistry
4080
+ );
4015
4081
  const abortController = new AbortController();
4016
4082
  let wasCancelled = false;
4017
4083
  let isStreaming = false;
@@ -4021,9 +4087,11 @@ async function executeAgent(promptArg, options, env) {
4021
4087
  wasCancelled = true;
4022
4088
  abortController.abort();
4023
4089
  progress.pause();
4024
- env.stderr.write(chalk5.yellow(`
4090
+ env.stderr.write(
4091
+ chalk5.yellow(`
4025
4092
  [Cancelled] ${progress.formatStats()}
4026
- `));
4093
+ `)
4094
+ );
4027
4095
  } else {
4028
4096
  handleQuit();
4029
4097
  }
@@ -4033,7 +4101,11 @@ async function executeAgent(promptArg, options, env) {
4033
4101
  cleanupSigint: null,
4034
4102
  restore: () => {
4035
4103
  if (stdinIsInteractive && stdinStream.isTTY && !wasCancelled) {
4036
- keyboard.cleanupEsc = createEscKeyListener(stdinStream, handleCancel, handleCancel);
4104
+ keyboard.cleanupEsc = createEscKeyListener(
4105
+ stdinStream,
4106
+ handleCancel,
4107
+ handleCancel
4108
+ );
4037
4109
  }
4038
4110
  }
4039
4111
  };
@@ -4058,7 +4130,11 @@ async function executeAgent(promptArg, options, env) {
4058
4130
  process.exit(130);
4059
4131
  };
4060
4132
  if (stdinIsInteractive && stdinStream.isTTY) {
4061
- keyboard.cleanupEsc = createEscKeyListener(stdinStream, handleCancel, handleCancel);
4133
+ keyboard.cleanupEsc = createEscKeyListener(
4134
+ stdinStream,
4135
+ handleCancel,
4136
+ handleCancel
4137
+ );
4062
4138
  }
4063
4139
  keyboard.cleanupSigint = createSigintListener(
4064
4140
  handleCancel,
@@ -4084,7 +4160,12 @@ async function executeAgent(promptArg, options, env) {
4084
4160
  gadgetApprovals,
4085
4161
  defaultMode: "allowed"
4086
4162
  };
4087
- const approvalManager = new ApprovalManager(approvalConfig, env, progress, keyboard);
4163
+ const approvalManager = new ApprovalManager(
4164
+ approvalConfig,
4165
+ env,
4166
+ progress,
4167
+ keyboard
4168
+ );
4088
4169
  let usage;
4089
4170
  let iterations = 0;
4090
4171
  const llmLogsBaseDir = resolveLogDir(options.logLlmRequests, "requests");
@@ -4094,7 +4175,10 @@ async function executeAgent(promptArg, options, env) {
4094
4175
  try {
4095
4176
  return await client.countTokens(model, messages);
4096
4177
  } catch {
4097
- const totalChars = messages.reduce((sum, m) => sum + (m.content?.length ?? 0), 0);
4178
+ const totalChars = messages.reduce(
4179
+ (sum, m) => sum + (m.content?.length ?? 0),
4180
+ 0
4181
+ );
4098
4182
  return Math.round(totalChars / FALLBACK_CHARS_PER_TOKEN);
4099
4183
  }
4100
4184
  };
@@ -4116,7 +4200,9 @@ async function executeAgent(promptArg, options, env) {
4116
4200
  observers: {
4117
4201
  // onLLMCallStart: Start progress indicator for each LLM call
4118
4202
  // This showcases how to react to agent lifecycle events
4203
+ // Skip for subagent events (tracked separately via nested display)
4119
4204
  onLLMCallStart: async (context) => {
4205
+ if (context.subagentContext) return;
4120
4206
  isStreaming = true;
4121
4207
  llmCallCounter++;
4122
4208
  const inputTokens = await countMessagesTokens(
@@ -4142,7 +4228,9 @@ async function executeAgent(promptArg, options, env) {
4142
4228
  },
4143
4229
  // onStreamChunk: Real-time updates as LLM generates tokens
4144
4230
  // This enables responsive UIs that show progress during generation
4231
+ // Skip for subagent events (tracked separately via nested display)
4145
4232
  onStreamChunk: async (context) => {
4233
+ if (context.subagentContext) return;
4146
4234
  progress.update(context.accumulatedText.length);
4147
4235
  if (context.usage) {
4148
4236
  if (context.usage.inputTokens) {
@@ -4159,7 +4247,9 @@ async function executeAgent(promptArg, options, env) {
4159
4247
  },
4160
4248
  // onLLMCallComplete: Finalize metrics after each LLM call
4161
4249
  // This is where you'd typically log metrics or update dashboards
4250
+ // Skip progress updates for subagent events (tracked separately via nested display)
4162
4251
  onLLMCallComplete: async (context) => {
4252
+ if (context.subagentContext) return;
4163
4253
  isStreaming = false;
4164
4254
  usage = context.usage;
4165
4255
  iterations = Math.max(iterations, context.iteration + 1);
@@ -4188,7 +4278,7 @@ async function executeAgent(promptArg, options, env) {
4188
4278
  }
4189
4279
  const callElapsed = progress.getCallElapsedSeconds();
4190
4280
  progress.endCall(context.usage);
4191
- if (!options.quiet) {
4281
+ if (!options.quiet && !context.subagentContext) {
4192
4282
  const summary = renderSummary({
4193
4283
  iterations: context.iteration + 1,
4194
4284
  model: options.model,
@@ -4246,7 +4336,10 @@ ${ctx.gadgetName} is denied by configuration.`
4246
4336
  }
4247
4337
  return { action: "proceed" };
4248
4338
  }
4249
- const result = await approvalManager.requestApproval(ctx.gadgetName, ctx.parameters);
4339
+ const result = await approvalManager.requestApproval(
4340
+ ctx.gadgetName,
4341
+ ctx.parameters
4342
+ );
4250
4343
  if (!result.approved) {
4251
4344
  return {
4252
4345
  action: "skip",
@@ -4289,11 +4382,11 @@ Denied: ${result.reason ?? "by user"}`
4289
4382
  builder.withSyntheticGadgetCall(
4290
4383
  "TellUser",
4291
4384
  {
4292
- 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?",
4385
+ message: "\u{1F44B} Hello! I'm ready to help.\n\nWhat would you like me to work on?",
4293
4386
  done: false,
4294
4387
  type: "info"
4295
4388
  },
4296
- "\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?"
4389
+ "\u2139\uFE0F \u{1F44B} Hello! I'm ready to help.\n\nWhat would you like me to work on?"
4297
4390
  );
4298
4391
  builder.withTextOnlyHandler("acknowledge");
4299
4392
  builder.withTextWithGadgetsHandler({
@@ -4324,15 +4417,22 @@ Denied: ${result.reason ?? "by user"}`
4324
4417
  } else if (subagentEvent.type === "llm_call_end") {
4325
4418
  const info = subagentEvent.event;
4326
4419
  const subagentId = `${subagentEvent.gadgetInvocationId}:${info.iteration}`;
4327
- progress.updateNestedAgent(subagentId, info.outputTokens);
4328
- setTimeout(() => progress.removeNestedAgent(subagentId), 100);
4420
+ progress.updateNestedAgent(subagentId, {
4421
+ inputTokens: info.usage?.inputTokens ?? info.inputTokens,
4422
+ outputTokens: info.usage?.outputTokens ?? info.outputTokens,
4423
+ cachedInputTokens: info.usage?.cachedInputTokens,
4424
+ cacheCreationInputTokens: info.usage?.cacheCreationInputTokens,
4425
+ finishReason: info.finishReason,
4426
+ cost: info.cost
4427
+ });
4329
4428
  } else if (subagentEvent.type === "gadget_call") {
4330
4429
  const gadgetEvent = subagentEvent.event;
4331
4430
  progress.addNestedGadget(
4332
4431
  gadgetEvent.call.invocationId,
4333
4432
  subagentEvent.depth,
4334
4433
  subagentEvent.gadgetInvocationId,
4335
- gadgetEvent.call.gadgetName
4434
+ gadgetEvent.call.gadgetName,
4435
+ gadgetEvent.call.parameters
4336
4436
  );
4337
4437
  } else if (subagentEvent.type === "gadget_result") {
4338
4438
  const resultEvent = subagentEvent.event;
@@ -4431,7 +4531,10 @@ Denied: ${result.reason ?? "by user"}`
4431
4531
  }
4432
4532
  }
4433
4533
  function registerAgentCommand(program, env, config, globalSubagents) {
4434
- 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.");
4534
+ const cmd = program.command(COMMANDS.agent).description("Run the llmist agent loop with optional gadgets.").argument(
4535
+ "[prompt]",
4536
+ "Prompt for the agent loop. Falls back to stdin when available."
4537
+ );
4435
4538
  addAgentOptions(cmd, config);
4436
4539
  cmd.action(
4437
4540
  (prompt, options) => executeAction(() => {
@@ -4541,7 +4644,7 @@ function registerCompleteCommand(program, env, config) {
4541
4644
 
4542
4645
  // src/cli/init-command.ts
4543
4646
  import { existsSync as existsSync5, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
4544
- import { dirname as dirname2 } from "node:path";
4647
+ import { dirname } from "node:path";
4545
4648
  var STARTER_CONFIG = `# ~/.llmist/cli.toml
4546
4649
  # llmist CLI configuration file
4547
4650
  #
@@ -4597,7 +4700,7 @@ var STARTER_CONFIG = `# ~/.llmist/cli.toml
4597
4700
  `;
4598
4701
  async function executeInit(_options, env) {
4599
4702
  const configPath = getConfigPath();
4600
- const configDir = dirname2(configPath);
4703
+ const configDir = dirname(configPath);
4601
4704
  if (existsSync5(configPath)) {
4602
4705
  env.stderr.write(`Configuration already exists at ${configPath}
4603
4706
  `);