llmist 2.6.0 → 3.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,21 +1,21 @@
1
1
  #!/usr/bin/env node
2
- import "./chunk-4IHLIYW5.js";
2
+ import "./chunk-NBPKLSXJ.js";
3
3
  import {
4
+ AbstractGadget,
4
5
  AgentBuilder,
5
- BaseGadget,
6
- BreakLoopException,
7
6
  FALLBACK_CHARS_PER_TOKEN,
8
7
  GadgetRegistry,
9
- HumanInputException,
8
+ HumanInputRequiredException,
10
9
  LLMMessageBuilder,
11
10
  LLMist,
12
11
  MODEL_ALIASES,
12
+ TaskCompletionSignal,
13
13
  audioFromBuffer,
14
14
  createGadget,
15
15
  createLogger,
16
16
  detectAudioMimeType,
17
17
  detectImageMimeType,
18
- extractText,
18
+ extractMessageText,
19
19
  imageFromBuffer,
20
20
  init_builder,
21
21
  init_client,
@@ -34,7 +34,7 @@ import {
34
34
  schemaToJSONSchema,
35
35
  text,
36
36
  validateGadgetSchema
37
- } from "./chunk-364PEMVT.js";
37
+ } from "./chunk-67MMSOAT.js";
38
38
 
39
39
  // src/cli/constants.ts
40
40
  var CLI_NAME = "llmist";
@@ -46,7 +46,8 @@ var COMMANDS = {
46
46
  gadget: "gadget",
47
47
  image: "image",
48
48
  speech: "speech",
49
- vision: "vision"
49
+ vision: "vision",
50
+ init: "init"
50
51
  };
51
52
  var LOG_LEVELS = ["silly", "trace", "debug", "info", "warn", "error", "fatal"];
52
53
  var DEFAULT_MODEL = "openai:gpt-5-nano";
@@ -122,7 +123,7 @@ import { Command, InvalidArgumentError as InvalidArgumentError2 } from "commande
122
123
  // package.json
123
124
  var package_default = {
124
125
  name: "llmist",
125
- version: "2.6.0",
126
+ version: "3.0.0",
126
127
  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.",
127
128
  type: "module",
128
129
  main: "dist/index.cjs",
@@ -530,7 +531,7 @@ var askUser = createGadget({
530
531
  }
531
532
  ],
532
533
  execute: ({ question }) => {
533
- throw new HumanInputException(question);
534
+ throw new HumanInputRequiredException(question);
534
535
  }
535
536
  });
536
537
  var tellUser = createGadget({
@@ -580,11 +581,59 @@ var finish = createGadget({
580
581
  }
581
582
  ],
582
583
  execute: () => {
583
- throw new BreakLoopException("Task completed");
584
+ throw new TaskCompletionSignal("Task completed");
584
585
  }
585
586
  });
586
587
  var builtinGadgets = [askUser, tellUser, finish];
587
588
 
589
+ // src/cli/subagent-config.ts
590
+ var INHERIT_MODEL = "inherit";
591
+ function resolveSubagentConfig(subagentName, parentModel, profileConfig, globalConfig) {
592
+ const resolved = {};
593
+ const globalDefaultModel = globalConfig?.["default-model"];
594
+ const globalSubagent = extractSubagentConfig(globalConfig, subagentName);
595
+ const profileSubagent = profileConfig?.[subagentName] ?? {};
596
+ const merged = { ...globalSubagent, ...profileSubagent };
597
+ const configModel = merged.model ?? globalDefaultModel ?? INHERIT_MODEL;
598
+ resolved.model = configModel === INHERIT_MODEL ? parentModel : configModel;
599
+ for (const [key, value] of Object.entries(merged)) {
600
+ if (key !== "model") {
601
+ resolved[key] = value;
602
+ }
603
+ }
604
+ return resolved;
605
+ }
606
+ function buildSubagentConfigMap(parentModel, profileConfig, globalConfig) {
607
+ const subagentNames = /* @__PURE__ */ new Set();
608
+ if (globalConfig) {
609
+ for (const key of Object.keys(globalConfig)) {
610
+ if (key !== "default-model" && typeof globalConfig[key] === "object") {
611
+ subagentNames.add(key);
612
+ }
613
+ }
614
+ }
615
+ if (profileConfig) {
616
+ for (const key of Object.keys(profileConfig)) {
617
+ subagentNames.add(key);
618
+ }
619
+ }
620
+ const result = {};
621
+ for (const name of subagentNames) {
622
+ result[name] = resolveSubagentConfig(name, parentModel, profileConfig, globalConfig);
623
+ }
624
+ return result;
625
+ }
626
+ function extractSubagentConfig(globalConfig, subagentName) {
627
+ if (!globalConfig) {
628
+ return {};
629
+ }
630
+ const value = globalConfig[subagentName];
631
+ if (typeof value === "object" && value !== null) {
632
+ return value;
633
+ }
634
+ return {};
635
+ }
636
+
588
637
  // src/cli/config.ts
589
638
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
590
639
  import { homedir } from "node:os";
@@ -673,7 +722,7 @@ function hasTemplateSyntax(str) {
673
722
  }
674
723
 
675
724
  // src/cli/config.ts
676
- var VALID_APPROVAL_MODES = ["allowed", "denied", "approval-required"];
725
+ var VALID_PERMISSION_LEVELS = ["allowed", "denied", "approval-required"];
677
726
  var GLOBAL_CONFIG_KEYS = /* @__PURE__ */ new Set(["log-level", "log-file", "log-reset"]);
678
727
  var VALID_LOG_LEVELS = ["silly", "trace", "debug", "info", "warn", "error", "fatal"];
679
728
  var COMPLETE_CONFIG_KEYS = /* @__PURE__ */ new Set([
@@ -713,6 +762,8 @@ var AGENT_CONFIG_KEYS = /* @__PURE__ */ new Set([
713
762
  "gadget-end-prefix",
714
763
  "gadget-arg-prefix",
715
764
  "gadget-approval",
765
+ "subagents",
766
+ // Per-subagent configuration overrides
716
767
  "quiet",
717
768
  "inherits",
718
769
  "log-level",
@@ -738,9 +789,9 @@ function getConfigPath() {
738
789
  return join(homedir(), ".llmist", "cli.toml");
739
790
  }
740
791
  var ConfigError = class extends Error {
741
- constructor(message, path5) {
742
- super(path5 ? `${path5}: ${message}` : message);
743
- this.path = path5;
792
+ constructor(message, path6) {
793
+ super(path6 ? `${path6}: ${message}` : message);
794
+ this.path = path6;
744
795
  this.name = "ConfigError";
745
796
  }
746
797
  };
@@ -796,6 +847,63 @@ function validateInherits(value, section) {
796
847
  }
797
848
  throw new ConfigError(`[${section}].inherits must be a string or array of strings`);
798
849
  }
850
+ function validateSingleSubagentConfig(value, subagentName, section) {
851
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
852
+ throw new ConfigError(
853
+ `[${section}].${subagentName} must be a table (e.g., { model = "inherit", maxIterations = 20 })`
854
+ );
855
+ }
856
+ const result = {};
857
+ const rawObj = value;
858
+ for (const [key, val] of Object.entries(rawObj)) {
859
+ if (key === "model") {
860
+ if (typeof val !== "string") {
861
+ throw new ConfigError(`[${section}].${subagentName}.model must be a string`);
862
+ }
863
+ result.model = val;
864
+ } else if (key === "maxIterations") {
865
+ if (typeof val !== "number" || !Number.isInteger(val) || val < 1) {
866
+ throw new ConfigError(
867
+ `[${section}].${subagentName}.maxIterations must be a positive integer`
868
+ );
869
+ }
870
+ result.maxIterations = val;
871
+ } else {
872
+ result[key] = val;
873
+ }
874
+ }
875
+ return result;
876
+ }
877
+ function validateSubagentConfigMap(value, section) {
878
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
879
+ throw new ConfigError(
880
+ `[${section}].subagents must be a table (e.g., { BrowseWeb = { model = "inherit" } })`
881
+ );
882
+ }
883
+ const result = {};
884
+ for (const [subagentName, config] of Object.entries(value)) {
885
+ result[subagentName] = validateSingleSubagentConfig(config, subagentName, `${section}.subagents`);
886
+ }
887
+ return result;
888
+ }
889
+ function validateGlobalSubagentConfig(value, section) {
890
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
891
+ throw new ConfigError(`[${section}] must be a table`);
892
+ }
893
+ const result = {};
894
+ const rawObj = value;
895
+ for (const [key, val] of Object.entries(rawObj)) {
896
+ if (key === "default-model") {
897
+ if (typeof val !== "string") {
898
+ throw new ConfigError(`[${section}].default-model must be a string`);
899
+ }
900
+ result["default-model"] = val;
901
+ } else {
902
+ result[key] = validateSingleSubagentConfig(val, key, section);
903
+ }
904
+ }
905
+ return result;
906
+ }
799
907
  function validateGadgetApproval(value, section) {
800
908
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
801
909
  throw new ConfigError(
@@ -807,9 +915,9 @@ function validateGadgetApproval(value, section) {
807
915
  if (typeof mode !== "string") {
808
916
  throw new ConfigError(`[${section}].gadget-approval.${gadgetName} must be a string`);
809
917
  }
810
- if (!VALID_APPROVAL_MODES.includes(mode)) {
918
+ if (!VALID_PERMISSION_LEVELS.includes(mode)) {
811
919
  throw new ConfigError(
812
- `[${section}].gadget-approval.${gadgetName} must be one of: ${VALID_APPROVAL_MODES.join(", ")}`
920
+ `[${section}].gadget-approval.${gadgetName} must be one of: ${VALID_PERMISSION_LEVELS.join(", ")}`
813
921
  );
814
922
  }
815
923
  result[gadgetName] = mode;
@@ -978,6 +1086,9 @@ function validateAgentConfig(raw, section) {
978
1086
  if ("gadget-approval" in rawObj) {
979
1087
  result["gadget-approval"] = validateGadgetApproval(rawObj["gadget-approval"], section);
980
1088
  }
1089
+ if ("subagents" in rawObj) {
1090
+ result.subagents = validateSubagentConfigMap(rawObj.subagents, section);
1091
+ }
981
1092
  if ("quiet" in rawObj) {
982
1093
  result.quiet = validateBoolean(rawObj.quiet, "quiet", section);
983
1094
  }
@@ -1146,6 +1257,9 @@ function validateCustomConfig(raw, section) {
1146
1257
  if ("gadget-approval" in rawObj) {
1147
1258
  result["gadget-approval"] = validateGadgetApproval(rawObj["gadget-approval"], section);
1148
1259
  }
1260
+ if ("subagents" in rawObj) {
1261
+ result.subagents = validateSubagentConfigMap(rawObj.subagents, section);
1262
+ }
1149
1263
  if ("max-tokens" in rawObj) {
1150
1264
  result["max-tokens"] = validateNumber(rawObj["max-tokens"], "max-tokens", section, {
1151
1265
  integer: true,
@@ -1193,6 +1307,8 @@ function validateConfig(raw, configPath) {
1193
1307
  result.prompts = validatePromptsConfig(value, key);
1194
1308
  } else if (key === "docker") {
1195
1309
  result.docker = validateDockerConfig(value, key);
1310
+ } else if (key === "subagents") {
1311
+ result.subagents = validateGlobalSubagentConfig(value, key);
1196
1312
  } else {
1197
1313
  result[key] = validateCustomConfig(value, key);
1198
1314
  }
@@ -1233,7 +1349,16 @@ function loadConfig() {
1233
1349
  return resolveTemplatesInConfig(inherited, configPath);
1234
1350
  }
1235
1351
  function getCustomCommandNames(config) {
1236
- const reserved = /* @__PURE__ */ new Set(["global", "complete", "agent", "image", "speech", "prompts", "docker"]);
1352
+ const reserved = /* @__PURE__ */ new Set([
1353
+ "global",
1354
+ "complete",
1355
+ "agent",
1356
+ "image",
1357
+ "speech",
1358
+ "prompts",
1359
+ "docker",
1360
+ "subagents"
1361
+ ]);
1237
1362
  return Object.keys(config).filter((key) => !reserved.has(key));
1238
1363
  }
1239
1364
  function resolveTemplatesInConfig(config, configPath) {
@@ -1767,11 +1892,11 @@ function resolveDevMode(config, cliDevMode) {
1767
1892
  }
1768
1893
  return { enabled: true, sourcePath };
1769
1894
  }
1770
- function expandHome(path5) {
1771
- if (path5.startsWith("~")) {
1772
- return path5.replace(/^~/, homedir3());
1895
+ function expandHome(path6) {
1896
+ if (path6.startsWith("~")) {
1897
+ return path6.replace(/^~/, homedir3());
1773
1898
  }
1774
- return path5;
1899
+ return path6;
1775
1900
  }
1776
1901
  function buildDockerRunArgs(ctx, imageName, devMode) {
1777
1902
  const args = ["run", "--rm"];
@@ -1949,9 +2074,9 @@ async function readFileBuffer(filePath, options = {}) {
1949
2074
 
1950
2075
  // src/cli/gadgets.ts
1951
2076
  init_gadget();
1952
- import fs5 from "node:fs";
1953
- import path4 from "node:path";
1954
- import { pathToFileURL } from "node:url";
2077
+ import fs6 from "node:fs";
2078
+ import path5 from "node:path";
2079
+ import { pathToFileURL as pathToFileURL2 } from "node:url";
1955
2080
 
1956
2081
  // src/cli/builtins/filesystem/edit-file.ts
1957
2082
  import { z as z2 } from "zod";
@@ -2404,6 +2529,208 @@ function isBuiltinGadgetName(name) {
2404
2529
  return name in builtinGadgetRegistry;
2405
2530
  }
2406
2531
 
2532
+ // src/cli/external-gadgets.ts
2533
+ import { execSync } from "node:child_process";
2534
+ import fs5 from "node:fs";
2535
+ import path4 from "node:path";
2536
+ import os from "node:os";
2537
+ import { pathToFileURL } from "node:url";
2538
+ var CACHE_DIR2 = path4.join(os.homedir(), ".llmist", "gadget-cache");
2539
+ function isExternalPackageSpecifier(specifier) {
2540
+ if (/^@?[a-z0-9][\w.-]*(?:@[\w.-]+)?(?::[a-z]+)?(?:\/\w+)?$/i.test(specifier)) {
2541
+ return true;
2542
+ }
2543
+ if (specifier.startsWith("git+")) {
2544
+ return true;
2545
+ }
2546
+ return false;
2547
+ }
2548
+ function parseGadgetSpecifier(specifier) {
2549
+ if (specifier.startsWith("git+")) {
2550
+ const url = specifier.slice(4);
2551
+ const [baseUrl, ref] = url.split("#");
2552
+ return {
2553
+ type: "git",
2554
+ package: baseUrl,
2555
+ version: ref
2556
+ };
2557
+ }
2558
+ const npmMatch = specifier.match(
2559
+ /^(@?[a-z0-9][\w.-]*)(?:@([\w.-]+))?(?::([a-z]+))?(?:\/(\w+))?$/i
2560
+ );
2561
+ if (npmMatch) {
2562
+ const [, pkg, version, preset, gadgetName] = npmMatch;
2563
+ return {
2564
+ type: "npm",
2565
+ package: pkg,
2566
+ version,
2567
+ preset,
2568
+ gadgetName
2569
+ };
2570
+ }
2571
+ return null;
2572
+ }
2573
+ function getCacheDir(spec) {
2574
+ const versionSuffix = spec.version ? `@${spec.version}` : "@latest";
2575
+ if (spec.type === "npm") {
2576
+ return path4.join(CACHE_DIR2, "npm", `${spec.package}${versionSuffix}`);
2577
+ }
2578
+ const sanitizedUrl = spec.package.replace(/[/:]/g, "-").replace(/^-+|-+$/g, "");
2579
+ return path4.join(CACHE_DIR2, "git", `${sanitizedUrl}${versionSuffix}`);
2580
+ }
2581
+ function isCached(cacheDir) {
2582
+ const packageJsonPath = path4.join(cacheDir, "package.json");
2583
+ return fs5.existsSync(packageJsonPath);
2584
+ }
2585
+ async function installNpmPackage(spec, cacheDir) {
2586
+ fs5.mkdirSync(cacheDir, { recursive: true });
2587
+ const packageJson = {
2588
+ name: "llmist-gadget-cache",
2589
+ private: true,
2590
+ type: "module"
2591
+ };
2592
+ fs5.writeFileSync(path4.join(cacheDir, "package.json"), JSON.stringify(packageJson, null, 2));
2593
+ const packageSpec = spec.version ? `${spec.package}@${spec.version}` : spec.package;
2594
+ try {
2595
+ execSync(`npm install --prefix "${cacheDir}" "${packageSpec}" --save`, {
2596
+ stdio: "pipe",
2597
+ cwd: cacheDir
2598
+ });
2599
+ } catch (error) {
2600
+ const message = error instanceof Error ? error.message : String(error);
2601
+ throw new Error(`Failed to install npm package '${packageSpec}': ${message}`);
2602
+ }
2603
+ }
2604
+ async function installGitPackage(spec, cacheDir) {
2605
+ fs5.mkdirSync(path4.dirname(cacheDir), { recursive: true });
2606
+ if (fs5.existsSync(cacheDir)) {
2607
+ try {
2608
+ execSync("git fetch", { cwd: cacheDir, stdio: "pipe" });
2609
+ if (spec.version) {
2610
+ execSync(`git checkout ${spec.version}`, { cwd: cacheDir, stdio: "pipe" });
2611
+ }
2612
+ } catch (error) {
2613
+ fs5.rmSync(cacheDir, { recursive: true, force: true });
2614
+ }
2615
+ }
2616
+ if (!fs5.existsSync(cacheDir)) {
2617
+ try {
2618
+ const cloneCmd = spec.version ? `git clone --branch ${spec.version} "${spec.package}" "${cacheDir}"` : `git clone "${spec.package}" "${cacheDir}"`;
2619
+ execSync(cloneCmd, { stdio: "pipe" });
2620
+ } catch (error) {
2621
+ const message = error instanceof Error ? error.message : String(error);
2622
+ throw new Error(`Failed to clone git repository '${spec.package}': ${message}`);
2623
+ }
2624
+ if (fs5.existsSync(path4.join(cacheDir, "package.json"))) {
2625
+ try {
2626
+ execSync("npm install", { cwd: cacheDir, stdio: "pipe" });
2627
+ } catch (error) {
2628
+ const message = error instanceof Error ? error.message : String(error);
2629
+ throw new Error(`Failed to install dependencies for '${spec.package}': ${message}`);
2630
+ }
2631
+ try {
2632
+ const packageJson = JSON.parse(fs5.readFileSync(path4.join(cacheDir, "package.json"), "utf-8"));
2633
+ if (packageJson.scripts?.build) {
2634
+ execSync("npm run build", { cwd: cacheDir, stdio: "pipe" });
2635
+ }
2636
+ } catch (error) {
2637
+ const message = error instanceof Error ? error.message : String(error);
2638
+ throw new Error(`Failed to build package '${spec.package}': ${message}`);
2639
+ }
2640
+ }
2641
+ }
2642
+ }
2643
+ function readManifest(packageDir) {
2644
+ const packageJsonPath = path4.join(packageDir, "package.json");
2645
+ if (!fs5.existsSync(packageJsonPath)) {
2646
+ return null;
2647
+ }
2648
+ try {
2649
+ const packageJson = JSON.parse(fs5.readFileSync(packageJsonPath, "utf-8"));
2650
+ return packageJson.llmist || null;
2651
+ } catch {
2652
+ return null;
2653
+ }
2654
+ }
2655
+ function getPackagePath(cacheDir, packageName) {
2656
+ const nodeModulesPath = path4.join(cacheDir, "node_modules", packageName);
2657
+ if (fs5.existsSync(nodeModulesPath)) {
2658
+ return nodeModulesPath;
2659
+ }
2660
+ return cacheDir;
2661
+ }
2662
+ async function loadExternalGadgets(specifier, forceInstall = false) {
2663
+ const spec = parseGadgetSpecifier(specifier);
2664
+ if (!spec) {
2665
+ throw new Error(`Invalid external package specifier: ${specifier}`);
2666
+ }
2667
+ const cacheDir = getCacheDir(spec);
2668
+ if (!isCached(cacheDir) || forceInstall) {
2669
+ if (spec.type === "npm") {
2670
+ await installNpmPackage(spec, cacheDir);
2671
+ } else {
2672
+ await installGitPackage(spec, cacheDir);
2673
+ }
2674
+ }
2675
+ const packagePath = getPackagePath(cacheDir, spec.package);
2676
+ const manifest = readManifest(packagePath);
2677
+ let entryPoint;
2678
+ let gadgetNames = null;
2679
+ if (spec.gadgetName) {
2680
+ gadgetNames = [spec.gadgetName];
2681
+ if (manifest?.subagents?.[spec.gadgetName]) {
2682
+ entryPoint = manifest.subagents[spec.gadgetName].entryPoint;
2683
+ } else {
2684
+ entryPoint = manifest?.gadgets || "./dist/index.js";
2685
+ }
2686
+ } else if (spec.preset) {
2687
+ if (!manifest?.presets?.[spec.preset]) {
2688
+ throw new Error(`Unknown preset '${spec.preset}' in package '${spec.package}'`);
2689
+ }
2690
+ const preset = manifest.presets[spec.preset];
2691
+ if (preset === "*") {
2692
+ gadgetNames = null;
2693
+ } else {
2694
+ gadgetNames = preset;
2695
+ }
2696
+ entryPoint = manifest?.gadgets || "./dist/index.js";
2697
+ } else {
2698
+ entryPoint = manifest?.gadgets || "./dist/index.js";
2699
+ }
2700
+ const resolvedEntryPoint = path4.resolve(packagePath, entryPoint);
2701
+ if (!fs5.existsSync(resolvedEntryPoint)) {
2702
+ throw new Error(
2703
+ `Entry point not found: ${resolvedEntryPoint}. Make sure the package is built (run 'npm run build' in the package directory).`
2704
+ );
2705
+ }
2706
+ const moduleUrl = pathToFileURL(resolvedEntryPoint).href;
2707
+ let exports;
2708
+ try {
2709
+ exports = await import(moduleUrl);
2710
+ } catch (error) {
2711
+ const message = error instanceof Error ? error.message : String(error);
2712
+ throw new Error(`Failed to import '${specifier}': ${message}`);
2713
+ }
2714
+ let gadgets = extractGadgetsFromModule(exports);
2715
+ if (gadgetNames) {
2716
+ const gadgetSet = new Set(gadgetNames.map((n) => n.toLowerCase()));
2717
+ gadgets = gadgets.filter((g) => {
2718
+ const name = g.name?.toLowerCase() || "";
2719
+ return gadgetSet.has(name);
2720
+ });
2721
+ const foundNames = new Set(gadgets.map((g) => g.name?.toLowerCase() || ""));
2722
+ for (const requested of gadgetNames) {
2723
+ if (!foundNames.has(requested.toLowerCase())) {
2724
+ throw new Error(`Gadget '${requested}' not found in package '${spec.package}'`);
2725
+ }
2726
+ }
2727
+ }
2728
+ if (gadgets.length === 0) {
2729
+ throw new Error(`No gadgets found in package '${spec.package}'`);
2730
+ }
2731
+ return gadgets;
2732
+ }
2733
+
2407
2734
  // src/cli/gadgets.ts
2408
2735
  var PATH_PREFIXES = [".", "/", "~"];
2409
2736
  var BUILTIN_PREFIX = "builtin:";
@@ -2419,7 +2746,17 @@ function isGadgetConstructor(value) {
2419
2746
  return false;
2420
2747
  }
2421
2748
  const prototype = value.prototype;
2422
- return Boolean(prototype) && (prototype instanceof BaseGadget || isGadgetLike(prototype));
2749
+ if (!prototype) {
2750
+ return false;
2751
+ }
2752
+ if (prototype instanceof AbstractGadget) {
2753
+ return true;
2754
+ }
2755
+ const proto = prototype;
2756
+ if (typeof proto.execute === "function") {
2757
+ return true;
2758
+ }
2759
+ return isGadgetLike(prototype);
2423
2760
  }
2424
2761
  function expandHomePath(input) {
2425
2762
  if (!input.startsWith("~")) {
@@ -2429,10 +2766,10 @@ function expandHomePath(input) {
2429
2766
  if (!home) {
2430
2767
  return input;
2431
2768
  }
2432
- return path4.join(home, input.slice(1));
2769
+ return path5.join(home, input.slice(1));
2433
2770
  }
2434
2771
  function isFileLikeSpecifier(specifier) {
2435
- return PATH_PREFIXES.some((prefix) => specifier.startsWith(prefix)) || specifier.includes(path4.sep);
2772
+ return PATH_PREFIXES.some((prefix) => specifier.startsWith(prefix)) || specifier.includes(path5.sep);
2436
2773
  }
2437
2774
  function tryResolveBuiltin(specifier) {
2438
2775
  if (specifier.startsWith(BUILTIN_PREFIX)) {
@@ -2455,11 +2792,11 @@ function resolveGadgetSpecifier(specifier, cwd) {
2455
2792
  return specifier;
2456
2793
  }
2457
2794
  const expanded = expandHomePath(specifier);
2458
- const resolvedPath = path4.resolve(cwd, expanded);
2459
- if (!fs5.existsSync(resolvedPath)) {
2795
+ const resolvedPath = path5.resolve(cwd, expanded);
2796
+ if (!fs6.existsSync(resolvedPath)) {
2460
2797
  throw new Error(`Gadget module not found at ${resolvedPath}`);
2461
2798
  }
2462
- return pathToFileURL(resolvedPath).href;
2799
+ return pathToFileURL2(resolvedPath).href;
2463
2800
  }
2464
2801
  function extractGadgetsFromModule(moduleExports) {
2465
2802
  const results = [];
@@ -2472,7 +2809,7 @@ function extractGadgetsFromModule(moduleExports) {
2472
2809
  return;
2473
2810
  }
2474
2811
  visited.add(value);
2475
- if (value instanceof BaseGadget || isGadgetLike(value)) {
2812
+ if (value instanceof AbstractGadget || isGadgetLike(value)) {
2476
2813
  results.push(value);
2477
2814
  return;
2478
2815
  }
@@ -2497,12 +2834,23 @@ function extractGadgetsFromModule(moduleExports) {
2497
2834
  }
2498
2835
  async function loadGadgets(specifiers, cwd, importer = (specifier) => import(specifier)) {
2499
2836
  const gadgets = [];
2837
+ const usingDefaultImporter = importer.toString().includes("import(specifier)");
2500
2838
  for (const specifier of specifiers) {
2501
2839
  const builtin = tryResolveBuiltin(specifier);
2502
2840
  if (builtin) {
2503
2841
  gadgets.push(builtin);
2504
2842
  continue;
2505
2843
  }
2844
+ if (usingDefaultImporter && isExternalPackageSpecifier(specifier)) {
2845
+ try {
2846
+ const externalGadgets = await loadExternalGadgets(specifier);
2847
+ gadgets.push(...externalGadgets);
2848
+ continue;
2849
+ } catch (error) {
2850
+ const message = error instanceof Error ? error.message : String(error);
2851
+ throw new Error(`Failed to load external package '${specifier}': ${message}`);
2852
+ }
2853
+ }
2506
2854
  const resolved = resolveGadgetSpecifier(specifier, cwd);
2507
2855
  let exports;
2508
2856
  try {
@@ -2545,7 +2893,7 @@ function formatLlmRequest(messages) {
2545
2893
  const lines = [];
2546
2894
  for (const msg of messages) {
2547
2895
  lines.push(`=== ${msg.role.toUpperCase()} ===`);
2548
- lines.push(msg.content ? extractText(msg.content) : "");
2896
+ lines.push(msg.content ? extractMessageText(msg.content) : "");
2549
2897
  lines.push("");
2550
2898
  }
2551
2899
  return lines.join("\n");
@@ -2760,8 +3108,8 @@ function formatMediaLine(media) {
2760
3108
  const id = chalk3.cyan(media.id);
2761
3109
  const mimeType = chalk3.dim(media.mimeType);
2762
3110
  const size = chalk3.yellow(formatBytes(media.sizeBytes));
2763
- const path5 = chalk3.dim(media.path);
2764
- return `${chalk3.dim("[")}${icon} ${id} ${mimeType} ${size}${chalk3.dim("]")} ${chalk3.dim("\u2192")} ${path5}`;
3111
+ const path6 = chalk3.dim(media.path);
3112
+ return `${chalk3.dim("[")}${icon} ${id} ${mimeType} ${size}${chalk3.dim("]")} ${chalk3.dim("\u2192")} ${path6}`;
2765
3113
  }
2766
3114
  function formatGadgetSummary2(result) {
2767
3115
  const gadgetLabel = chalk3.magenta.bold(result.gadgetName);
@@ -3381,6 +3729,7 @@ function configToAgentOptions(config) {
3381
3729
  if (config.docker !== void 0) result.docker = config.docker;
3382
3730
  if (config["docker-cwd-permission"] !== void 0)
3383
3731
  result.dockerCwdPermission = config["docker-cwd-permission"];
3732
+ if (config.subagents !== void 0) result.subagents = config.subagents;
3384
3733
  return result;
3385
3734
  }
3386
3735
 
@@ -3568,7 +3917,12 @@ async function executeAgent(promptArg, options, env) {
3568
3917
  return void 0;
3569
3918
  }
3570
3919
  };
3571
- const builder = new AgentBuilder(client).withModel(options.model).withLogger(env.createLogger("llmist:cli:agent")).withHooks({
3920
+ const resolvedSubagentConfig = buildSubagentConfigMap(
3921
+ options.model,
3922
+ options.subagents,
3923
+ options.globalSubagents
3924
+ );
3925
+ const builder = new AgentBuilder(client).withModel(options.model).withSubagentConfig(resolvedSubagentConfig).withLogger(env.createLogger("llmist:cli:agent")).withHooks({
3572
3926
  observers: {
3573
3927
  // onLLMCallStart: Start progress indicator for each LLM call
3574
3928
  // This showcases how to react to agent lifecycle events
@@ -3838,14 +4192,16 @@ Denied: ${result.reason ?? "by user"}`
3838
4192
  }
3839
4193
  }
3840
4194
  }
3841
- function registerAgentCommand(program, env, config) {
4195
+ function registerAgentCommand(program, env, config, globalSubagents) {
3842
4196
  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.");
3843
4197
  addAgentOptions(cmd, config);
3844
4198
  cmd.action(
3845
4199
  (prompt, options) => executeAction(() => {
3846
4200
  const mergedOptions = {
3847
4201
  ...options,
3848
- gadgetApproval: config?.["gadget-approval"]
4202
+ gadgetApproval: config?.["gadget-approval"],
4203
+ subagents: config?.subagents,
4204
+ globalSubagents
3849
4205
  };
3850
4206
  return executeAgent(prompt, mergedOptions, env);
3851
4207
  }, env)
@@ -3945,6 +4301,104 @@ function registerCompleteCommand(program, env, config) {
3945
4301
  );
3946
4302
  }
3947
4303
 
4304
+ // src/cli/init-command.ts
4305
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
4306
+ import { dirname as dirname2 } from "node:path";
4307
+ var STARTER_CONFIG = `# ~/.llmist/cli.toml
4308
+ # llmist CLI configuration file
4309
+ #
4310
+ # This is a minimal starter config. For a comprehensive example with all options:
4311
+ # https://github.com/zbigniewsobiecki/llmist/blob/main/examples/cli.example.toml
4312
+ #
4313
+ # Key concepts:
4314
+ # - Any section can inherit from others using: inherits = "section-name"
4315
+ # - Prompts can use templates with Eta syntax: <%~ include("@prompt-name") %>
4316
+ # - Custom sections become CLI commands: [my-command] -> llmist my-command
4317
+
4318
+ #\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4319
+ # GLOBAL OPTIONS
4320
+ # These apply to all commands. CLI flags override these settings.
4321
+ #\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4322
+ [global]
4323
+ # log-level = "info" # silly, trace, debug, info, warn, error, fatal
4324
+ # log-file = "/tmp/llmist.log" # Enable file logging (JSON format)
4325
+
4326
+ #\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4327
+ # COMPLETE COMMAND DEFAULTS
4328
+ # For single LLM responses: llmist complete "prompt"
4329
+ # Model format: provider:model (e.g., openai:gpt-4o, anthropic:claude-sonnet-4-5)
4330
+ #\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4331
+ [complete]
4332
+ # model = "openai:gpt-4o"
4333
+ # temperature = 0.7 # 0-2, higher = more creative
4334
+ # max-tokens = 4096 # Maximum response length
4335
+
4336
+ #\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4337
+ # AGENT COMMAND DEFAULTS
4338
+ # For tool-using agents: llmist agent "prompt"
4339
+ #\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4340
+ [agent]
4341
+ # model = "anthropic:claude-sonnet-4-5"
4342
+ # max-iterations = 15 # Max tool-use loops before stopping
4343
+ # gadgets = [ # Tools the agent can use
4344
+ # "ListDirectory",
4345
+ # "ReadFile",
4346
+ # "WriteFile",
4347
+ # ]
4348
+
4349
+ #\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4350
+ # CUSTOM COMMANDS
4351
+ # Any other section becomes a new CLI command!
4352
+ # Uncomment below to create: llmist summarize "your text"
4353
+ #\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4354
+ # [summarize]
4355
+ # type = "complete" # "complete" or "agent"
4356
+ # description = "Summarize text concisely."
4357
+ # system = "Summarize the following text in 2-3 bullet points."
4358
+ # temperature = 0.3
4359
+ `;
4360
+ async function executeInit(_options, env) {
4361
+ const configPath = getConfigPath();
4362
+ const configDir = dirname2(configPath);
4363
+ if (existsSync5(configPath)) {
4364
+ env.stderr.write(`Configuration already exists at ${configPath}
4365
+ `);
4366
+ env.stderr.write("\n");
4367
+ env.stderr.write(`To view it: cat ${configPath}
4368
+ `);
4369
+ env.stderr.write(`To reset: rm ${configPath} && llmist init
4370
+ `);
4371
+ return;
4372
+ }
4373
+ if (!existsSync5(configDir)) {
4374
+ mkdirSync2(configDir, { recursive: true });
4375
+ }
4376
+ writeFileSync2(configPath, STARTER_CONFIG, "utf-8");
4377
+ env.stderr.write(`Created ${configPath}
4378
+ `);
4379
+ env.stderr.write("\n");
4380
+ env.stderr.write("Next steps:\n");
4381
+ env.stderr.write(" 1. Set your API key:\n");
4382
+ env.stderr.write(" export OPENAI_API_KEY=sk-...\n");
4383
+ env.stderr.write(" export ANTHROPIC_API_KEY=sk-...\n");
4384
+ env.stderr.write(" export GEMINI_API_KEY=...\n");
4385
+ env.stderr.write("\n");
4386
+ env.stderr.write(` 2. Customize your config:
4387
+ `);
4388
+ env.stderr.write(` $EDITOR ${configPath}
4389
+ `);
4390
+ env.stderr.write("\n");
4391
+ env.stderr.write(" 3. See all options:\n");
4392
+ env.stderr.write(
4393
+ " https://github.com/zbigniewsobiecki/llmist/blob/main/examples/cli.example.toml\n"
4394
+ );
4395
+ env.stderr.write("\n");
4396
+ env.stderr.write('Try it: llmist complete "Hello, world!"\n');
4397
+ }
4398
+ function registerInitCommand(program, env) {
4399
+ program.command(COMMANDS.init).description("Initialize llmist configuration at ~/.llmist/cli.toml").action((options) => executeAction(() => executeInit(options, env), env));
4400
+ }
4401
+
3948
4402
  // src/cli/environment.ts
3949
4403
  init_client();
3950
4404
  init_logger();
@@ -4047,7 +4501,7 @@ function createCommandEnvironment(baseEnv, config) {
4047
4501
  createLogger: createLoggerFactory(loggerConfig)
4048
4502
  };
4049
4503
  }
4050
- function registerCustomCommand(program, name, config, env) {
4504
+ function registerCustomCommand(program, name, config, env, globalSubagents) {
4051
4505
  const type = config.type ?? "agent";
4052
4506
  const description = config.description ?? `Custom ${type} command`;
4053
4507
  const cmd = program.command(name).description(description).argument("[prompt]", "Prompt for the command. Falls back to stdin when available.");
@@ -4072,7 +4526,8 @@ function registerCustomCommand(program, name, config, env) {
4072
4526
  const configDefaults = configToAgentOptions(config);
4073
4527
  const options = {
4074
4528
  ...configDefaults,
4075
- ...cliOptions
4529
+ ...cliOptions,
4530
+ globalSubagents
4076
4531
  };
4077
4532
  await executeAgent(prompt, options, cmdEnv);
4078
4533
  }, cmdEnv);
@@ -4515,7 +4970,7 @@ function registerGadgetCommand(program, env) {
4515
4970
  }
4516
4971
 
4517
4972
  // src/cli/image-command.ts
4518
- import { writeFileSync as writeFileSync2 } from "node:fs";
4973
+ import { writeFileSync as writeFileSync3 } from "node:fs";
4519
4974
  var DEFAULT_IMAGE_MODEL = "dall-e-3";
4520
4975
  async function executeImage(promptArg, options, env) {
4521
4976
  const prompt = await resolvePrompt(promptArg, env);
@@ -4539,7 +4994,7 @@ async function executeImage(promptArg, options, env) {
4539
4994
  const imageData = result.images[0];
4540
4995
  if (imageData.b64Json) {
4541
4996
  const buffer = Buffer.from(imageData.b64Json, "base64");
4542
- writeFileSync2(options.output, buffer);
4997
+ writeFileSync3(options.output, buffer);
4543
4998
  if (!options.quiet) {
4544
4999
  env.stderr.write(`${SUMMARY_PREFIX} Image saved to ${options.output}
4545
5000
  `);
@@ -4980,7 +5435,7 @@ function registerModelsCommand(program, env) {
4980
5435
  }
4981
5436
 
4982
5437
  // src/cli/speech-command.ts
4983
- import { writeFileSync as writeFileSync3 } from "node:fs";
5438
+ import { writeFileSync as writeFileSync4 } from "node:fs";
4984
5439
  var DEFAULT_SPEECH_MODEL = "tts-1";
4985
5440
  var DEFAULT_VOICE = "nova";
4986
5441
  async function executeSpeech(textArg, options, env) {
@@ -5003,7 +5458,7 @@ async function executeSpeech(textArg, options, env) {
5003
5458
  });
5004
5459
  const audioBuffer = Buffer.from(result.audio);
5005
5460
  if (options.output) {
5006
- writeFileSync3(options.output, audioBuffer);
5461
+ writeFileSync4(options.output, audioBuffer);
5007
5462
  if (!options.quiet) {
5008
5463
  env.stderr.write(`${SUMMARY_PREFIX} Audio saved to ${options.output}
5009
5464
  `);
@@ -5077,17 +5532,18 @@ function createProgram(env, config) {
5077
5532
  writeErr: (str) => env.stderr.write(str)
5078
5533
  });
5079
5534
  registerCompleteCommand(program, env, config?.complete);
5080
- registerAgentCommand(program, env, config?.agent);
5535
+ registerAgentCommand(program, env, config?.agent, config?.subagents);
5081
5536
  registerImageCommand(program, env, config?.image);
5082
5537
  registerSpeechCommand(program, env, config?.speech);
5083
5538
  registerVisionCommand(program, env);
5084
5539
  registerModelsCommand(program, env);
5085
5540
  registerGadgetCommand(program, env);
5541
+ registerInitCommand(program, env);
5086
5542
  if (config) {
5087
5543
  const customNames = getCustomCommandNames(config);
5088
5544
  for (const name of customNames) {
5089
5545
  const cmdConfig = config[name];
5090
- registerCustomCommand(program, name, cmdConfig, env);
5546
+ registerCustomCommand(program, name, cmdConfig, env, config.subagents);
5091
5547
  }
5092
5548
  }
5093
5549
  return program;