llmist 1.5.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +1577 -1023
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1579 -1025
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -54,7 +54,11 @@ var OPTION_FLAGS = {
|
|
|
54
54
|
logLlmResponses: "--log-llm-responses [dir]",
|
|
55
55
|
noBuiltins: "--no-builtins",
|
|
56
56
|
noBuiltinInteraction: "--no-builtin-interaction",
|
|
57
|
-
quiet: "-q, --quiet"
|
|
57
|
+
quiet: "-q, --quiet",
|
|
58
|
+
docker: "--docker",
|
|
59
|
+
dockerRo: "--docker-ro",
|
|
60
|
+
noDocker: "--no-docker",
|
|
61
|
+
dockerDev: "--docker-dev"
|
|
58
62
|
};
|
|
59
63
|
var OPTION_DESCRIPTIONS = {
|
|
60
64
|
model: "Model identifier, e.g. openai:gpt-5-nano or anthropic:claude-sonnet-4-5.",
|
|
@@ -70,7 +74,11 @@ var OPTION_DESCRIPTIONS = {
|
|
|
70
74
|
logLlmResponses: "Save raw LLM responses as plain text. Optional dir, defaults to ~/.llmist/logs/responses/",
|
|
71
75
|
noBuiltins: "Disable built-in gadgets (AskUser, TellUser).",
|
|
72
76
|
noBuiltinInteraction: "Disable interactive gadgets (AskUser) while keeping TellUser.",
|
|
73
|
-
quiet: "Suppress all output except content (text and TellUser messages)."
|
|
77
|
+
quiet: "Suppress all output except content (text and TellUser messages).",
|
|
78
|
+
docker: "Run agent in a Docker sandbox container for security isolation.",
|
|
79
|
+
dockerRo: "Run in Docker with current directory mounted read-only.",
|
|
80
|
+
noDocker: "Disable Docker sandboxing (override config).",
|
|
81
|
+
dockerDev: "Run in Docker dev mode (mount local source instead of npm install)."
|
|
74
82
|
};
|
|
75
83
|
var SUMMARY_PREFIX = "[llmist]";
|
|
76
84
|
|
|
@@ -80,7 +88,7 @@ import { Command, InvalidArgumentError as InvalidArgumentError2 } from "commande
|
|
|
80
88
|
// package.json
|
|
81
89
|
var package_default = {
|
|
82
90
|
name: "llmist",
|
|
83
|
-
version: "1.
|
|
91
|
+
version: "1.5.0",
|
|
84
92
|
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.",
|
|
85
93
|
type: "module",
|
|
86
94
|
main: "dist/index.cjs",
|
|
@@ -1830,7 +1838,7 @@ function addAgentOptions(cmd, defaults) {
|
|
|
1830
1838
|
OPTION_FLAGS.noBuiltinInteraction,
|
|
1831
1839
|
OPTION_DESCRIPTIONS.noBuiltinInteraction,
|
|
1832
1840
|
defaults?.["builtin-interaction"] !== false
|
|
1833
|
-
).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"]);
|
|
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);
|
|
1834
1842
|
}
|
|
1835
1843
|
function configToCompleteOptions(config) {
|
|
1836
1844
|
const result = {};
|
|
@@ -1865,668 +1873,791 @@ function configToAgentOptions(config) {
|
|
|
1865
1873
|
if (config.quiet !== void 0) result.quiet = config.quiet;
|
|
1866
1874
|
if (config["log-llm-requests"] !== void 0) result.logLlmRequests = config["log-llm-requests"];
|
|
1867
1875
|
if (config["log-llm-responses"] !== void 0) result.logLlmResponses = config["log-llm-responses"];
|
|
1876
|
+
if (config.docker !== void 0) result.docker = config.docker;
|
|
1877
|
+
if (config["docker-cwd-permission"] !== void 0)
|
|
1878
|
+
result.dockerCwdPermission = config["docker-cwd-permission"];
|
|
1868
1879
|
return result;
|
|
1869
1880
|
}
|
|
1870
1881
|
|
|
1871
|
-
// src/cli/
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1882
|
+
// src/cli/docker/types.ts
|
|
1883
|
+
var VALID_MOUNT_PERMISSIONS = ["ro", "rw"];
|
|
1884
|
+
var DOCKER_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
1885
|
+
"enabled",
|
|
1886
|
+
"dockerfile",
|
|
1887
|
+
"cwd-permission",
|
|
1888
|
+
"config-permission",
|
|
1889
|
+
"mounts",
|
|
1890
|
+
"env-vars",
|
|
1891
|
+
"image-name",
|
|
1892
|
+
"dev-mode",
|
|
1893
|
+
"dev-source"
|
|
1894
|
+
]);
|
|
1895
|
+
var DEFAULT_IMAGE_NAME = "llmist-sandbox";
|
|
1896
|
+
var DEFAULT_CWD_PERMISSION = "rw";
|
|
1897
|
+
var DEFAULT_CONFIG_PERMISSION = "ro";
|
|
1898
|
+
var FORWARDED_API_KEYS = [
|
|
1899
|
+
"ANTHROPIC_API_KEY",
|
|
1900
|
+
"OPENAI_API_KEY",
|
|
1901
|
+
"GEMINI_API_KEY"
|
|
1902
|
+
];
|
|
1903
|
+
var DEV_IMAGE_NAME = "llmist-dev-sandbox";
|
|
1904
|
+
var DEV_SOURCE_MOUNT_TARGET = "/llmist-src";
|
|
1905
|
+
|
|
1906
|
+
// src/cli/config.ts
|
|
1907
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
1908
|
+
import { homedir as homedir2 } from "node:os";
|
|
1909
|
+
import { join as join2 } from "node:path";
|
|
1910
|
+
import { load as parseToml } from "js-toml";
|
|
1911
|
+
|
|
1912
|
+
// src/cli/templates.ts
|
|
1913
|
+
import { Eta } from "eta";
|
|
1914
|
+
var TemplateError = class extends Error {
|
|
1915
|
+
constructor(message, promptName, configPath) {
|
|
1916
|
+
super(promptName ? `[prompts.${promptName}]: ${message}` : message);
|
|
1917
|
+
this.promptName = promptName;
|
|
1918
|
+
this.configPath = configPath;
|
|
1919
|
+
this.name = "TemplateError";
|
|
1876
1920
|
}
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1921
|
+
};
|
|
1922
|
+
function createTemplateEngine(prompts, configPath) {
|
|
1923
|
+
const eta = new Eta({
|
|
1924
|
+
views: "/",
|
|
1925
|
+
// Required but we use named templates
|
|
1926
|
+
autoEscape: false,
|
|
1927
|
+
// Don't escape - these are prompts, not HTML
|
|
1928
|
+
autoTrim: false
|
|
1929
|
+
// Preserve whitespace in prompts
|
|
1930
|
+
});
|
|
1931
|
+
for (const [name, template] of Object.entries(prompts)) {
|
|
1932
|
+
try {
|
|
1933
|
+
eta.loadTemplate(`@${name}`, template);
|
|
1934
|
+
} catch (error) {
|
|
1935
|
+
throw new TemplateError(
|
|
1936
|
+
error instanceof Error ? error.message : String(error),
|
|
1937
|
+
name,
|
|
1938
|
+
configPath
|
|
1939
|
+
);
|
|
1882
1940
|
}
|
|
1883
|
-
|
|
1941
|
+
}
|
|
1942
|
+
return eta;
|
|
1943
|
+
}
|
|
1944
|
+
function resolveTemplate(eta, template, context = {}, configPath) {
|
|
1945
|
+
try {
|
|
1946
|
+
const fullContext = {
|
|
1947
|
+
...context,
|
|
1948
|
+
env: process.env,
|
|
1949
|
+
date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
|
|
1950
|
+
// "2025-12-01"
|
|
1951
|
+
};
|
|
1952
|
+
return eta.renderString(template, fullContext);
|
|
1953
|
+
} catch (error) {
|
|
1954
|
+
throw new TemplateError(
|
|
1955
|
+
error instanceof Error ? error.message : String(error),
|
|
1956
|
+
void 0,
|
|
1957
|
+
configPath
|
|
1958
|
+
);
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
function validatePrompts(prompts, configPath) {
|
|
1962
|
+
const eta = createTemplateEngine(prompts, configPath);
|
|
1963
|
+
for (const [name, template] of Object.entries(prompts)) {
|
|
1884
1964
|
try {
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
isFirst = false;
|
|
1893
|
-
const answer = await rl.question(prompt);
|
|
1894
|
-
const trimmed = answer.trim();
|
|
1895
|
-
if (trimmed) {
|
|
1896
|
-
return trimmed;
|
|
1897
|
-
}
|
|
1898
|
-
}
|
|
1899
|
-
} finally {
|
|
1900
|
-
rl.close();
|
|
1901
|
-
keyboard.restore();
|
|
1965
|
+
eta.renderString(template, { env: {} });
|
|
1966
|
+
} catch (error) {
|
|
1967
|
+
throw new TemplateError(
|
|
1968
|
+
error instanceof Error ? error.message : String(error),
|
|
1969
|
+
name,
|
|
1970
|
+
configPath
|
|
1971
|
+
);
|
|
1902
1972
|
}
|
|
1903
|
-
}
|
|
1973
|
+
}
|
|
1904
1974
|
}
|
|
1905
|
-
|
|
1906
|
-
const
|
|
1907
|
-
const
|
|
1908
|
-
const
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1975
|
+
function validateEnvVars(template, promptName, configPath) {
|
|
1976
|
+
const envVarPattern = /<%=\s*it\.env\.(\w+)\s*%>/g;
|
|
1977
|
+
const matches = template.matchAll(envVarPattern);
|
|
1978
|
+
for (const match of matches) {
|
|
1979
|
+
const varName = match[1];
|
|
1980
|
+
if (process.env[varName] === void 0) {
|
|
1981
|
+
throw new TemplateError(
|
|
1982
|
+
`Environment variable '${varName}' is not set`,
|
|
1983
|
+
promptName,
|
|
1984
|
+
configPath
|
|
1985
|
+
);
|
|
1916
1986
|
}
|
|
1917
1987
|
}
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1988
|
+
}
|
|
1989
|
+
function hasTemplateSyntax(str) {
|
|
1990
|
+
return str.includes("<%");
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
// src/cli/config.ts
|
|
1994
|
+
var VALID_APPROVAL_MODES = ["allowed", "denied", "approval-required"];
|
|
1995
|
+
var GLOBAL_CONFIG_KEYS = /* @__PURE__ */ new Set(["log-level", "log-file", "log-reset"]);
|
|
1996
|
+
var VALID_LOG_LEVELS = ["silly", "trace", "debug", "info", "warn", "error", "fatal"];
|
|
1997
|
+
var COMPLETE_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
1998
|
+
"model",
|
|
1999
|
+
"system",
|
|
2000
|
+
"temperature",
|
|
2001
|
+
"max-tokens",
|
|
2002
|
+
"quiet",
|
|
2003
|
+
"inherits",
|
|
2004
|
+
"log-level",
|
|
2005
|
+
"log-file",
|
|
2006
|
+
"log-reset",
|
|
2007
|
+
"log-llm-requests",
|
|
2008
|
+
"log-llm-responses",
|
|
2009
|
+
"type",
|
|
2010
|
+
// Allowed for inheritance compatibility, ignored for built-in commands
|
|
2011
|
+
"docker",
|
|
2012
|
+
// Enable Docker sandboxing (only effective for agent type)
|
|
2013
|
+
"docker-cwd-permission"
|
|
2014
|
+
// Override CWD mount permission for this profile
|
|
2015
|
+
]);
|
|
2016
|
+
var AGENT_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
2017
|
+
"model",
|
|
2018
|
+
"system",
|
|
2019
|
+
"temperature",
|
|
2020
|
+
"max-iterations",
|
|
2021
|
+
"gadgets",
|
|
2022
|
+
// Full replacement (preferred)
|
|
2023
|
+
"gadget-add",
|
|
2024
|
+
// Add to inherited gadgets
|
|
2025
|
+
"gadget-remove",
|
|
2026
|
+
// Remove from inherited gadgets
|
|
2027
|
+
"gadget",
|
|
2028
|
+
// DEPRECATED: alias for gadgets
|
|
2029
|
+
"builtins",
|
|
2030
|
+
"builtin-interaction",
|
|
2031
|
+
"gadget-start-prefix",
|
|
2032
|
+
"gadget-end-prefix",
|
|
2033
|
+
"gadget-arg-prefix",
|
|
2034
|
+
"gadget-approval",
|
|
2035
|
+
"quiet",
|
|
2036
|
+
"inherits",
|
|
2037
|
+
"log-level",
|
|
2038
|
+
"log-file",
|
|
2039
|
+
"log-reset",
|
|
2040
|
+
"log-llm-requests",
|
|
2041
|
+
"log-llm-responses",
|
|
2042
|
+
"type",
|
|
2043
|
+
// Allowed for inheritance compatibility, ignored for built-in commands
|
|
2044
|
+
"docker",
|
|
2045
|
+
// Enable Docker sandboxing for this profile
|
|
2046
|
+
"docker-cwd-permission"
|
|
2047
|
+
// Override CWD mount permission for this profile
|
|
2048
|
+
]);
|
|
2049
|
+
var CUSTOM_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
2050
|
+
...COMPLETE_CONFIG_KEYS,
|
|
2051
|
+
...AGENT_CONFIG_KEYS,
|
|
2052
|
+
"type",
|
|
2053
|
+
"description"
|
|
2054
|
+
]);
|
|
2055
|
+
function getConfigPath() {
|
|
2056
|
+
return join2(homedir2(), ".llmist", "cli.toml");
|
|
2057
|
+
}
|
|
2058
|
+
var ConfigError = class extends Error {
|
|
2059
|
+
constructor(message, path5) {
|
|
2060
|
+
super(path5 ? `${path5}: ${message}` : message);
|
|
2061
|
+
this.path = path5;
|
|
2062
|
+
this.name = "ConfigError";
|
|
2063
|
+
}
|
|
2064
|
+
};
|
|
2065
|
+
function validateString(value, key, section) {
|
|
2066
|
+
if (typeof value !== "string") {
|
|
2067
|
+
throw new ConfigError(`[${section}].${key} must be a string`);
|
|
2068
|
+
}
|
|
2069
|
+
return value;
|
|
2070
|
+
}
|
|
2071
|
+
function validateNumber(value, key, section, opts) {
|
|
2072
|
+
if (typeof value !== "number") {
|
|
2073
|
+
throw new ConfigError(`[${section}].${key} must be a number`);
|
|
2074
|
+
}
|
|
2075
|
+
if (opts?.integer && !Number.isInteger(value)) {
|
|
2076
|
+
throw new ConfigError(`[${section}].${key} must be an integer`);
|
|
2077
|
+
}
|
|
2078
|
+
if (opts?.min !== void 0 && value < opts.min) {
|
|
2079
|
+
throw new ConfigError(`[${section}].${key} must be >= ${opts.min}`);
|
|
2080
|
+
}
|
|
2081
|
+
if (opts?.max !== void 0 && value > opts.max) {
|
|
2082
|
+
throw new ConfigError(`[${section}].${key} must be <= ${opts.max}`);
|
|
2083
|
+
}
|
|
2084
|
+
return value;
|
|
2085
|
+
}
|
|
2086
|
+
function validateBoolean(value, key, section) {
|
|
2087
|
+
if (typeof value !== "boolean") {
|
|
2088
|
+
throw new ConfigError(`[${section}].${key} must be a boolean`);
|
|
2089
|
+
}
|
|
2090
|
+
return value;
|
|
2091
|
+
}
|
|
2092
|
+
function validateStringArray(value, key, section) {
|
|
2093
|
+
if (!Array.isArray(value)) {
|
|
2094
|
+
throw new ConfigError(`[${section}].${key} must be an array`);
|
|
2095
|
+
}
|
|
2096
|
+
for (let i = 0; i < value.length; i++) {
|
|
2097
|
+
if (typeof value[i] !== "string") {
|
|
2098
|
+
throw new ConfigError(`[${section}].${key}[${i}] must be a string`);
|
|
1923
2099
|
}
|
|
1924
2100
|
}
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
progress.pause();
|
|
1937
|
-
env.stderr.write(chalk5.yellow(`
|
|
1938
|
-
[Cancelled] ${progress.formatStats()}
|
|
1939
|
-
`));
|
|
2101
|
+
return value;
|
|
2102
|
+
}
|
|
2103
|
+
function validateInherits(value, section) {
|
|
2104
|
+
if (typeof value === "string") {
|
|
2105
|
+
return value;
|
|
2106
|
+
}
|
|
2107
|
+
if (Array.isArray(value)) {
|
|
2108
|
+
for (let i = 0; i < value.length; i++) {
|
|
2109
|
+
if (typeof value[i] !== "string") {
|
|
2110
|
+
throw new ConfigError(`[${section}].inherits[${i}] must be a string`);
|
|
2111
|
+
}
|
|
1940
2112
|
}
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
}
|
|
2113
|
+
return value;
|
|
2114
|
+
}
|
|
2115
|
+
throw new ConfigError(`[${section}].inherits must be a string or array of strings`);
|
|
2116
|
+
}
|
|
2117
|
+
function validateGadgetApproval(value, section) {
|
|
2118
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
2119
|
+
throw new ConfigError(
|
|
2120
|
+
`[${section}].gadget-approval must be a table (e.g., { WriteFile = "approval-required" })`
|
|
2121
|
+
);
|
|
2122
|
+
}
|
|
2123
|
+
const result = {};
|
|
2124
|
+
for (const [gadgetName, mode] of Object.entries(value)) {
|
|
2125
|
+
if (typeof mode !== "string") {
|
|
2126
|
+
throw new ConfigError(
|
|
2127
|
+
`[${section}].gadget-approval.${gadgetName} must be a string`
|
|
2128
|
+
);
|
|
1949
2129
|
}
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
2130
|
+
if (!VALID_APPROVAL_MODES.includes(mode)) {
|
|
2131
|
+
throw new ConfigError(
|
|
2132
|
+
`[${section}].gadget-approval.${gadgetName} must be one of: ${VALID_APPROVAL_MODES.join(", ")}`
|
|
2133
|
+
);
|
|
2134
|
+
}
|
|
2135
|
+
result[gadgetName] = mode;
|
|
2136
|
+
}
|
|
2137
|
+
return result;
|
|
2138
|
+
}
|
|
2139
|
+
function validateLoggingConfig(raw, section) {
|
|
2140
|
+
const result = {};
|
|
2141
|
+
if ("log-level" in raw) {
|
|
2142
|
+
const level = validateString(raw["log-level"], "log-level", section);
|
|
2143
|
+
if (!VALID_LOG_LEVELS.includes(level)) {
|
|
2144
|
+
throw new ConfigError(
|
|
2145
|
+
`[${section}].log-level must be one of: ${VALID_LOG_LEVELS.join(", ")}`
|
|
2146
|
+
);
|
|
2147
|
+
}
|
|
2148
|
+
result["log-level"] = level;
|
|
2149
|
+
}
|
|
2150
|
+
if ("log-file" in raw) {
|
|
2151
|
+
result["log-file"] = validateString(raw["log-file"], "log-file", section);
|
|
2152
|
+
}
|
|
2153
|
+
if ("log-reset" in raw) {
|
|
2154
|
+
result["log-reset"] = validateBoolean(raw["log-reset"], "log-reset", section);
|
|
2155
|
+
}
|
|
2156
|
+
return result;
|
|
2157
|
+
}
|
|
2158
|
+
function validateBaseConfig(raw, section) {
|
|
2159
|
+
const result = {};
|
|
2160
|
+
if ("model" in raw) {
|
|
2161
|
+
result.model = validateString(raw.model, "model", section);
|
|
2162
|
+
}
|
|
2163
|
+
if ("system" in raw) {
|
|
2164
|
+
result.system = validateString(raw.system, "system", section);
|
|
2165
|
+
}
|
|
2166
|
+
if ("temperature" in raw) {
|
|
2167
|
+
result.temperature = validateNumber(raw.temperature, "temperature", section, {
|
|
2168
|
+
min: 0,
|
|
2169
|
+
max: 2
|
|
1961
2170
|
});
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
2171
|
+
}
|
|
2172
|
+
if ("inherits" in raw) {
|
|
2173
|
+
result.inherits = validateInherits(raw.inherits, section);
|
|
2174
|
+
}
|
|
2175
|
+
if ("docker" in raw) {
|
|
2176
|
+
result.docker = validateBoolean(raw.docker, "docker", section);
|
|
2177
|
+
}
|
|
2178
|
+
if ("docker-cwd-permission" in raw) {
|
|
2179
|
+
const perm = validateString(raw["docker-cwd-permission"], "docker-cwd-permission", section);
|
|
2180
|
+
if (perm !== "ro" && perm !== "rw") {
|
|
2181
|
+
throw new ConfigError(`[${section}].docker-cwd-permission must be "ro" or "rw"`);
|
|
1967
2182
|
}
|
|
1968
|
-
|
|
1969
|
-
process.exit(130);
|
|
1970
|
-
};
|
|
1971
|
-
if (stdinIsInteractive && stdinStream.isTTY) {
|
|
1972
|
-
keyboard.cleanupEsc = createEscKeyListener(stdinStream, handleCancel);
|
|
2183
|
+
result["docker-cwd-permission"] = perm;
|
|
1973
2184
|
}
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
const
|
|
1981
|
-
const
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
};
|
|
1985
|
-
for (const gadget of DEFAULT_APPROVAL_REQUIRED) {
|
|
1986
|
-
const normalizedGadget = gadget.toLowerCase();
|
|
1987
|
-
const isConfigured = Object.keys(userApprovals).some(
|
|
1988
|
-
(key) => key.toLowerCase() === normalizedGadget
|
|
1989
|
-
);
|
|
1990
|
-
if (!isConfigured) {
|
|
1991
|
-
gadgetApprovals[gadget] = "approval-required";
|
|
2185
|
+
return result;
|
|
2186
|
+
}
|
|
2187
|
+
function validateGlobalConfig(raw, section) {
|
|
2188
|
+
if (typeof raw !== "object" || raw === null) {
|
|
2189
|
+
throw new ConfigError(`[${section}] must be a table`);
|
|
2190
|
+
}
|
|
2191
|
+
const rawObj = raw;
|
|
2192
|
+
for (const key of Object.keys(rawObj)) {
|
|
2193
|
+
if (!GLOBAL_CONFIG_KEYS.has(key)) {
|
|
2194
|
+
throw new ConfigError(`[${section}].${key} is not a valid option`);
|
|
1992
2195
|
}
|
|
1993
2196
|
}
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
const
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
const countMessagesTokens = async (model, messages) => {
|
|
2005
|
-
try {
|
|
2006
|
-
return await client.countTokens(model, messages);
|
|
2007
|
-
} catch {
|
|
2008
|
-
const totalChars = messages.reduce((sum, m) => sum + (m.content?.length ?? 0), 0);
|
|
2009
|
-
return Math.round(totalChars / FALLBACK_CHARS_PER_TOKEN);
|
|
2197
|
+
return validateLoggingConfig(rawObj, section);
|
|
2198
|
+
}
|
|
2199
|
+
function validateCompleteConfig(raw, section) {
|
|
2200
|
+
if (typeof raw !== "object" || raw === null) {
|
|
2201
|
+
throw new ConfigError(`[${section}] must be a table`);
|
|
2202
|
+
}
|
|
2203
|
+
const rawObj = raw;
|
|
2204
|
+
for (const key of Object.keys(rawObj)) {
|
|
2205
|
+
if (!COMPLETE_CONFIG_KEYS.has(key)) {
|
|
2206
|
+
throw new ConfigError(`[${section}].${key} is not a valid option`);
|
|
2010
2207
|
}
|
|
2208
|
+
}
|
|
2209
|
+
const result = {
|
|
2210
|
+
...validateBaseConfig(rawObj, section),
|
|
2211
|
+
...validateLoggingConfig(rawObj, section)
|
|
2011
2212
|
};
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2213
|
+
if ("max-tokens" in rawObj) {
|
|
2214
|
+
result["max-tokens"] = validateNumber(rawObj["max-tokens"], "max-tokens", section, {
|
|
2215
|
+
integer: true,
|
|
2216
|
+
min: 1
|
|
2217
|
+
});
|
|
2218
|
+
}
|
|
2219
|
+
if ("quiet" in rawObj) {
|
|
2220
|
+
result.quiet = validateBoolean(rawObj.quiet, "quiet", section);
|
|
2221
|
+
}
|
|
2222
|
+
if ("log-llm-requests" in rawObj) {
|
|
2223
|
+
result["log-llm-requests"] = validateStringOrBoolean(
|
|
2224
|
+
rawObj["log-llm-requests"],
|
|
2225
|
+
"log-llm-requests",
|
|
2226
|
+
section
|
|
2227
|
+
);
|
|
2228
|
+
}
|
|
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
|
+
return result;
|
|
2237
|
+
}
|
|
2238
|
+
function validateAgentConfig(raw, section) {
|
|
2239
|
+
if (typeof raw !== "object" || raw === null) {
|
|
2240
|
+
throw new ConfigError(`[${section}] must be a table`);
|
|
2241
|
+
}
|
|
2242
|
+
const rawObj = raw;
|
|
2243
|
+
for (const key of Object.keys(rawObj)) {
|
|
2244
|
+
if (!AGENT_CONFIG_KEYS.has(key)) {
|
|
2245
|
+
throw new ConfigError(`[${section}].${key} is not a valid option`);
|
|
2019
2246
|
}
|
|
2247
|
+
}
|
|
2248
|
+
const result = {
|
|
2249
|
+
...validateBaseConfig(rawObj, section),
|
|
2250
|
+
...validateLoggingConfig(rawObj, section)
|
|
2020
2251
|
};
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
isStreaming = true;
|
|
2027
|
-
llmCallCounter++;
|
|
2028
|
-
const inputTokens = await countMessagesTokens(
|
|
2029
|
-
context.options.model,
|
|
2030
|
-
context.options.messages
|
|
2031
|
-
);
|
|
2032
|
-
progress.startCall(context.options.model, inputTokens);
|
|
2033
|
-
progress.setInputTokens(inputTokens, false);
|
|
2034
|
-
if (llmRequestsDir) {
|
|
2035
|
-
const filename = `${Date.now()}_call_${llmCallCounter}.request.txt`;
|
|
2036
|
-
const content = formatLlmRequest(context.options.messages);
|
|
2037
|
-
await writeLogFile(llmRequestsDir, filename, content);
|
|
2038
|
-
}
|
|
2039
|
-
},
|
|
2040
|
-
// onStreamChunk: Real-time updates as LLM generates tokens
|
|
2041
|
-
// This enables responsive UIs that show progress during generation
|
|
2042
|
-
onStreamChunk: async (context) => {
|
|
2043
|
-
progress.update(context.accumulatedText.length);
|
|
2044
|
-
if (context.usage) {
|
|
2045
|
-
if (context.usage.inputTokens) {
|
|
2046
|
-
progress.setInputTokens(context.usage.inputTokens, false);
|
|
2047
|
-
}
|
|
2048
|
-
if (context.usage.outputTokens) {
|
|
2049
|
-
progress.setOutputTokens(context.usage.outputTokens, false);
|
|
2050
|
-
}
|
|
2051
|
-
progress.setCachedTokens(
|
|
2052
|
-
context.usage.cachedInputTokens ?? 0,
|
|
2053
|
-
context.usage.cacheCreationInputTokens ?? 0
|
|
2054
|
-
);
|
|
2055
|
-
}
|
|
2056
|
-
},
|
|
2057
|
-
// onLLMCallComplete: Finalize metrics after each LLM call
|
|
2058
|
-
// This is where you'd typically log metrics or update dashboards
|
|
2059
|
-
onLLMCallComplete: async (context) => {
|
|
2060
|
-
isStreaming = false;
|
|
2061
|
-
usage = context.usage;
|
|
2062
|
-
iterations = Math.max(iterations, context.iteration + 1);
|
|
2063
|
-
if (context.usage) {
|
|
2064
|
-
if (context.usage.inputTokens) {
|
|
2065
|
-
progress.setInputTokens(context.usage.inputTokens, false);
|
|
2066
|
-
}
|
|
2067
|
-
if (context.usage.outputTokens) {
|
|
2068
|
-
progress.setOutputTokens(context.usage.outputTokens, false);
|
|
2069
|
-
}
|
|
2070
|
-
}
|
|
2071
|
-
let callCost;
|
|
2072
|
-
if (context.usage && client.modelRegistry) {
|
|
2073
|
-
try {
|
|
2074
|
-
const modelName = context.options.model.includes(":") ? context.options.model.split(":")[1] : context.options.model;
|
|
2075
|
-
const costResult = client.modelRegistry.estimateCost(
|
|
2076
|
-
modelName,
|
|
2077
|
-
context.usage.inputTokens,
|
|
2078
|
-
context.usage.outputTokens,
|
|
2079
|
-
context.usage.cachedInputTokens ?? 0,
|
|
2080
|
-
context.usage.cacheCreationInputTokens ?? 0
|
|
2081
|
-
);
|
|
2082
|
-
if (costResult) callCost = costResult.totalCost;
|
|
2083
|
-
} catch {
|
|
2084
|
-
}
|
|
2085
|
-
}
|
|
2086
|
-
const callElapsed = progress.getCallElapsedSeconds();
|
|
2087
|
-
progress.endCall(context.usage);
|
|
2088
|
-
if (!options.quiet) {
|
|
2089
|
-
const summary = renderSummary({
|
|
2090
|
-
iterations: context.iteration + 1,
|
|
2091
|
-
model: options.model,
|
|
2092
|
-
usage: context.usage,
|
|
2093
|
-
elapsedSeconds: callElapsed,
|
|
2094
|
-
cost: callCost,
|
|
2095
|
-
finishReason: context.finishReason
|
|
2096
|
-
});
|
|
2097
|
-
if (summary) {
|
|
2098
|
-
env.stderr.write(`${summary}
|
|
2099
|
-
`);
|
|
2100
|
-
}
|
|
2101
|
-
}
|
|
2102
|
-
if (llmResponsesDir) {
|
|
2103
|
-
const filename = `${Date.now()}_call_${llmCallCounter}.response.txt`;
|
|
2104
|
-
await writeLogFile(llmResponsesDir, filename, context.rawResponse);
|
|
2105
|
-
}
|
|
2106
|
-
}
|
|
2107
|
-
},
|
|
2108
|
-
// SHOWCASE: Controller-based approval gating for gadgets
|
|
2109
|
-
//
|
|
2110
|
-
// This demonstrates how to add safety layers WITHOUT modifying gadgets.
|
|
2111
|
-
// The ApprovalManager handles approval flows externally via beforeGadgetExecution.
|
|
2112
|
-
// Approval modes are configurable via cli.toml:
|
|
2113
|
-
// - "allowed": auto-proceed
|
|
2114
|
-
// - "denied": auto-reject, return message to LLM
|
|
2115
|
-
// - "approval-required": prompt user interactively
|
|
2116
|
-
//
|
|
2117
|
-
// Default: RunCommand, WriteFile, EditFile require approval unless overridden.
|
|
2118
|
-
controllers: {
|
|
2119
|
-
beforeGadgetExecution: async (ctx) => {
|
|
2120
|
-
const mode = approvalManager.getApprovalMode(ctx.gadgetName);
|
|
2121
|
-
if (mode === "allowed") {
|
|
2122
|
-
return { action: "proceed" };
|
|
2123
|
-
}
|
|
2124
|
-
const stdinTTY = isInteractive(env.stdin);
|
|
2125
|
-
const stderrTTY2 = env.stderr.isTTY === true;
|
|
2126
|
-
const canPrompt = stdinTTY && stderrTTY2;
|
|
2127
|
-
if (!canPrompt) {
|
|
2128
|
-
if (mode === "approval-required") {
|
|
2129
|
-
return {
|
|
2130
|
-
action: "skip",
|
|
2131
|
-
syntheticResult: `status=denied
|
|
2132
|
-
|
|
2133
|
-
${ctx.gadgetName} requires interactive approval. Run in a terminal to approve.`
|
|
2134
|
-
};
|
|
2135
|
-
}
|
|
2136
|
-
if (mode === "denied") {
|
|
2137
|
-
return {
|
|
2138
|
-
action: "skip",
|
|
2139
|
-
syntheticResult: `status=denied
|
|
2140
|
-
|
|
2141
|
-
${ctx.gadgetName} is denied by configuration.`
|
|
2142
|
-
};
|
|
2143
|
-
}
|
|
2144
|
-
return { action: "proceed" };
|
|
2145
|
-
}
|
|
2146
|
-
const result = await approvalManager.requestApproval(ctx.gadgetName, ctx.parameters);
|
|
2147
|
-
if (!result.approved) {
|
|
2148
|
-
return {
|
|
2149
|
-
action: "skip",
|
|
2150
|
-
syntheticResult: `status=denied
|
|
2151
|
-
|
|
2152
|
-
Denied: ${result.reason ?? "by user"}`
|
|
2153
|
-
};
|
|
2154
|
-
}
|
|
2155
|
-
return { action: "proceed" };
|
|
2156
|
-
}
|
|
2157
|
-
}
|
|
2158
|
-
});
|
|
2159
|
-
if (options.system) {
|
|
2160
|
-
builder.withSystem(options.system);
|
|
2252
|
+
if ("max-iterations" in rawObj) {
|
|
2253
|
+
result["max-iterations"] = validateNumber(rawObj["max-iterations"], "max-iterations", section, {
|
|
2254
|
+
integer: true,
|
|
2255
|
+
min: 1
|
|
2256
|
+
});
|
|
2161
2257
|
}
|
|
2162
|
-
if (
|
|
2163
|
-
|
|
2258
|
+
if ("gadgets" in rawObj) {
|
|
2259
|
+
result.gadgets = validateStringArray(rawObj.gadgets, "gadgets", section);
|
|
2164
2260
|
}
|
|
2165
|
-
if (
|
|
2166
|
-
|
|
2261
|
+
if ("gadget-add" in rawObj) {
|
|
2262
|
+
result["gadget-add"] = validateStringArray(rawObj["gadget-add"], "gadget-add", section);
|
|
2167
2263
|
}
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
builder.onHumanInput(humanInputHandler);
|
|
2264
|
+
if ("gadget-remove" in rawObj) {
|
|
2265
|
+
result["gadget-remove"] = validateStringArray(rawObj["gadget-remove"], "gadget-remove", section);
|
|
2171
2266
|
}
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
if (gadgets.length > 0) {
|
|
2175
|
-
builder.withGadgets(...gadgets);
|
|
2267
|
+
if ("gadget" in rawObj) {
|
|
2268
|
+
result.gadget = validateStringArray(rawObj.gadget, "gadget", section);
|
|
2176
2269
|
}
|
|
2177
|
-
if (
|
|
2178
|
-
|
|
2270
|
+
if ("builtins" in rawObj) {
|
|
2271
|
+
result.builtins = validateBoolean(rawObj.builtins, "builtins", section);
|
|
2179
2272
|
}
|
|
2180
|
-
if (
|
|
2181
|
-
|
|
2273
|
+
if ("builtin-interaction" in rawObj) {
|
|
2274
|
+
result["builtin-interaction"] = validateBoolean(
|
|
2275
|
+
rawObj["builtin-interaction"],
|
|
2276
|
+
"builtin-interaction",
|
|
2277
|
+
section
|
|
2278
|
+
);
|
|
2182
2279
|
}
|
|
2183
|
-
if (
|
|
2184
|
-
|
|
2280
|
+
if ("gadget-start-prefix" in rawObj) {
|
|
2281
|
+
result["gadget-start-prefix"] = validateString(
|
|
2282
|
+
rawObj["gadget-start-prefix"],
|
|
2283
|
+
"gadget-start-prefix",
|
|
2284
|
+
section
|
|
2285
|
+
);
|
|
2185
2286
|
}
|
|
2186
|
-
|
|
2187
|
-
"
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
},
|
|
2193
|
-
"\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?"
|
|
2194
|
-
);
|
|
2195
|
-
builder.withTextOnlyHandler("acknowledge");
|
|
2196
|
-
builder.withTextWithGadgetsHandler({
|
|
2197
|
-
gadgetName: "TellUser",
|
|
2198
|
-
parameterMapping: (text) => ({ message: text, done: false, type: "info" }),
|
|
2199
|
-
resultMapping: (text) => `\u2139\uFE0F ${text}`
|
|
2200
|
-
});
|
|
2201
|
-
const agent = builder.ask(prompt);
|
|
2202
|
-
let textBuffer = "";
|
|
2203
|
-
const flushTextBuffer = () => {
|
|
2204
|
-
if (textBuffer) {
|
|
2205
|
-
const output = options.quiet ? textBuffer : renderMarkdownWithSeparators(textBuffer);
|
|
2206
|
-
printer.write(output);
|
|
2207
|
-
textBuffer = "";
|
|
2208
|
-
}
|
|
2209
|
-
};
|
|
2210
|
-
try {
|
|
2211
|
-
for await (const event of agent.run()) {
|
|
2212
|
-
if (event.type === "text") {
|
|
2213
|
-
progress.pause();
|
|
2214
|
-
textBuffer += event.content;
|
|
2215
|
-
} else if (event.type === "gadget_result") {
|
|
2216
|
-
flushTextBuffer();
|
|
2217
|
-
progress.pause();
|
|
2218
|
-
if (options.quiet) {
|
|
2219
|
-
if (event.result.gadgetName === "TellUser" && event.result.parameters?.message) {
|
|
2220
|
-
const message = String(event.result.parameters.message);
|
|
2221
|
-
env.stdout.write(`${message}
|
|
2222
|
-
`);
|
|
2223
|
-
}
|
|
2224
|
-
} else {
|
|
2225
|
-
const tokenCount = await countGadgetOutputTokens(event.result.result);
|
|
2226
|
-
env.stderr.write(`${formatGadgetSummary({ ...event.result, tokenCount })}
|
|
2227
|
-
`);
|
|
2228
|
-
}
|
|
2229
|
-
}
|
|
2230
|
-
}
|
|
2231
|
-
} catch (error) {
|
|
2232
|
-
if (!isAbortError(error)) {
|
|
2233
|
-
throw error;
|
|
2234
|
-
}
|
|
2235
|
-
} finally {
|
|
2236
|
-
isStreaming = false;
|
|
2237
|
-
keyboard.cleanupEsc?.();
|
|
2238
|
-
keyboard.cleanupSigint?.();
|
|
2287
|
+
if ("gadget-end-prefix" in rawObj) {
|
|
2288
|
+
result["gadget-end-prefix"] = validateString(
|
|
2289
|
+
rawObj["gadget-end-prefix"],
|
|
2290
|
+
"gadget-end-prefix",
|
|
2291
|
+
section
|
|
2292
|
+
);
|
|
2239
2293
|
}
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2294
|
+
if ("gadget-arg-prefix" in rawObj) {
|
|
2295
|
+
result["gadget-arg-prefix"] = validateString(
|
|
2296
|
+
rawObj["gadget-arg-prefix"],
|
|
2297
|
+
"gadget-arg-prefix",
|
|
2298
|
+
section
|
|
2299
|
+
);
|
|
2300
|
+
}
|
|
2301
|
+
if ("gadget-approval" in rawObj) {
|
|
2302
|
+
result["gadget-approval"] = validateGadgetApproval(rawObj["gadget-approval"], section);
|
|
2303
|
+
}
|
|
2304
|
+
if ("quiet" in rawObj) {
|
|
2305
|
+
result.quiet = validateBoolean(rawObj.quiet, "quiet", section);
|
|
2306
|
+
}
|
|
2307
|
+
if ("log-llm-requests" in rawObj) {
|
|
2308
|
+
result["log-llm-requests"] = validateStringOrBoolean(
|
|
2309
|
+
rawObj["log-llm-requests"],
|
|
2310
|
+
"log-llm-requests",
|
|
2311
|
+
section
|
|
2312
|
+
);
|
|
2313
|
+
}
|
|
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
|
+
);
|
|
2256
2320
|
}
|
|
2321
|
+
return result;
|
|
2257
2322
|
}
|
|
2258
|
-
function
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
const mergedOptions = {
|
|
2264
|
-
...options,
|
|
2265
|
-
gadgetApproval: config?.["gadget-approval"]
|
|
2266
|
-
};
|
|
2267
|
-
return executeAgent(prompt, mergedOptions, env);
|
|
2268
|
-
}, env)
|
|
2269
|
-
);
|
|
2323
|
+
function validateStringOrBoolean(value, field, section) {
|
|
2324
|
+
if (typeof value === "string" || typeof value === "boolean") {
|
|
2325
|
+
return value;
|
|
2326
|
+
}
|
|
2327
|
+
throw new ConfigError(`[${section}].${field} must be a string or boolean`);
|
|
2270
2328
|
}
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
init_model_shortcuts();
|
|
2275
|
-
init_constants();
|
|
2276
|
-
async function executeComplete(promptArg, options, env) {
|
|
2277
|
-
const prompt = await resolvePrompt(promptArg, env);
|
|
2278
|
-
const client = env.createClient();
|
|
2279
|
-
const model = resolveModel(options.model);
|
|
2280
|
-
const builder = new LLMMessageBuilder();
|
|
2281
|
-
if (options.system) {
|
|
2282
|
-
builder.addSystem(options.system);
|
|
2329
|
+
function validateCustomConfig(raw, section) {
|
|
2330
|
+
if (typeof raw !== "object" || raw === null) {
|
|
2331
|
+
throw new ConfigError(`[${section}] must be a table`);
|
|
2283
2332
|
}
|
|
2284
|
-
|
|
2285
|
-
const
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
const timestamp = Date.now();
|
|
2289
|
-
if (llmRequestsDir) {
|
|
2290
|
-
const filename = `${timestamp}_complete.request.txt`;
|
|
2291
|
-
const content = formatLlmRequest(messages);
|
|
2292
|
-
await writeLogFile(llmRequestsDir, filename, content);
|
|
2293
|
-
}
|
|
2294
|
-
const stream = client.stream({
|
|
2295
|
-
model,
|
|
2296
|
-
messages,
|
|
2297
|
-
temperature: options.temperature,
|
|
2298
|
-
maxTokens: options.maxTokens
|
|
2299
|
-
});
|
|
2300
|
-
const printer = new StreamPrinter(env.stdout);
|
|
2301
|
-
const stderrTTY = env.stderr.isTTY === true;
|
|
2302
|
-
const progress = new StreamProgress(env.stderr, stderrTTY, client.modelRegistry);
|
|
2303
|
-
const estimatedInputTokens = Math.round(prompt.length / FALLBACK_CHARS_PER_TOKEN);
|
|
2304
|
-
progress.startCall(model, estimatedInputTokens);
|
|
2305
|
-
let finishReason;
|
|
2306
|
-
let usage;
|
|
2307
|
-
let accumulatedResponse = "";
|
|
2308
|
-
for await (const chunk of stream) {
|
|
2309
|
-
if (chunk.usage) {
|
|
2310
|
-
usage = chunk.usage;
|
|
2311
|
-
if (chunk.usage.inputTokens) {
|
|
2312
|
-
progress.setInputTokens(chunk.usage.inputTokens, false);
|
|
2313
|
-
}
|
|
2314
|
-
if (chunk.usage.outputTokens) {
|
|
2315
|
-
progress.setOutputTokens(chunk.usage.outputTokens, false);
|
|
2316
|
-
}
|
|
2317
|
-
}
|
|
2318
|
-
if (chunk.text) {
|
|
2319
|
-
progress.pause();
|
|
2320
|
-
accumulatedResponse += chunk.text;
|
|
2321
|
-
progress.update(accumulatedResponse.length);
|
|
2322
|
-
printer.write(chunk.text);
|
|
2333
|
+
const rawObj = raw;
|
|
2334
|
+
for (const key of Object.keys(rawObj)) {
|
|
2335
|
+
if (!CUSTOM_CONFIG_KEYS.has(key)) {
|
|
2336
|
+
throw new ConfigError(`[${section}].${key} is not a valid option`);
|
|
2323
2337
|
}
|
|
2324
|
-
|
|
2325
|
-
|
|
2338
|
+
}
|
|
2339
|
+
let type = "agent";
|
|
2340
|
+
if ("type" in rawObj) {
|
|
2341
|
+
const typeValue = validateString(rawObj.type, "type", section);
|
|
2342
|
+
if (typeValue !== "agent" && typeValue !== "complete") {
|
|
2343
|
+
throw new ConfigError(`[${section}].type must be "agent" or "complete"`);
|
|
2326
2344
|
}
|
|
2345
|
+
type = typeValue;
|
|
2327
2346
|
}
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2347
|
+
const result = {
|
|
2348
|
+
...validateBaseConfig(rawObj, section),
|
|
2349
|
+
type
|
|
2350
|
+
};
|
|
2351
|
+
if ("description" in rawObj) {
|
|
2352
|
+
result.description = validateString(rawObj.description, "description", section);
|
|
2334
2353
|
}
|
|
2335
|
-
if (
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2354
|
+
if ("max-iterations" in rawObj) {
|
|
2355
|
+
result["max-iterations"] = validateNumber(rawObj["max-iterations"], "max-iterations", section, {
|
|
2356
|
+
integer: true,
|
|
2357
|
+
min: 1
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
if ("gadgets" in rawObj) {
|
|
2361
|
+
result.gadgets = validateStringArray(rawObj.gadgets, "gadgets", section);
|
|
2362
|
+
}
|
|
2363
|
+
if ("gadget-add" in rawObj) {
|
|
2364
|
+
result["gadget-add"] = validateStringArray(rawObj["gadget-add"], "gadget-add", section);
|
|
2365
|
+
}
|
|
2366
|
+
if ("gadget-remove" in rawObj) {
|
|
2367
|
+
result["gadget-remove"] = validateStringArray(rawObj["gadget-remove"], "gadget-remove", section);
|
|
2368
|
+
}
|
|
2369
|
+
if ("gadget" in rawObj) {
|
|
2370
|
+
result.gadget = validateStringArray(rawObj.gadget, "gadget", section);
|
|
2371
|
+
}
|
|
2372
|
+
if ("builtins" in rawObj) {
|
|
2373
|
+
result.builtins = validateBoolean(rawObj.builtins, "builtins", section);
|
|
2374
|
+
}
|
|
2375
|
+
if ("builtin-interaction" in rawObj) {
|
|
2376
|
+
result["builtin-interaction"] = validateBoolean(
|
|
2377
|
+
rawObj["builtin-interaction"],
|
|
2378
|
+
"builtin-interaction",
|
|
2379
|
+
section
|
|
2380
|
+
);
|
|
2381
|
+
}
|
|
2382
|
+
if ("gadget-start-prefix" in rawObj) {
|
|
2383
|
+
result["gadget-start-prefix"] = validateString(
|
|
2384
|
+
rawObj["gadget-start-prefix"],
|
|
2385
|
+
"gadget-start-prefix",
|
|
2386
|
+
section
|
|
2387
|
+
);
|
|
2388
|
+
}
|
|
2389
|
+
if ("gadget-end-prefix" in rawObj) {
|
|
2390
|
+
result["gadget-end-prefix"] = validateString(
|
|
2391
|
+
rawObj["gadget-end-prefix"],
|
|
2392
|
+
"gadget-end-prefix",
|
|
2393
|
+
section
|
|
2394
|
+
);
|
|
2395
|
+
}
|
|
2396
|
+
if ("gadget-arg-prefix" in rawObj) {
|
|
2397
|
+
result["gadget-arg-prefix"] = validateString(
|
|
2398
|
+
rawObj["gadget-arg-prefix"],
|
|
2399
|
+
"gadget-arg-prefix",
|
|
2400
|
+
section
|
|
2401
|
+
);
|
|
2402
|
+
}
|
|
2403
|
+
if ("gadget-approval" in rawObj) {
|
|
2404
|
+
result["gadget-approval"] = validateGadgetApproval(rawObj["gadget-approval"], section);
|
|
2405
|
+
}
|
|
2406
|
+
if ("max-tokens" in rawObj) {
|
|
2407
|
+
result["max-tokens"] = validateNumber(rawObj["max-tokens"], "max-tokens", section, {
|
|
2408
|
+
integer: true,
|
|
2409
|
+
min: 1
|
|
2410
|
+
});
|
|
2411
|
+
}
|
|
2412
|
+
if ("quiet" in rawObj) {
|
|
2413
|
+
result.quiet = validateBoolean(rawObj.quiet, "quiet", section);
|
|
2341
2414
|
}
|
|
2415
|
+
Object.assign(result, validateLoggingConfig(rawObj, section));
|
|
2416
|
+
return result;
|
|
2342
2417
|
}
|
|
2343
|
-
function
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
)
|
|
2418
|
+
function validatePromptsConfig(raw, section) {
|
|
2419
|
+
if (typeof raw !== "object" || raw === null) {
|
|
2420
|
+
throw new ConfigError(`[${section}] must be a table`);
|
|
2421
|
+
}
|
|
2422
|
+
const result = {};
|
|
2423
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
2424
|
+
if (typeof value !== "string") {
|
|
2425
|
+
throw new ConfigError(`[${section}].${key} must be a string`);
|
|
2426
|
+
}
|
|
2427
|
+
result[key] = value;
|
|
2428
|
+
}
|
|
2429
|
+
return result;
|
|
2349
2430
|
}
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
import { homedir as homedir2 } from "node:os";
|
|
2354
|
-
import { join as join2 } from "node:path";
|
|
2355
|
-
import { load as parseToml } from "js-toml";
|
|
2356
|
-
|
|
2357
|
-
// src/cli/templates.ts
|
|
2358
|
-
import { Eta } from "eta";
|
|
2359
|
-
var TemplateError = class extends Error {
|
|
2360
|
-
constructor(message, promptName, configPath) {
|
|
2361
|
-
super(promptName ? `[prompts.${promptName}]: ${message}` : message);
|
|
2362
|
-
this.promptName = promptName;
|
|
2363
|
-
this.configPath = configPath;
|
|
2364
|
-
this.name = "TemplateError";
|
|
2431
|
+
function validateConfig(raw, configPath) {
|
|
2432
|
+
if (typeof raw !== "object" || raw === null) {
|
|
2433
|
+
throw new ConfigError("Config must be a TOML table", configPath);
|
|
2365
2434
|
}
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
const
|
|
2369
|
-
views: "/",
|
|
2370
|
-
// Required but we use named templates
|
|
2371
|
-
autoEscape: false,
|
|
2372
|
-
// Don't escape - these are prompts, not HTML
|
|
2373
|
-
autoTrim: false
|
|
2374
|
-
// Preserve whitespace in prompts
|
|
2375
|
-
});
|
|
2376
|
-
for (const [name, template] of Object.entries(prompts)) {
|
|
2435
|
+
const rawObj = raw;
|
|
2436
|
+
const result = {};
|
|
2437
|
+
for (const [key, value] of Object.entries(rawObj)) {
|
|
2377
2438
|
try {
|
|
2378
|
-
|
|
2439
|
+
if (key === "global") {
|
|
2440
|
+
result.global = validateGlobalConfig(value, key);
|
|
2441
|
+
} else if (key === "complete") {
|
|
2442
|
+
result.complete = validateCompleteConfig(value, key);
|
|
2443
|
+
} else if (key === "agent") {
|
|
2444
|
+
result.agent = validateAgentConfig(value, key);
|
|
2445
|
+
} else if (key === "prompts") {
|
|
2446
|
+
result.prompts = validatePromptsConfig(value, key);
|
|
2447
|
+
} else if (key === "docker") {
|
|
2448
|
+
result.docker = validateDockerConfig(value, key);
|
|
2449
|
+
} else {
|
|
2450
|
+
result[key] = validateCustomConfig(value, key);
|
|
2451
|
+
}
|
|
2379
2452
|
} catch (error) {
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
);
|
|
2453
|
+
if (error instanceof ConfigError) {
|
|
2454
|
+
throw new ConfigError(error.message, configPath);
|
|
2455
|
+
}
|
|
2456
|
+
throw error;
|
|
2385
2457
|
}
|
|
2386
2458
|
}
|
|
2387
|
-
return
|
|
2459
|
+
return result;
|
|
2388
2460
|
}
|
|
2389
|
-
function
|
|
2461
|
+
function loadConfig() {
|
|
2462
|
+
const configPath = getConfigPath();
|
|
2463
|
+
if (!existsSync2(configPath)) {
|
|
2464
|
+
return {};
|
|
2465
|
+
}
|
|
2466
|
+
let content;
|
|
2390
2467
|
try {
|
|
2391
|
-
|
|
2392
|
-
...context,
|
|
2393
|
-
env: process.env,
|
|
2394
|
-
date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
|
|
2395
|
-
// "2025-12-01"
|
|
2396
|
-
};
|
|
2397
|
-
return eta.renderString(template, fullContext);
|
|
2468
|
+
content = readFileSync2(configPath, "utf-8");
|
|
2398
2469
|
} catch (error) {
|
|
2399
|
-
throw new
|
|
2400
|
-
error instanceof Error ? error.message :
|
|
2401
|
-
void 0,
|
|
2470
|
+
throw new ConfigError(
|
|
2471
|
+
`Failed to read config file: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
2402
2472
|
configPath
|
|
2403
2473
|
);
|
|
2404
2474
|
}
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
error instanceof Error ? error.message : String(error),
|
|
2414
|
-
name,
|
|
2415
|
-
configPath
|
|
2416
|
-
);
|
|
2417
|
-
}
|
|
2475
|
+
let raw;
|
|
2476
|
+
try {
|
|
2477
|
+
raw = parseToml(content);
|
|
2478
|
+
} catch (error) {
|
|
2479
|
+
throw new ConfigError(
|
|
2480
|
+
`Invalid TOML syntax: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
2481
|
+
configPath
|
|
2482
|
+
);
|
|
2418
2483
|
}
|
|
2484
|
+
const validated = validateConfig(raw, configPath);
|
|
2485
|
+
const inherited = resolveInheritance(validated, configPath);
|
|
2486
|
+
return resolveTemplatesInConfig(inherited, configPath);
|
|
2419
2487
|
}
|
|
2420
|
-
function
|
|
2421
|
-
const
|
|
2422
|
-
|
|
2423
|
-
for (const match of matches) {
|
|
2424
|
-
const varName = match[1];
|
|
2425
|
-
if (process.env[varName] === void 0) {
|
|
2426
|
-
throw new TemplateError(
|
|
2427
|
-
`Environment variable '${varName}' is not set`,
|
|
2428
|
-
promptName,
|
|
2429
|
-
configPath
|
|
2430
|
-
);
|
|
2431
|
-
}
|
|
2432
|
-
}
|
|
2433
|
-
}
|
|
2434
|
-
function hasTemplateSyntax(str) {
|
|
2435
|
-
return str.includes("<%");
|
|
2436
|
-
}
|
|
2437
|
-
|
|
2438
|
-
// src/cli/config.ts
|
|
2439
|
-
var VALID_APPROVAL_MODES = ["allowed", "denied", "approval-required"];
|
|
2440
|
-
var GLOBAL_CONFIG_KEYS = /* @__PURE__ */ new Set(["log-level", "log-file", "log-reset"]);
|
|
2441
|
-
var VALID_LOG_LEVELS = ["silly", "trace", "debug", "info", "warn", "error", "fatal"];
|
|
2442
|
-
var COMPLETE_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
2443
|
-
"model",
|
|
2444
|
-
"system",
|
|
2445
|
-
"temperature",
|
|
2446
|
-
"max-tokens",
|
|
2447
|
-
"quiet",
|
|
2448
|
-
"inherits",
|
|
2449
|
-
"log-level",
|
|
2450
|
-
"log-file",
|
|
2451
|
-
"log-reset",
|
|
2452
|
-
"log-llm-requests",
|
|
2453
|
-
"log-llm-responses",
|
|
2454
|
-
"type"
|
|
2455
|
-
// Allowed for inheritance compatibility, ignored for built-in commands
|
|
2456
|
-
]);
|
|
2457
|
-
var AGENT_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
2458
|
-
"model",
|
|
2459
|
-
"system",
|
|
2460
|
-
"temperature",
|
|
2461
|
-
"max-iterations",
|
|
2462
|
-
"gadgets",
|
|
2463
|
-
// Full replacement (preferred)
|
|
2464
|
-
"gadget-add",
|
|
2465
|
-
// Add to inherited gadgets
|
|
2466
|
-
"gadget-remove",
|
|
2467
|
-
// Remove from inherited gadgets
|
|
2468
|
-
"gadget",
|
|
2469
|
-
// DEPRECATED: alias for gadgets
|
|
2470
|
-
"builtins",
|
|
2471
|
-
"builtin-interaction",
|
|
2472
|
-
"gadget-start-prefix",
|
|
2473
|
-
"gadget-end-prefix",
|
|
2474
|
-
"gadget-arg-prefix",
|
|
2475
|
-
"gadget-approval",
|
|
2476
|
-
"quiet",
|
|
2477
|
-
"inherits",
|
|
2478
|
-
"log-level",
|
|
2479
|
-
"log-file",
|
|
2480
|
-
"log-reset",
|
|
2481
|
-
"log-llm-requests",
|
|
2482
|
-
"log-llm-responses",
|
|
2483
|
-
"type"
|
|
2484
|
-
// Allowed for inheritance compatibility, ignored for built-in commands
|
|
2485
|
-
]);
|
|
2486
|
-
var CUSTOM_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
2487
|
-
...COMPLETE_CONFIG_KEYS,
|
|
2488
|
-
...AGENT_CONFIG_KEYS,
|
|
2489
|
-
"type",
|
|
2490
|
-
"description"
|
|
2491
|
-
]);
|
|
2492
|
-
function getConfigPath() {
|
|
2493
|
-
return join2(homedir2(), ".llmist", "cli.toml");
|
|
2488
|
+
function getCustomCommandNames(config) {
|
|
2489
|
+
const reserved = /* @__PURE__ */ new Set(["global", "complete", "agent", "prompts", "docker"]);
|
|
2490
|
+
return Object.keys(config).filter((key) => !reserved.has(key));
|
|
2494
2491
|
}
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2492
|
+
function resolveTemplatesInConfig(config, configPath) {
|
|
2493
|
+
const prompts = config.prompts ?? {};
|
|
2494
|
+
const hasPrompts = Object.keys(prompts).length > 0;
|
|
2495
|
+
let hasTemplates = false;
|
|
2496
|
+
for (const [sectionName, section] of Object.entries(config)) {
|
|
2497
|
+
if (sectionName === "global" || sectionName === "prompts") continue;
|
|
2498
|
+
if (!section || typeof section !== "object") continue;
|
|
2499
|
+
const sectionObj = section;
|
|
2500
|
+
if (typeof sectionObj.system === "string" && hasTemplateSyntax(sectionObj.system)) {
|
|
2501
|
+
hasTemplates = true;
|
|
2502
|
+
break;
|
|
2503
|
+
}
|
|
2500
2504
|
}
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
+
for (const template of Object.values(prompts)) {
|
|
2506
|
+
if (hasTemplateSyntax(template)) {
|
|
2507
|
+
hasTemplates = true;
|
|
2508
|
+
break;
|
|
2509
|
+
}
|
|
2505
2510
|
}
|
|
2506
|
-
|
|
2511
|
+
if (!hasPrompts && !hasTemplates) {
|
|
2512
|
+
return config;
|
|
2513
|
+
}
|
|
2514
|
+
try {
|
|
2515
|
+
validatePrompts(prompts, configPath);
|
|
2516
|
+
} catch (error) {
|
|
2517
|
+
if (error instanceof TemplateError) {
|
|
2518
|
+
throw new ConfigError(error.message, configPath);
|
|
2519
|
+
}
|
|
2520
|
+
throw error;
|
|
2521
|
+
}
|
|
2522
|
+
for (const [name, template] of Object.entries(prompts)) {
|
|
2523
|
+
try {
|
|
2524
|
+
validateEnvVars(template, name, configPath);
|
|
2525
|
+
} catch (error) {
|
|
2526
|
+
if (error instanceof TemplateError) {
|
|
2527
|
+
throw new ConfigError(error.message, configPath);
|
|
2528
|
+
}
|
|
2529
|
+
throw error;
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
const eta = createTemplateEngine(prompts, configPath);
|
|
2533
|
+
const result = { ...config };
|
|
2534
|
+
for (const [sectionName, section] of Object.entries(config)) {
|
|
2535
|
+
if (sectionName === "global" || sectionName === "prompts") continue;
|
|
2536
|
+
if (!section || typeof section !== "object") continue;
|
|
2537
|
+
const sectionObj = section;
|
|
2538
|
+
if (typeof sectionObj.system === "string" && hasTemplateSyntax(sectionObj.system)) {
|
|
2539
|
+
try {
|
|
2540
|
+
validateEnvVars(sectionObj.system, void 0, configPath);
|
|
2541
|
+
} catch (error) {
|
|
2542
|
+
if (error instanceof TemplateError) {
|
|
2543
|
+
throw new ConfigError(`[${sectionName}].system: ${error.message}`, configPath);
|
|
2544
|
+
}
|
|
2545
|
+
throw error;
|
|
2546
|
+
}
|
|
2547
|
+
try {
|
|
2548
|
+
const resolved = resolveTemplate(eta, sectionObj.system, {}, configPath);
|
|
2549
|
+
result[sectionName] = {
|
|
2550
|
+
...sectionObj,
|
|
2551
|
+
system: resolved
|
|
2552
|
+
};
|
|
2553
|
+
} catch (error) {
|
|
2554
|
+
if (error instanceof TemplateError) {
|
|
2555
|
+
throw new ConfigError(`[${sectionName}].system: ${error.message}`, configPath);
|
|
2556
|
+
}
|
|
2557
|
+
throw error;
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
return result;
|
|
2507
2562
|
}
|
|
2508
|
-
function
|
|
2509
|
-
|
|
2510
|
-
|
|
2563
|
+
function resolveGadgets(section, inheritedGadgets, sectionName, configPath) {
|
|
2564
|
+
const hasGadgets = "gadgets" in section;
|
|
2565
|
+
const hasGadgetLegacy = "gadget" in section;
|
|
2566
|
+
const hasGadgetAdd = "gadget-add" in section;
|
|
2567
|
+
const hasGadgetRemove = "gadget-remove" in section;
|
|
2568
|
+
if (hasGadgetLegacy && !hasGadgets) {
|
|
2569
|
+
console.warn(
|
|
2570
|
+
`[config] Warning: [${sectionName}].gadget is deprecated, use 'gadgets' (plural) instead`
|
|
2571
|
+
);
|
|
2511
2572
|
}
|
|
2512
|
-
if (
|
|
2513
|
-
throw new ConfigError(
|
|
2573
|
+
if ((hasGadgets || hasGadgetLegacy) && (hasGadgetAdd || hasGadgetRemove)) {
|
|
2574
|
+
throw new ConfigError(
|
|
2575
|
+
`[${sectionName}] Cannot use 'gadgets' with 'gadget-add'/'gadget-remove'. Use either full replacement (gadgets) OR modification (gadget-add/gadget-remove).`,
|
|
2576
|
+
configPath
|
|
2577
|
+
);
|
|
2514
2578
|
}
|
|
2515
|
-
if (
|
|
2516
|
-
|
|
2579
|
+
if (hasGadgets) {
|
|
2580
|
+
return section.gadgets;
|
|
2517
2581
|
}
|
|
2518
|
-
if (
|
|
2519
|
-
|
|
2582
|
+
if (hasGadgetLegacy) {
|
|
2583
|
+
return section.gadget;
|
|
2584
|
+
}
|
|
2585
|
+
let result = [...inheritedGadgets];
|
|
2586
|
+
if (hasGadgetRemove) {
|
|
2587
|
+
const toRemove = new Set(section["gadget-remove"]);
|
|
2588
|
+
result = result.filter((g) => !toRemove.has(g));
|
|
2589
|
+
}
|
|
2590
|
+
if (hasGadgetAdd) {
|
|
2591
|
+
const toAdd = section["gadget-add"];
|
|
2592
|
+
result.push(...toAdd);
|
|
2593
|
+
}
|
|
2594
|
+
return result;
|
|
2595
|
+
}
|
|
2596
|
+
function resolveInheritance(config, configPath) {
|
|
2597
|
+
const resolved = {};
|
|
2598
|
+
const resolving = /* @__PURE__ */ new Set();
|
|
2599
|
+
function resolveSection(name) {
|
|
2600
|
+
if (name in resolved) {
|
|
2601
|
+
return resolved[name];
|
|
2602
|
+
}
|
|
2603
|
+
if (resolving.has(name)) {
|
|
2604
|
+
throw new ConfigError(`Circular inheritance detected: ${name}`, configPath);
|
|
2605
|
+
}
|
|
2606
|
+
const section = config[name];
|
|
2607
|
+
if (section === void 0 || typeof section !== "object") {
|
|
2608
|
+
throw new ConfigError(`Cannot inherit from unknown section: ${name}`, configPath);
|
|
2609
|
+
}
|
|
2610
|
+
resolving.add(name);
|
|
2611
|
+
const sectionObj = section;
|
|
2612
|
+
const inheritsRaw = sectionObj.inherits;
|
|
2613
|
+
const inheritsList = inheritsRaw ? Array.isArray(inheritsRaw) ? inheritsRaw : [inheritsRaw] : [];
|
|
2614
|
+
let merged = {};
|
|
2615
|
+
for (const parent of inheritsList) {
|
|
2616
|
+
const parentResolved = resolveSection(parent);
|
|
2617
|
+
merged = { ...merged, ...parentResolved };
|
|
2618
|
+
}
|
|
2619
|
+
const inheritedGadgets = merged.gadgets ?? [];
|
|
2620
|
+
const {
|
|
2621
|
+
inherits: _inherits,
|
|
2622
|
+
gadgets: _gadgets,
|
|
2623
|
+
gadget: _gadget,
|
|
2624
|
+
"gadget-add": _gadgetAdd,
|
|
2625
|
+
"gadget-remove": _gadgetRemove,
|
|
2626
|
+
...ownValues
|
|
2627
|
+
} = sectionObj;
|
|
2628
|
+
merged = { ...merged, ...ownValues };
|
|
2629
|
+
const resolvedGadgets = resolveGadgets(sectionObj, inheritedGadgets, name, configPath);
|
|
2630
|
+
if (resolvedGadgets.length > 0) {
|
|
2631
|
+
merged.gadgets = resolvedGadgets;
|
|
2632
|
+
}
|
|
2633
|
+
delete merged["gadget"];
|
|
2634
|
+
delete merged["gadget-add"];
|
|
2635
|
+
delete merged["gadget-remove"];
|
|
2636
|
+
resolving.delete(name);
|
|
2637
|
+
resolved[name] = merged;
|
|
2638
|
+
return merged;
|
|
2639
|
+
}
|
|
2640
|
+
for (const name of Object.keys(config)) {
|
|
2641
|
+
resolveSection(name);
|
|
2642
|
+
}
|
|
2643
|
+
return resolved;
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
// src/cli/docker/docker-config.ts
|
|
2647
|
+
var MOUNT_CONFIG_KEYS = /* @__PURE__ */ new Set(["source", "target", "permission"]);
|
|
2648
|
+
function validateString2(value, key, section) {
|
|
2649
|
+
if (typeof value !== "string") {
|
|
2650
|
+
throw new ConfigError(`[${section}].${key} must be a string`);
|
|
2520
2651
|
}
|
|
2521
2652
|
return value;
|
|
2522
2653
|
}
|
|
2523
|
-
function
|
|
2654
|
+
function validateBoolean2(value, key, section) {
|
|
2524
2655
|
if (typeof value !== "boolean") {
|
|
2525
2656
|
throw new ConfigError(`[${section}].${key} must be a boolean`);
|
|
2526
2657
|
}
|
|
2527
2658
|
return value;
|
|
2528
2659
|
}
|
|
2529
|
-
function
|
|
2660
|
+
function validateStringArray2(value, key, section) {
|
|
2530
2661
|
if (!Array.isArray(value)) {
|
|
2531
2662
|
throw new ConfigError(`[${section}].${key} must be an array`);
|
|
2532
2663
|
}
|
|
@@ -2537,535 +2668,949 @@ function validateStringArray(value, key, section) {
|
|
|
2537
2668
|
}
|
|
2538
2669
|
return value;
|
|
2539
2670
|
}
|
|
2540
|
-
function
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
if (typeof value[i] !== "string") {
|
|
2547
|
-
throw new ConfigError(`[${section}].inherits[${i}] must be a string`);
|
|
2548
|
-
}
|
|
2549
|
-
}
|
|
2550
|
-
return value;
|
|
2671
|
+
function validateMountPermission(value, key, section) {
|
|
2672
|
+
const str = validateString2(value, key, section);
|
|
2673
|
+
if (!VALID_MOUNT_PERMISSIONS.includes(str)) {
|
|
2674
|
+
throw new ConfigError(
|
|
2675
|
+
`[${section}].${key} must be one of: ${VALID_MOUNT_PERMISSIONS.join(", ")}`
|
|
2676
|
+
);
|
|
2551
2677
|
}
|
|
2552
|
-
|
|
2678
|
+
return str;
|
|
2553
2679
|
}
|
|
2554
|
-
function
|
|
2680
|
+
function validateMountConfig(value, index, section) {
|
|
2555
2681
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
2556
|
-
throw new ConfigError(
|
|
2557
|
-
`[${section}].gadget-approval must be a table (e.g., { WriteFile = "approval-required" })`
|
|
2558
|
-
);
|
|
2682
|
+
throw new ConfigError(`[${section}].mounts[${index}] must be a table`);
|
|
2559
2683
|
}
|
|
2560
|
-
const
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
);
|
|
2566
|
-
}
|
|
2567
|
-
if (!VALID_APPROVAL_MODES.includes(mode)) {
|
|
2568
|
-
throw new ConfigError(
|
|
2569
|
-
`[${section}].gadget-approval.${gadgetName} must be one of: ${VALID_APPROVAL_MODES.join(", ")}`
|
|
2570
|
-
);
|
|
2684
|
+
const rawObj = value;
|
|
2685
|
+
const mountSection = `${section}.mounts[${index}]`;
|
|
2686
|
+
for (const key of Object.keys(rawObj)) {
|
|
2687
|
+
if (!MOUNT_CONFIG_KEYS.has(key)) {
|
|
2688
|
+
throw new ConfigError(`[${mountSection}].${key} is not a valid mount option`);
|
|
2571
2689
|
}
|
|
2572
|
-
result[gadgetName] = mode;
|
|
2573
2690
|
}
|
|
2574
|
-
|
|
2575
|
-
}
|
|
2576
|
-
function validateLoggingConfig(raw, section) {
|
|
2577
|
-
const result = {};
|
|
2578
|
-
if ("log-level" in raw) {
|
|
2579
|
-
const level = validateString(raw["log-level"], "log-level", section);
|
|
2580
|
-
if (!VALID_LOG_LEVELS.includes(level)) {
|
|
2581
|
-
throw new ConfigError(
|
|
2582
|
-
`[${section}].log-level must be one of: ${VALID_LOG_LEVELS.join(", ")}`
|
|
2583
|
-
);
|
|
2584
|
-
}
|
|
2585
|
-
result["log-level"] = level;
|
|
2691
|
+
if (!("source" in rawObj)) {
|
|
2692
|
+
throw new ConfigError(`[${mountSection}] missing required field 'source'`);
|
|
2586
2693
|
}
|
|
2587
|
-
if ("
|
|
2588
|
-
|
|
2694
|
+
if (!("target" in rawObj)) {
|
|
2695
|
+
throw new ConfigError(`[${mountSection}] missing required field 'target'`);
|
|
2589
2696
|
}
|
|
2590
|
-
if ("
|
|
2591
|
-
|
|
2697
|
+
if (!("permission" in rawObj)) {
|
|
2698
|
+
throw new ConfigError(`[${mountSection}] missing required field 'permission'`);
|
|
2592
2699
|
}
|
|
2593
|
-
return
|
|
2700
|
+
return {
|
|
2701
|
+
source: validateString2(rawObj.source, "source", mountSection),
|
|
2702
|
+
target: validateString2(rawObj.target, "target", mountSection),
|
|
2703
|
+
permission: validateMountPermission(rawObj.permission, "permission", mountSection)
|
|
2704
|
+
};
|
|
2594
2705
|
}
|
|
2595
|
-
function
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
result.model = validateString(raw.model, "model", section);
|
|
2599
|
-
}
|
|
2600
|
-
if ("system" in raw) {
|
|
2601
|
-
result.system = validateString(raw.system, "system", section);
|
|
2602
|
-
}
|
|
2603
|
-
if ("temperature" in raw) {
|
|
2604
|
-
result.temperature = validateNumber(raw.temperature, "temperature", section, {
|
|
2605
|
-
min: 0,
|
|
2606
|
-
max: 2
|
|
2607
|
-
});
|
|
2706
|
+
function validateMountsArray(value, section) {
|
|
2707
|
+
if (!Array.isArray(value)) {
|
|
2708
|
+
throw new ConfigError(`[${section}].mounts must be an array of tables`);
|
|
2608
2709
|
}
|
|
2609
|
-
|
|
2610
|
-
|
|
2710
|
+
const result = [];
|
|
2711
|
+
for (let i = 0; i < value.length; i++) {
|
|
2712
|
+
result.push(validateMountConfig(value[i], i, section));
|
|
2611
2713
|
}
|
|
2612
2714
|
return result;
|
|
2613
2715
|
}
|
|
2614
|
-
function
|
|
2615
|
-
if (typeof raw !== "object" || raw === null) {
|
|
2616
|
-
throw new ConfigError(`[${section}] must be a table`);
|
|
2617
|
-
}
|
|
2618
|
-
const rawObj = raw;
|
|
2619
|
-
for (const key of Object.keys(rawObj)) {
|
|
2620
|
-
if (!GLOBAL_CONFIG_KEYS.has(key)) {
|
|
2621
|
-
throw new ConfigError(`[${section}].${key} is not a valid option`);
|
|
2622
|
-
}
|
|
2623
|
-
}
|
|
2624
|
-
return validateLoggingConfig(rawObj, section);
|
|
2625
|
-
}
|
|
2626
|
-
function validateCompleteConfig(raw, section) {
|
|
2716
|
+
function validateDockerConfig(raw, section) {
|
|
2627
2717
|
if (typeof raw !== "object" || raw === null) {
|
|
2628
2718
|
throw new ConfigError(`[${section}] must be a table`);
|
|
2629
2719
|
}
|
|
2630
2720
|
const rawObj = raw;
|
|
2631
2721
|
for (const key of Object.keys(rawObj)) {
|
|
2632
|
-
if (!
|
|
2722
|
+
if (!DOCKER_CONFIG_KEYS.has(key)) {
|
|
2633
2723
|
throw new ConfigError(`[${section}].${key} is not a valid option`);
|
|
2634
2724
|
}
|
|
2635
2725
|
}
|
|
2636
|
-
const result = {
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
};
|
|
2640
|
-
if ("max-tokens" in rawObj) {
|
|
2641
|
-
result["max-tokens"] = validateNumber(rawObj["max-tokens"], "max-tokens", section, {
|
|
2642
|
-
integer: true,
|
|
2643
|
-
min: 1
|
|
2644
|
-
});
|
|
2726
|
+
const result = {};
|
|
2727
|
+
if ("enabled" in rawObj) {
|
|
2728
|
+
result.enabled = validateBoolean2(rawObj.enabled, "enabled", section);
|
|
2645
2729
|
}
|
|
2646
|
-
if ("
|
|
2647
|
-
result.
|
|
2730
|
+
if ("dockerfile" in rawObj) {
|
|
2731
|
+
result.dockerfile = validateString2(rawObj.dockerfile, "dockerfile", section);
|
|
2648
2732
|
}
|
|
2649
|
-
if ("
|
|
2650
|
-
result["
|
|
2651
|
-
rawObj["
|
|
2652
|
-
"
|
|
2733
|
+
if ("cwd-permission" in rawObj) {
|
|
2734
|
+
result["cwd-permission"] = validateMountPermission(
|
|
2735
|
+
rawObj["cwd-permission"],
|
|
2736
|
+
"cwd-permission",
|
|
2653
2737
|
section
|
|
2654
2738
|
);
|
|
2655
2739
|
}
|
|
2656
|
-
if ("
|
|
2657
|
-
result["
|
|
2658
|
-
rawObj["
|
|
2659
|
-
"
|
|
2740
|
+
if ("config-permission" in rawObj) {
|
|
2741
|
+
result["config-permission"] = validateMountPermission(
|
|
2742
|
+
rawObj["config-permission"],
|
|
2743
|
+
"config-permission",
|
|
2660
2744
|
section
|
|
2661
2745
|
);
|
|
2662
2746
|
}
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
function validateAgentConfig(raw, section) {
|
|
2666
|
-
if (typeof raw !== "object" || raw === null) {
|
|
2667
|
-
throw new ConfigError(`[${section}] must be a table`);
|
|
2747
|
+
if ("mounts" in rawObj) {
|
|
2748
|
+
result.mounts = validateMountsArray(rawObj.mounts, section);
|
|
2668
2749
|
}
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
if (!AGENT_CONFIG_KEYS.has(key)) {
|
|
2672
|
-
throw new ConfigError(`[${section}].${key} is not a valid option`);
|
|
2673
|
-
}
|
|
2750
|
+
if ("env-vars" in rawObj) {
|
|
2751
|
+
result["env-vars"] = validateStringArray2(rawObj["env-vars"], "env-vars", section);
|
|
2674
2752
|
}
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
...validateLoggingConfig(rawObj, section)
|
|
2678
|
-
};
|
|
2679
|
-
if ("max-iterations" in rawObj) {
|
|
2680
|
-
result["max-iterations"] = validateNumber(rawObj["max-iterations"], "max-iterations", section, {
|
|
2681
|
-
integer: true,
|
|
2682
|
-
min: 1
|
|
2683
|
-
});
|
|
2753
|
+
if ("image-name" in rawObj) {
|
|
2754
|
+
result["image-name"] = validateString2(rawObj["image-name"], "image-name", section);
|
|
2684
2755
|
}
|
|
2685
|
-
if ("
|
|
2686
|
-
result
|
|
2756
|
+
if ("dev-mode" in rawObj) {
|
|
2757
|
+
result["dev-mode"] = validateBoolean2(rawObj["dev-mode"], "dev-mode", section);
|
|
2687
2758
|
}
|
|
2688
|
-
if ("
|
|
2689
|
-
result["
|
|
2759
|
+
if ("dev-source" in rawObj) {
|
|
2760
|
+
result["dev-source"] = validateString2(rawObj["dev-source"], "dev-source", section);
|
|
2690
2761
|
}
|
|
2691
|
-
|
|
2692
|
-
|
|
2762
|
+
return result;
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2765
|
+
// src/cli/docker/dockerfile.ts
|
|
2766
|
+
var DEFAULT_DOCKERFILE = `# llmist sandbox image
|
|
2767
|
+
# Auto-generated - customize via [docker].dockerfile in cli.toml
|
|
2768
|
+
|
|
2769
|
+
FROM oven/bun:1-debian
|
|
2770
|
+
|
|
2771
|
+
# Install essential tools
|
|
2772
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \\
|
|
2773
|
+
# ripgrep for fast file searching
|
|
2774
|
+
ripgrep \\
|
|
2775
|
+
# git for version control operations
|
|
2776
|
+
git \\
|
|
2777
|
+
# curl for downloads and API calls
|
|
2778
|
+
curl \\
|
|
2779
|
+
# ca-certificates for HTTPS
|
|
2780
|
+
ca-certificates \\
|
|
2781
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
2782
|
+
|
|
2783
|
+
# Install ast-grep for code search/refactoring
|
|
2784
|
+
# Using the official install script
|
|
2785
|
+
RUN curl -fsSL https://raw.githubusercontent.com/ast-grep/ast-grep/main/install.sh | bash \\
|
|
2786
|
+
&& mv /root/.local/bin/ast-grep /usr/local/bin/ 2>/dev/null || true \\
|
|
2787
|
+
&& mv /root/.local/bin/sg /usr/local/bin/ 2>/dev/null || true
|
|
2788
|
+
|
|
2789
|
+
# Install llmist globally via bun
|
|
2790
|
+
RUN bun add -g llmist
|
|
2791
|
+
|
|
2792
|
+
# Working directory (host CWD will be mounted here)
|
|
2793
|
+
WORKDIR /workspace
|
|
2794
|
+
|
|
2795
|
+
# Entry point - llmist with all arguments forwarded
|
|
2796
|
+
ENTRYPOINT ["llmist"]
|
|
2797
|
+
`;
|
|
2798
|
+
var DEV_DOCKERFILE = `# llmist DEV sandbox image
|
|
2799
|
+
# For development/testing with local source code
|
|
2800
|
+
|
|
2801
|
+
FROM oven/bun:1-debian
|
|
2802
|
+
|
|
2803
|
+
# Install essential tools (same as production)
|
|
2804
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \\
|
|
2805
|
+
ripgrep \\
|
|
2806
|
+
git \\
|
|
2807
|
+
curl \\
|
|
2808
|
+
ca-certificates \\
|
|
2809
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
2810
|
+
|
|
2811
|
+
# Install ast-grep for code search/refactoring
|
|
2812
|
+
RUN curl -fsSL https://raw.githubusercontent.com/ast-grep/ast-grep/main/install.sh | bash \\
|
|
2813
|
+
&& mv /root/.local/bin/ast-grep /usr/local/bin/ 2>/dev/null || true \\
|
|
2814
|
+
&& mv /root/.local/bin/sg /usr/local/bin/ 2>/dev/null || true
|
|
2815
|
+
|
|
2816
|
+
# Working directory (host CWD will be mounted here)
|
|
2817
|
+
WORKDIR /workspace
|
|
2818
|
+
|
|
2819
|
+
# Entry point - run llmist from mounted source
|
|
2820
|
+
# Source is mounted at ${DEV_SOURCE_MOUNT_TARGET}
|
|
2821
|
+
ENTRYPOINT ["bun", "run", "${DEV_SOURCE_MOUNT_TARGET}/src/cli.ts"]
|
|
2822
|
+
`;
|
|
2823
|
+
function resolveDockerfile(config, devMode = false) {
|
|
2824
|
+
if (config.dockerfile) {
|
|
2825
|
+
return config.dockerfile;
|
|
2693
2826
|
}
|
|
2694
|
-
|
|
2695
|
-
|
|
2827
|
+
return devMode ? DEV_DOCKERFILE : DEFAULT_DOCKERFILE;
|
|
2828
|
+
}
|
|
2829
|
+
function computeDockerfileHash(dockerfile) {
|
|
2830
|
+
const encoder = new TextEncoder();
|
|
2831
|
+
const data = encoder.encode(dockerfile);
|
|
2832
|
+
return Bun.hash(data).toString(16);
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2835
|
+
// src/cli/docker/image-manager.ts
|
|
2836
|
+
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "node:fs";
|
|
2837
|
+
import { homedir as homedir3 } from "node:os";
|
|
2838
|
+
import { join as join3 } from "node:path";
|
|
2839
|
+
var CACHE_DIR = join3(homedir3(), ".llmist", "docker-cache");
|
|
2840
|
+
var HASH_FILE = "image-hash.json";
|
|
2841
|
+
function ensureCacheDir() {
|
|
2842
|
+
if (!existsSync3(CACHE_DIR)) {
|
|
2843
|
+
mkdirSync(CACHE_DIR, { recursive: true });
|
|
2696
2844
|
}
|
|
2697
|
-
|
|
2698
|
-
|
|
2845
|
+
}
|
|
2846
|
+
function getCachedHash(imageName) {
|
|
2847
|
+
const hashPath = join3(CACHE_DIR, HASH_FILE);
|
|
2848
|
+
if (!existsSync3(hashPath)) {
|
|
2849
|
+
return void 0;
|
|
2699
2850
|
}
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2851
|
+
try {
|
|
2852
|
+
const content = readFileSync3(hashPath, "utf-8");
|
|
2853
|
+
const cache = JSON.parse(content);
|
|
2854
|
+
return cache[imageName]?.dockerfileHash;
|
|
2855
|
+
} catch {
|
|
2856
|
+
return void 0;
|
|
2706
2857
|
}
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2858
|
+
}
|
|
2859
|
+
function setCachedHash(imageName, hash) {
|
|
2860
|
+
ensureCacheDir();
|
|
2861
|
+
const hashPath = join3(CACHE_DIR, HASH_FILE);
|
|
2862
|
+
let cache = {};
|
|
2863
|
+
if (existsSync3(hashPath)) {
|
|
2864
|
+
try {
|
|
2865
|
+
const content = readFileSync3(hashPath, "utf-8");
|
|
2866
|
+
cache = JSON.parse(content);
|
|
2867
|
+
} catch {
|
|
2868
|
+
cache = {};
|
|
2869
|
+
}
|
|
2713
2870
|
}
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2871
|
+
cache[imageName] = {
|
|
2872
|
+
imageName,
|
|
2873
|
+
dockerfileHash: hash,
|
|
2874
|
+
builtAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2875
|
+
};
|
|
2876
|
+
writeFileSync(hashPath, JSON.stringify(cache, null, 2));
|
|
2877
|
+
}
|
|
2878
|
+
var DockerBuildError = class extends Error {
|
|
2879
|
+
constructor(message, output) {
|
|
2880
|
+
super(message);
|
|
2881
|
+
this.output = output;
|
|
2882
|
+
this.name = "DockerBuildError";
|
|
2883
|
+
}
|
|
2884
|
+
};
|
|
2885
|
+
async function buildImage(imageName, dockerfile) {
|
|
2886
|
+
ensureCacheDir();
|
|
2887
|
+
const dockerfilePath = join3(CACHE_DIR, "Dockerfile");
|
|
2888
|
+
writeFileSync(dockerfilePath, dockerfile);
|
|
2889
|
+
const proc = Bun.spawn(
|
|
2890
|
+
["docker", "build", "-t", imageName, "-f", dockerfilePath, CACHE_DIR],
|
|
2891
|
+
{
|
|
2892
|
+
stdout: "pipe",
|
|
2893
|
+
stderr: "pipe"
|
|
2894
|
+
}
|
|
2895
|
+
);
|
|
2896
|
+
const exitCode = await proc.exited;
|
|
2897
|
+
const stdout = await new Response(proc.stdout).text();
|
|
2898
|
+
const stderr = await new Response(proc.stderr).text();
|
|
2899
|
+
if (exitCode !== 0) {
|
|
2900
|
+
const output = [stdout, stderr].filter(Boolean).join("\n");
|
|
2901
|
+
throw new DockerBuildError(
|
|
2902
|
+
`Docker build failed with exit code ${exitCode}`,
|
|
2903
|
+
output
|
|
2719
2904
|
);
|
|
2720
2905
|
}
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2906
|
+
}
|
|
2907
|
+
async function ensureImage(imageName = DEFAULT_IMAGE_NAME, dockerfile) {
|
|
2908
|
+
const hash = computeDockerfileHash(dockerfile);
|
|
2909
|
+
const cachedHash = getCachedHash(imageName);
|
|
2910
|
+
if (cachedHash === hash) {
|
|
2911
|
+
return imageName;
|
|
2912
|
+
}
|
|
2913
|
+
console.error(`Building Docker image '${imageName}'...`);
|
|
2914
|
+
await buildImage(imageName, dockerfile);
|
|
2915
|
+
setCachedHash(imageName, hash);
|
|
2916
|
+
console.error(`Docker image '${imageName}' built successfully.`);
|
|
2917
|
+
return imageName;
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2920
|
+
// src/cli/docker/docker-wrapper.ts
|
|
2921
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
|
|
2922
|
+
import { dirname, join as join4 } from "node:path";
|
|
2923
|
+
import { homedir as homedir4 } from "node:os";
|
|
2924
|
+
var DockerUnavailableError = class extends Error {
|
|
2925
|
+
constructor() {
|
|
2926
|
+
super(
|
|
2927
|
+
"Docker is required but not available. Install Docker or disable Docker sandboxing in your configuration."
|
|
2726
2928
|
);
|
|
2929
|
+
this.name = "DockerUnavailableError";
|
|
2727
2930
|
}
|
|
2728
|
-
|
|
2729
|
-
|
|
2931
|
+
};
|
|
2932
|
+
var DockerSkipError = class extends Error {
|
|
2933
|
+
constructor() {
|
|
2934
|
+
super("Docker execution skipped - already inside container");
|
|
2935
|
+
this.name = "DockerSkipError";
|
|
2730
2936
|
}
|
|
2731
|
-
|
|
2732
|
-
|
|
2937
|
+
};
|
|
2938
|
+
async function checkDockerAvailable() {
|
|
2939
|
+
try {
|
|
2940
|
+
const proc = Bun.spawn(["docker", "info"], {
|
|
2941
|
+
stdout: "pipe",
|
|
2942
|
+
stderr: "pipe"
|
|
2943
|
+
});
|
|
2944
|
+
await proc.exited;
|
|
2945
|
+
return proc.exitCode === 0;
|
|
2946
|
+
} catch {
|
|
2947
|
+
return false;
|
|
2733
2948
|
}
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
section
|
|
2739
|
-
);
|
|
2949
|
+
}
|
|
2950
|
+
function isInsideContainer() {
|
|
2951
|
+
if (existsSync4("/.dockerenv")) {
|
|
2952
|
+
return true;
|
|
2740
2953
|
}
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2954
|
+
try {
|
|
2955
|
+
const cgroup = readFileSync4("/proc/1/cgroup", "utf-8");
|
|
2956
|
+
if (cgroup.includes("docker") || cgroup.includes("containerd")) {
|
|
2957
|
+
return true;
|
|
2958
|
+
}
|
|
2959
|
+
} catch {
|
|
2747
2960
|
}
|
|
2748
|
-
return
|
|
2961
|
+
return false;
|
|
2749
2962
|
}
|
|
2750
|
-
function
|
|
2751
|
-
|
|
2752
|
-
|
|
2963
|
+
function autoDetectDevSource() {
|
|
2964
|
+
const scriptPath = process.argv[1];
|
|
2965
|
+
if (!scriptPath || !scriptPath.endsWith("src/cli.ts")) {
|
|
2966
|
+
return void 0;
|
|
2753
2967
|
}
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
if (
|
|
2758
|
-
|
|
2968
|
+
const srcDir = dirname(scriptPath);
|
|
2969
|
+
const projectDir = dirname(srcDir);
|
|
2970
|
+
const packageJsonPath = join4(projectDir, "package.json");
|
|
2971
|
+
if (!existsSync4(packageJsonPath)) {
|
|
2972
|
+
return void 0;
|
|
2759
2973
|
}
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
if (
|
|
2763
|
-
|
|
2974
|
+
try {
|
|
2975
|
+
const pkg = JSON.parse(readFileSync4(packageJsonPath, "utf-8"));
|
|
2976
|
+
if (pkg.name === "llmist") {
|
|
2977
|
+
return projectDir;
|
|
2764
2978
|
}
|
|
2979
|
+
} catch {
|
|
2765
2980
|
}
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
}
|
|
2772
|
-
type = typeValue;
|
|
2981
|
+
return void 0;
|
|
2982
|
+
}
|
|
2983
|
+
function resolveDevMode(config, cliDevMode) {
|
|
2984
|
+
const enabled = cliDevMode || config?.["dev-mode"] || process.env.LLMIST_DEV_MODE === "1";
|
|
2985
|
+
if (!enabled) {
|
|
2986
|
+
return { enabled: false, sourcePath: void 0 };
|
|
2773
2987
|
}
|
|
2774
|
-
const
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
result.description = validateString(rawObj.description, "description", section);
|
|
2988
|
+
const sourcePath = config?.["dev-source"] || process.env.LLMIST_DEV_SOURCE || autoDetectDevSource();
|
|
2989
|
+
if (!sourcePath) {
|
|
2990
|
+
throw new Error(
|
|
2991
|
+
"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)."
|
|
2992
|
+
);
|
|
2780
2993
|
}
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2994
|
+
return { enabled: true, sourcePath };
|
|
2995
|
+
}
|
|
2996
|
+
function expandHome(path5) {
|
|
2997
|
+
if (path5.startsWith("~")) {
|
|
2998
|
+
return path5.replace(/^~/, homedir4());
|
|
2786
2999
|
}
|
|
2787
|
-
|
|
2788
|
-
|
|
3000
|
+
return path5;
|
|
3001
|
+
}
|
|
3002
|
+
function buildDockerRunArgs(ctx, imageName, devMode) {
|
|
3003
|
+
const args = ["run", "--rm"];
|
|
3004
|
+
const timestamp = Date.now();
|
|
3005
|
+
const random = Math.random().toString(36).slice(2, 8);
|
|
3006
|
+
const containerName = `llmist-${timestamp}-${random}`;
|
|
3007
|
+
args.push("--name", containerName);
|
|
3008
|
+
if (process.stdin.isTTY) {
|
|
3009
|
+
args.push("-it");
|
|
3010
|
+
}
|
|
3011
|
+
const cwdPermission = ctx.options.dockerRo ? "ro" : ctx.profileCwdPermission ?? ctx.config["cwd-permission"] ?? DEFAULT_CWD_PERMISSION;
|
|
3012
|
+
args.push("-v", `${ctx.cwd}:/workspace:${cwdPermission}`);
|
|
3013
|
+
args.push("-w", "/workspace");
|
|
3014
|
+
const configPermission = ctx.config["config-permission"] ?? DEFAULT_CONFIG_PERMISSION;
|
|
3015
|
+
const llmistDir = expandHome("~/.llmist");
|
|
3016
|
+
args.push("-v", `${llmistDir}:/root/.llmist:${configPermission}`);
|
|
3017
|
+
if (devMode.enabled && devMode.sourcePath) {
|
|
3018
|
+
const expandedSource = expandHome(devMode.sourcePath);
|
|
3019
|
+
args.push("-v", `${expandedSource}:${DEV_SOURCE_MOUNT_TARGET}:ro`);
|
|
3020
|
+
}
|
|
3021
|
+
if (ctx.config.mounts) {
|
|
3022
|
+
for (const mount of ctx.config.mounts) {
|
|
3023
|
+
const source = expandHome(mount.source);
|
|
3024
|
+
args.push("-v", `${source}:${mount.target}:${mount.permission}`);
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
for (const key of FORWARDED_API_KEYS) {
|
|
3028
|
+
if (process.env[key]) {
|
|
3029
|
+
args.push("-e", key);
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
if (ctx.config["env-vars"]) {
|
|
3033
|
+
for (const key of ctx.config["env-vars"]) {
|
|
3034
|
+
if (process.env[key]) {
|
|
3035
|
+
args.push("-e", key);
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
2789
3038
|
}
|
|
2790
|
-
|
|
2791
|
-
|
|
3039
|
+
args.push(imageName);
|
|
3040
|
+
args.push(...ctx.forwardArgs);
|
|
3041
|
+
return args;
|
|
3042
|
+
}
|
|
3043
|
+
function filterDockerArgs(argv) {
|
|
3044
|
+
const dockerFlags = /* @__PURE__ */ new Set(["--docker", "--docker-ro", "--no-docker", "--docker-dev"]);
|
|
3045
|
+
return argv.filter((arg) => !dockerFlags.has(arg));
|
|
3046
|
+
}
|
|
3047
|
+
function resolveDockerEnabled(config, options, profileDocker) {
|
|
3048
|
+
if (options.noDocker) {
|
|
3049
|
+
return false;
|
|
2792
3050
|
}
|
|
2793
|
-
if (
|
|
2794
|
-
|
|
3051
|
+
if (options.docker || options.dockerRo) {
|
|
3052
|
+
return true;
|
|
2795
3053
|
}
|
|
2796
|
-
if (
|
|
2797
|
-
|
|
3054
|
+
if (profileDocker !== void 0) {
|
|
3055
|
+
return profileDocker;
|
|
2798
3056
|
}
|
|
2799
|
-
|
|
2800
|
-
|
|
3057
|
+
return config?.enabled ?? false;
|
|
3058
|
+
}
|
|
3059
|
+
async function executeInDocker(ctx, devMode) {
|
|
3060
|
+
if (isInsideContainer()) {
|
|
3061
|
+
console.error(
|
|
3062
|
+
"Warning: Docker mode requested but already inside a container. Proceeding without re-containerization."
|
|
3063
|
+
);
|
|
3064
|
+
throw new DockerSkipError();
|
|
2801
3065
|
}
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
"builtin-interaction",
|
|
2806
|
-
section
|
|
2807
|
-
);
|
|
2808
|
-
}
|
|
2809
|
-
if ("gadget-start-prefix" in rawObj) {
|
|
2810
|
-
result["gadget-start-prefix"] = validateString(
|
|
2811
|
-
rawObj["gadget-start-prefix"],
|
|
2812
|
-
"gadget-start-prefix",
|
|
2813
|
-
section
|
|
2814
|
-
);
|
|
2815
|
-
}
|
|
2816
|
-
if ("gadget-end-prefix" in rawObj) {
|
|
2817
|
-
result["gadget-end-prefix"] = validateString(
|
|
2818
|
-
rawObj["gadget-end-prefix"],
|
|
2819
|
-
"gadget-end-prefix",
|
|
2820
|
-
section
|
|
2821
|
-
);
|
|
2822
|
-
}
|
|
2823
|
-
if ("gadget-arg-prefix" in rawObj) {
|
|
2824
|
-
result["gadget-arg-prefix"] = validateString(
|
|
2825
|
-
rawObj["gadget-arg-prefix"],
|
|
2826
|
-
"gadget-arg-prefix",
|
|
2827
|
-
section
|
|
2828
|
-
);
|
|
3066
|
+
const available = await checkDockerAvailable();
|
|
3067
|
+
if (!available) {
|
|
3068
|
+
throw new DockerUnavailableError();
|
|
2829
3069
|
}
|
|
2830
|
-
|
|
2831
|
-
|
|
3070
|
+
const dockerfile = resolveDockerfile(ctx.config, devMode.enabled);
|
|
3071
|
+
const imageName = devMode.enabled ? DEV_IMAGE_NAME : ctx.config["image-name"] ?? DEFAULT_IMAGE_NAME;
|
|
3072
|
+
if (devMode.enabled) {
|
|
3073
|
+
console.error(`[dev mode] Mounting source from ${devMode.sourcePath}`);
|
|
2832
3074
|
}
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
result.quiet = validateBoolean(rawObj.quiet, "quiet", section);
|
|
2841
|
-
}
|
|
2842
|
-
Object.assign(result, validateLoggingConfig(rawObj, section));
|
|
2843
|
-
return result;
|
|
2844
|
-
}
|
|
2845
|
-
function validatePromptsConfig(raw, section) {
|
|
2846
|
-
if (typeof raw !== "object" || raw === null) {
|
|
2847
|
-
throw new ConfigError(`[${section}] must be a table`);
|
|
2848
|
-
}
|
|
2849
|
-
const result = {};
|
|
2850
|
-
for (const [key, value] of Object.entries(raw)) {
|
|
2851
|
-
if (typeof value !== "string") {
|
|
2852
|
-
throw new ConfigError(`[${section}].${key} must be a string`);
|
|
3075
|
+
try {
|
|
3076
|
+
await ensureImage(imageName, dockerfile);
|
|
3077
|
+
} catch (error) {
|
|
3078
|
+
if (error instanceof DockerBuildError) {
|
|
3079
|
+
console.error("Docker build failed:");
|
|
3080
|
+
console.error(error.output);
|
|
3081
|
+
throw error;
|
|
2853
3082
|
}
|
|
2854
|
-
|
|
3083
|
+
throw error;
|
|
2855
3084
|
}
|
|
2856
|
-
|
|
3085
|
+
const dockerArgs = buildDockerRunArgs(ctx, imageName, devMode);
|
|
3086
|
+
const proc = Bun.spawn(["docker", ...dockerArgs], {
|
|
3087
|
+
stdin: "inherit",
|
|
3088
|
+
stdout: "inherit",
|
|
3089
|
+
stderr: "inherit"
|
|
3090
|
+
});
|
|
3091
|
+
const exitCode = await proc.exited;
|
|
3092
|
+
process.exit(exitCode);
|
|
2857
3093
|
}
|
|
2858
|
-
function
|
|
2859
|
-
|
|
2860
|
-
|
|
3094
|
+
function createDockerContext(config, options, argv, cwd, profileCwdPermission) {
|
|
3095
|
+
return {
|
|
3096
|
+
config: config ?? {},
|
|
3097
|
+
options,
|
|
3098
|
+
forwardArgs: filterDockerArgs(argv),
|
|
3099
|
+
cwd,
|
|
3100
|
+
profileCwdPermission
|
|
3101
|
+
};
|
|
3102
|
+
}
|
|
3103
|
+
|
|
3104
|
+
// src/cli/agent-command.ts
|
|
3105
|
+
function createHumanInputHandler(env, progress, keyboard) {
|
|
3106
|
+
const stdout = env.stdout;
|
|
3107
|
+
if (!isInteractive(env.stdin) || typeof stdout.isTTY !== "boolean" || !stdout.isTTY) {
|
|
3108
|
+
return void 0;
|
|
2861
3109
|
}
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
3110
|
+
return async (question) => {
|
|
3111
|
+
progress.pause();
|
|
3112
|
+
if (keyboard.cleanupEsc) {
|
|
3113
|
+
keyboard.cleanupEsc();
|
|
3114
|
+
keyboard.cleanupEsc = null;
|
|
3115
|
+
}
|
|
3116
|
+
const rl = createInterface2({ input: env.stdin, output: env.stdout });
|
|
2865
3117
|
try {
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
throw new ConfigError(error.message, configPath);
|
|
3118
|
+
const questionLine = question.trim() ? `
|
|
3119
|
+
${renderMarkdownWithSeparators(question.trim())}` : "";
|
|
3120
|
+
let isFirst = true;
|
|
3121
|
+
while (true) {
|
|
3122
|
+
const statsPrompt = progress.formatPrompt();
|
|
3123
|
+
const prompt = isFirst ? `${questionLine}
|
|
3124
|
+
${statsPrompt}` : statsPrompt;
|
|
3125
|
+
isFirst = false;
|
|
3126
|
+
const answer = await rl.question(prompt);
|
|
3127
|
+
const trimmed = answer.trim();
|
|
3128
|
+
if (trimmed) {
|
|
3129
|
+
return trimmed;
|
|
3130
|
+
}
|
|
2880
3131
|
}
|
|
2881
|
-
|
|
3132
|
+
} finally {
|
|
3133
|
+
rl.close();
|
|
3134
|
+
keyboard.restore();
|
|
2882
3135
|
}
|
|
2883
|
-
}
|
|
2884
|
-
return result;
|
|
3136
|
+
};
|
|
2885
3137
|
}
|
|
2886
|
-
function
|
|
2887
|
-
const
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
3138
|
+
async function executeAgent(promptArg, options, env) {
|
|
3139
|
+
const dockerOptions = {
|
|
3140
|
+
docker: options.docker ?? false,
|
|
3141
|
+
dockerRo: options.dockerRo ?? false,
|
|
3142
|
+
noDocker: options.noDocker ?? false,
|
|
3143
|
+
dockerDev: options.dockerDev ?? false
|
|
3144
|
+
};
|
|
3145
|
+
const dockerEnabled = resolveDockerEnabled(
|
|
3146
|
+
env.dockerConfig,
|
|
3147
|
+
dockerOptions,
|
|
3148
|
+
options.docker
|
|
3149
|
+
// Profile-level docker: true/false
|
|
3150
|
+
);
|
|
3151
|
+
if (dockerEnabled) {
|
|
3152
|
+
const devMode = resolveDevMode(env.dockerConfig, dockerOptions.dockerDev);
|
|
3153
|
+
const ctx = createDockerContext(
|
|
3154
|
+
env.dockerConfig,
|
|
3155
|
+
dockerOptions,
|
|
3156
|
+
env.argv.slice(2),
|
|
3157
|
+
// Remove 'node' and script path
|
|
3158
|
+
process.cwd(),
|
|
3159
|
+
options.dockerCwdPermission
|
|
3160
|
+
// Profile-level CWD permission override
|
|
2907
3161
|
);
|
|
3162
|
+
try {
|
|
3163
|
+
await executeInDocker(ctx, devMode);
|
|
3164
|
+
} catch (error) {
|
|
3165
|
+
if (error instanceof Error && error.message === "SKIP_DOCKER") {
|
|
3166
|
+
} else {
|
|
3167
|
+
throw error;
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
2908
3170
|
}
|
|
2909
|
-
const
|
|
2910
|
-
const
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
const hasPrompts = Object.keys(prompts).length > 0;
|
|
2920
|
-
let hasTemplates = false;
|
|
2921
|
-
for (const [sectionName, section] of Object.entries(config)) {
|
|
2922
|
-
if (sectionName === "global" || sectionName === "prompts") continue;
|
|
2923
|
-
if (!section || typeof section !== "object") continue;
|
|
2924
|
-
const sectionObj = section;
|
|
2925
|
-
if (typeof sectionObj.system === "string" && hasTemplateSyntax(sectionObj.system)) {
|
|
2926
|
-
hasTemplates = true;
|
|
2927
|
-
break;
|
|
3171
|
+
const prompt = await resolvePrompt(promptArg, env);
|
|
3172
|
+
const client = env.createClient();
|
|
3173
|
+
const registry = new GadgetRegistry();
|
|
3174
|
+
const stdinIsInteractive = isInteractive(env.stdin);
|
|
3175
|
+
if (options.builtins !== false) {
|
|
3176
|
+
for (const gadget of builtinGadgets) {
|
|
3177
|
+
if (gadget.name === "AskUser" && (options.builtinInteraction === false || !stdinIsInteractive)) {
|
|
3178
|
+
continue;
|
|
3179
|
+
}
|
|
3180
|
+
registry.registerByClass(gadget);
|
|
2928
3181
|
}
|
|
2929
3182
|
}
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
3183
|
+
const gadgetSpecifiers = options.gadget ?? [];
|
|
3184
|
+
if (gadgetSpecifiers.length > 0) {
|
|
3185
|
+
const gadgets2 = await loadGadgets(gadgetSpecifiers, process.cwd());
|
|
3186
|
+
for (const gadget of gadgets2) {
|
|
3187
|
+
registry.registerByClass(gadget);
|
|
2934
3188
|
}
|
|
2935
3189
|
}
|
|
2936
|
-
|
|
2937
|
-
|
|
3190
|
+
const printer = new StreamPrinter(env.stdout);
|
|
3191
|
+
const stderrTTY = env.stderr.isTTY === true;
|
|
3192
|
+
const progress = new StreamProgress(env.stderr, stderrTTY, client.modelRegistry);
|
|
3193
|
+
const abortController = new AbortController();
|
|
3194
|
+
let wasCancelled = false;
|
|
3195
|
+
let isStreaming = false;
|
|
3196
|
+
const stdinStream = env.stdin;
|
|
3197
|
+
const handleCancel = () => {
|
|
3198
|
+
if (!abortController.signal.aborted) {
|
|
3199
|
+
wasCancelled = true;
|
|
3200
|
+
abortController.abort();
|
|
3201
|
+
progress.pause();
|
|
3202
|
+
env.stderr.write(chalk5.yellow(`
|
|
3203
|
+
[Cancelled] ${progress.formatStats()}
|
|
3204
|
+
`));
|
|
3205
|
+
}
|
|
3206
|
+
};
|
|
3207
|
+
const keyboard = {
|
|
3208
|
+
cleanupEsc: null,
|
|
3209
|
+
cleanupSigint: null,
|
|
3210
|
+
restore: () => {
|
|
3211
|
+
if (stdinIsInteractive && stdinStream.isTTY && !wasCancelled) {
|
|
3212
|
+
keyboard.cleanupEsc = createEscKeyListener(stdinStream, handleCancel);
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
};
|
|
3216
|
+
const handleQuit = () => {
|
|
3217
|
+
keyboard.cleanupEsc?.();
|
|
3218
|
+
keyboard.cleanupSigint?.();
|
|
3219
|
+
progress.complete();
|
|
3220
|
+
printer.ensureNewline();
|
|
3221
|
+
const summary = renderOverallSummary({
|
|
3222
|
+
totalTokens: usage?.totalTokens,
|
|
3223
|
+
iterations,
|
|
3224
|
+
elapsedSeconds: progress.getTotalElapsedSeconds(),
|
|
3225
|
+
cost: progress.getTotalCost()
|
|
3226
|
+
});
|
|
3227
|
+
if (summary) {
|
|
3228
|
+
env.stderr.write(`${chalk5.dim("\u2500".repeat(40))}
|
|
3229
|
+
`);
|
|
3230
|
+
env.stderr.write(`${summary}
|
|
3231
|
+
`);
|
|
3232
|
+
}
|
|
3233
|
+
env.stderr.write(chalk5.dim("[Quit]\n"));
|
|
3234
|
+
process.exit(130);
|
|
3235
|
+
};
|
|
3236
|
+
if (stdinIsInteractive && stdinStream.isTTY) {
|
|
3237
|
+
keyboard.cleanupEsc = createEscKeyListener(stdinStream, handleCancel);
|
|
2938
3238
|
}
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
3239
|
+
keyboard.cleanupSigint = createSigintListener(
|
|
3240
|
+
handleCancel,
|
|
3241
|
+
handleQuit,
|
|
3242
|
+
() => isStreaming && !abortController.signal.aborted,
|
|
3243
|
+
env.stderr
|
|
3244
|
+
);
|
|
3245
|
+
const DEFAULT_APPROVAL_REQUIRED = ["RunCommand", "WriteFile", "EditFile"];
|
|
3246
|
+
const userApprovals = options.gadgetApproval ?? {};
|
|
3247
|
+
const gadgetApprovals = {
|
|
3248
|
+
...userApprovals
|
|
3249
|
+
};
|
|
3250
|
+
for (const gadget of DEFAULT_APPROVAL_REQUIRED) {
|
|
3251
|
+
const normalizedGadget = gadget.toLowerCase();
|
|
3252
|
+
const isConfigured = Object.keys(userApprovals).some(
|
|
3253
|
+
(key) => key.toLowerCase() === normalizedGadget
|
|
3254
|
+
);
|
|
3255
|
+
if (!isConfigured) {
|
|
3256
|
+
gadgetApprovals[gadget] = "approval-required";
|
|
2944
3257
|
}
|
|
2945
|
-
throw error;
|
|
2946
3258
|
}
|
|
2947
|
-
|
|
3259
|
+
const approvalConfig = {
|
|
3260
|
+
gadgetApprovals,
|
|
3261
|
+
defaultMode: "allowed"
|
|
3262
|
+
};
|
|
3263
|
+
const approvalManager = new ApprovalManager(approvalConfig, env, progress);
|
|
3264
|
+
let usage;
|
|
3265
|
+
let iterations = 0;
|
|
3266
|
+
const llmRequestsDir = resolveLogDir(options.logLlmRequests, "requests");
|
|
3267
|
+
const llmResponsesDir = resolveLogDir(options.logLlmResponses, "responses");
|
|
3268
|
+
let llmCallCounter = 0;
|
|
3269
|
+
const countMessagesTokens = async (model, messages) => {
|
|
2948
3270
|
try {
|
|
2949
|
-
|
|
2950
|
-
} catch
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
}
|
|
2954
|
-
throw error;
|
|
3271
|
+
return await client.countTokens(model, messages);
|
|
3272
|
+
} catch {
|
|
3273
|
+
const totalChars = messages.reduce((sum, m) => sum + (m.content?.length ?? 0), 0);
|
|
3274
|
+
return Math.round(totalChars / FALLBACK_CHARS_PER_TOKEN);
|
|
2955
3275
|
}
|
|
2956
|
-
}
|
|
2957
|
-
const
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
3276
|
+
};
|
|
3277
|
+
const countGadgetOutputTokens = async (output) => {
|
|
3278
|
+
if (!output) return void 0;
|
|
3279
|
+
try {
|
|
3280
|
+
const messages = [{ role: "assistant", content: output }];
|
|
3281
|
+
return await client.countTokens(options.model, messages);
|
|
3282
|
+
} catch {
|
|
3283
|
+
return void 0;
|
|
3284
|
+
}
|
|
3285
|
+
};
|
|
3286
|
+
const builder = new AgentBuilder(client).withModel(options.model).withLogger(env.createLogger("llmist:cli:agent")).withHooks({
|
|
3287
|
+
observers: {
|
|
3288
|
+
// onLLMCallStart: Start progress indicator for each LLM call
|
|
3289
|
+
// This showcases how to react to agent lifecycle events
|
|
3290
|
+
onLLMCallStart: async (context) => {
|
|
3291
|
+
isStreaming = true;
|
|
3292
|
+
llmCallCounter++;
|
|
3293
|
+
const inputTokens = await countMessagesTokens(
|
|
3294
|
+
context.options.model,
|
|
3295
|
+
context.options.messages
|
|
3296
|
+
);
|
|
3297
|
+
progress.startCall(context.options.model, inputTokens);
|
|
3298
|
+
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);
|
|
3303
|
+
}
|
|
3304
|
+
},
|
|
3305
|
+
// onStreamChunk: Real-time updates as LLM generates tokens
|
|
3306
|
+
// This enables responsive UIs that show progress during generation
|
|
3307
|
+
onStreamChunk: async (context) => {
|
|
3308
|
+
progress.update(context.accumulatedText.length);
|
|
3309
|
+
if (context.usage) {
|
|
3310
|
+
if (context.usage.inputTokens) {
|
|
3311
|
+
progress.setInputTokens(context.usage.inputTokens, false);
|
|
3312
|
+
}
|
|
3313
|
+
if (context.usage.outputTokens) {
|
|
3314
|
+
progress.setOutputTokens(context.usage.outputTokens, false);
|
|
3315
|
+
}
|
|
3316
|
+
progress.setCachedTokens(
|
|
3317
|
+
context.usage.cachedInputTokens ?? 0,
|
|
3318
|
+
context.usage.cacheCreationInputTokens ?? 0
|
|
3319
|
+
);
|
|
3320
|
+
}
|
|
3321
|
+
},
|
|
3322
|
+
// onLLMCallComplete: Finalize metrics after each LLM call
|
|
3323
|
+
// This is where you'd typically log metrics or update dashboards
|
|
3324
|
+
onLLMCallComplete: async (context) => {
|
|
3325
|
+
isStreaming = false;
|
|
3326
|
+
usage = context.usage;
|
|
3327
|
+
iterations = Math.max(iterations, context.iteration + 1);
|
|
3328
|
+
if (context.usage) {
|
|
3329
|
+
if (context.usage.inputTokens) {
|
|
3330
|
+
progress.setInputTokens(context.usage.inputTokens, false);
|
|
3331
|
+
}
|
|
3332
|
+
if (context.usage.outputTokens) {
|
|
3333
|
+
progress.setOutputTokens(context.usage.outputTokens, false);
|
|
3334
|
+
}
|
|
3335
|
+
}
|
|
3336
|
+
let callCost;
|
|
3337
|
+
if (context.usage && client.modelRegistry) {
|
|
3338
|
+
try {
|
|
3339
|
+
const modelName = context.options.model.includes(":") ? context.options.model.split(":")[1] : context.options.model;
|
|
3340
|
+
const costResult = client.modelRegistry.estimateCost(
|
|
3341
|
+
modelName,
|
|
3342
|
+
context.usage.inputTokens,
|
|
3343
|
+
context.usage.outputTokens,
|
|
3344
|
+
context.usage.cachedInputTokens ?? 0,
|
|
3345
|
+
context.usage.cacheCreationInputTokens ?? 0
|
|
3346
|
+
);
|
|
3347
|
+
if (costResult) callCost = costResult.totalCost;
|
|
3348
|
+
} catch {
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
const callElapsed = progress.getCallElapsedSeconds();
|
|
3352
|
+
progress.endCall(context.usage);
|
|
3353
|
+
if (!options.quiet) {
|
|
3354
|
+
const summary = renderSummary({
|
|
3355
|
+
iterations: context.iteration + 1,
|
|
3356
|
+
model: options.model,
|
|
3357
|
+
usage: context.usage,
|
|
3358
|
+
elapsedSeconds: callElapsed,
|
|
3359
|
+
cost: callCost,
|
|
3360
|
+
finishReason: context.finishReason
|
|
3361
|
+
});
|
|
3362
|
+
if (summary) {
|
|
3363
|
+
env.stderr.write(`${summary}
|
|
3364
|
+
`);
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
if (llmResponsesDir) {
|
|
3368
|
+
const filename = `${Date.now()}_call_${llmCallCounter}.response.txt`;
|
|
3369
|
+
await writeLogFile(llmResponsesDir, filename, context.rawResponse);
|
|
2969
3370
|
}
|
|
2970
|
-
throw error;
|
|
2971
3371
|
}
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
3372
|
+
},
|
|
3373
|
+
// SHOWCASE: Controller-based approval gating for gadgets
|
|
3374
|
+
//
|
|
3375
|
+
// This demonstrates how to add safety layers WITHOUT modifying gadgets.
|
|
3376
|
+
// The ApprovalManager handles approval flows externally via beforeGadgetExecution.
|
|
3377
|
+
// Approval modes are configurable via cli.toml:
|
|
3378
|
+
// - "allowed": auto-proceed
|
|
3379
|
+
// - "denied": auto-reject, return message to LLM
|
|
3380
|
+
// - "approval-required": prompt user interactively
|
|
3381
|
+
//
|
|
3382
|
+
// Default: RunCommand, WriteFile, EditFile require approval unless overridden.
|
|
3383
|
+
controllers: {
|
|
3384
|
+
beforeGadgetExecution: async (ctx) => {
|
|
3385
|
+
const mode = approvalManager.getApprovalMode(ctx.gadgetName);
|
|
3386
|
+
if (mode === "allowed") {
|
|
3387
|
+
return { action: "proceed" };
|
|
3388
|
+
}
|
|
3389
|
+
const stdinTTY = isInteractive(env.stdin);
|
|
3390
|
+
const stderrTTY2 = env.stderr.isTTY === true;
|
|
3391
|
+
const canPrompt = stdinTTY && stderrTTY2;
|
|
3392
|
+
if (!canPrompt) {
|
|
3393
|
+
if (mode === "approval-required") {
|
|
3394
|
+
return {
|
|
3395
|
+
action: "skip",
|
|
3396
|
+
syntheticResult: `status=denied
|
|
3397
|
+
|
|
3398
|
+
${ctx.gadgetName} requires interactive approval. Run in a terminal to approve.`
|
|
3399
|
+
};
|
|
3400
|
+
}
|
|
3401
|
+
if (mode === "denied") {
|
|
3402
|
+
return {
|
|
3403
|
+
action: "skip",
|
|
3404
|
+
syntheticResult: `status=denied
|
|
3405
|
+
|
|
3406
|
+
${ctx.gadgetName} is denied by configuration.`
|
|
3407
|
+
};
|
|
3408
|
+
}
|
|
3409
|
+
return { action: "proceed" };
|
|
3410
|
+
}
|
|
3411
|
+
const result = await approvalManager.requestApproval(ctx.gadgetName, ctx.parameters);
|
|
3412
|
+
if (!result.approved) {
|
|
3413
|
+
return {
|
|
3414
|
+
action: "skip",
|
|
3415
|
+
syntheticResult: `status=denied
|
|
3416
|
+
|
|
3417
|
+
Denied: ${result.reason ?? "by user"}`
|
|
3418
|
+
};
|
|
2981
3419
|
}
|
|
2982
|
-
|
|
3420
|
+
return { action: "proceed" };
|
|
2983
3421
|
}
|
|
2984
3422
|
}
|
|
3423
|
+
});
|
|
3424
|
+
if (options.system) {
|
|
3425
|
+
builder.withSystem(options.system);
|
|
2985
3426
|
}
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
function resolveGadgets(section, inheritedGadgets, sectionName, configPath) {
|
|
2989
|
-
const hasGadgets = "gadgets" in section;
|
|
2990
|
-
const hasGadgetLegacy = "gadget" in section;
|
|
2991
|
-
const hasGadgetAdd = "gadget-add" in section;
|
|
2992
|
-
const hasGadgetRemove = "gadget-remove" in section;
|
|
2993
|
-
if (hasGadgetLegacy && !hasGadgets) {
|
|
2994
|
-
console.warn(
|
|
2995
|
-
`[config] Warning: [${sectionName}].gadget is deprecated, use 'gadgets' (plural) instead`
|
|
2996
|
-
);
|
|
3427
|
+
if (options.maxIterations !== void 0) {
|
|
3428
|
+
builder.withMaxIterations(options.maxIterations);
|
|
2997
3429
|
}
|
|
2998
|
-
if (
|
|
2999
|
-
|
|
3000
|
-
`[${sectionName}] Cannot use 'gadgets' with 'gadget-add'/'gadget-remove'. Use either full replacement (gadgets) OR modification (gadget-add/gadget-remove).`,
|
|
3001
|
-
configPath
|
|
3002
|
-
);
|
|
3430
|
+
if (options.temperature !== void 0) {
|
|
3431
|
+
builder.withTemperature(options.temperature);
|
|
3003
3432
|
}
|
|
3004
|
-
|
|
3005
|
-
|
|
3433
|
+
const humanInputHandler = createHumanInputHandler(env, progress, keyboard);
|
|
3434
|
+
if (humanInputHandler) {
|
|
3435
|
+
builder.onHumanInput(humanInputHandler);
|
|
3006
3436
|
}
|
|
3007
|
-
|
|
3008
|
-
|
|
3437
|
+
builder.withSignal(abortController.signal);
|
|
3438
|
+
const gadgets = registry.getAll();
|
|
3439
|
+
if (gadgets.length > 0) {
|
|
3440
|
+
builder.withGadgets(...gadgets);
|
|
3009
3441
|
}
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
const toRemove = new Set(section["gadget-remove"]);
|
|
3013
|
-
result = result.filter((g) => !toRemove.has(g));
|
|
3442
|
+
if (options.gadgetStartPrefix) {
|
|
3443
|
+
builder.withGadgetStartPrefix(options.gadgetStartPrefix);
|
|
3014
3444
|
}
|
|
3015
|
-
if (
|
|
3016
|
-
|
|
3017
|
-
result.push(...toAdd);
|
|
3445
|
+
if (options.gadgetEndPrefix) {
|
|
3446
|
+
builder.withGadgetEndPrefix(options.gadgetEndPrefix);
|
|
3018
3447
|
}
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3448
|
+
if (options.gadgetArgPrefix) {
|
|
3449
|
+
builder.withGadgetArgPrefix(options.gadgetArgPrefix);
|
|
3450
|
+
}
|
|
3451
|
+
builder.withSyntheticGadgetCall(
|
|
3452
|
+
"TellUser",
|
|
3453
|
+
{
|
|
3454
|
+
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?",
|
|
3455
|
+
done: false,
|
|
3456
|
+
type: "info"
|
|
3457
|
+
},
|
|
3458
|
+
"\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?"
|
|
3459
|
+
);
|
|
3460
|
+
builder.withTextOnlyHandler("acknowledge");
|
|
3461
|
+
builder.withTextWithGadgetsHandler({
|
|
3462
|
+
gadgetName: "TellUser",
|
|
3463
|
+
parameterMapping: (text) => ({ message: text, done: false, type: "info" }),
|
|
3464
|
+
resultMapping: (text) => `\u2139\uFE0F ${text}`
|
|
3465
|
+
});
|
|
3466
|
+
const agent = builder.ask(prompt);
|
|
3467
|
+
let textBuffer = "";
|
|
3468
|
+
const flushTextBuffer = () => {
|
|
3469
|
+
if (textBuffer) {
|
|
3470
|
+
const output = options.quiet ? textBuffer : renderMarkdownWithSeparators(textBuffer);
|
|
3471
|
+
printer.write(output);
|
|
3472
|
+
textBuffer = "";
|
|
3027
3473
|
}
|
|
3028
|
-
|
|
3029
|
-
|
|
3474
|
+
};
|
|
3475
|
+
try {
|
|
3476
|
+
for await (const event of agent.run()) {
|
|
3477
|
+
if (event.type === "text") {
|
|
3478
|
+
progress.pause();
|
|
3479
|
+
textBuffer += event.content;
|
|
3480
|
+
} else if (event.type === "gadget_result") {
|
|
3481
|
+
flushTextBuffer();
|
|
3482
|
+
progress.pause();
|
|
3483
|
+
if (options.quiet) {
|
|
3484
|
+
if (event.result.gadgetName === "TellUser" && event.result.parameters?.message) {
|
|
3485
|
+
const message = String(event.result.parameters.message);
|
|
3486
|
+
env.stdout.write(`${message}
|
|
3487
|
+
`);
|
|
3488
|
+
}
|
|
3489
|
+
} else {
|
|
3490
|
+
const tokenCount = await countGadgetOutputTokens(event.result.result);
|
|
3491
|
+
env.stderr.write(`${formatGadgetSummary({ ...event.result, tokenCount })}
|
|
3492
|
+
`);
|
|
3493
|
+
}
|
|
3494
|
+
}
|
|
3030
3495
|
}
|
|
3031
|
-
|
|
3032
|
-
if (
|
|
3033
|
-
throw
|
|
3496
|
+
} catch (error) {
|
|
3497
|
+
if (!isAbortError(error)) {
|
|
3498
|
+
throw error;
|
|
3034
3499
|
}
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3500
|
+
} finally {
|
|
3501
|
+
isStreaming = false;
|
|
3502
|
+
keyboard.cleanupEsc?.();
|
|
3503
|
+
keyboard.cleanupSigint?.();
|
|
3504
|
+
}
|
|
3505
|
+
flushTextBuffer();
|
|
3506
|
+
progress.complete();
|
|
3507
|
+
printer.ensureNewline();
|
|
3508
|
+
if (!options.quiet && iterations > 1) {
|
|
3509
|
+
env.stderr.write(`${chalk5.dim("\u2500".repeat(40))}
|
|
3510
|
+
`);
|
|
3511
|
+
const summary = renderOverallSummary({
|
|
3512
|
+
totalTokens: usage?.totalTokens,
|
|
3513
|
+
iterations,
|
|
3514
|
+
elapsedSeconds: progress.getTotalElapsedSeconds(),
|
|
3515
|
+
cost: progress.getTotalCost()
|
|
3516
|
+
});
|
|
3517
|
+
if (summary) {
|
|
3518
|
+
env.stderr.write(`${summary}
|
|
3519
|
+
`);
|
|
3043
3520
|
}
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3521
|
+
}
|
|
3522
|
+
}
|
|
3523
|
+
function registerAgentCommand(program, env, config) {
|
|
3524
|
+
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.");
|
|
3525
|
+
addAgentOptions(cmd, config);
|
|
3526
|
+
cmd.action(
|
|
3527
|
+
(prompt, options) => executeAction(() => {
|
|
3528
|
+
const mergedOptions = {
|
|
3529
|
+
...options,
|
|
3530
|
+
gadgetApproval: config?.["gadget-approval"]
|
|
3531
|
+
};
|
|
3532
|
+
return executeAgent(prompt, mergedOptions, env);
|
|
3533
|
+
}, env)
|
|
3534
|
+
);
|
|
3535
|
+
}
|
|
3536
|
+
|
|
3537
|
+
// src/cli/complete-command.ts
|
|
3538
|
+
init_messages();
|
|
3539
|
+
init_model_shortcuts();
|
|
3540
|
+
init_constants();
|
|
3541
|
+
async function executeComplete(promptArg, options, env) {
|
|
3542
|
+
const prompt = await resolvePrompt(promptArg, env);
|
|
3543
|
+
const client = env.createClient();
|
|
3544
|
+
const model = resolveModel(options.model);
|
|
3545
|
+
const builder = new LLMMessageBuilder();
|
|
3546
|
+
if (options.system) {
|
|
3547
|
+
builder.addSystem(options.system);
|
|
3548
|
+
}
|
|
3549
|
+
builder.addUser(prompt);
|
|
3550
|
+
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);
|
|
3558
|
+
}
|
|
3559
|
+
const stream = client.stream({
|
|
3560
|
+
model,
|
|
3561
|
+
messages,
|
|
3562
|
+
temperature: options.temperature,
|
|
3563
|
+
maxTokens: options.maxTokens
|
|
3564
|
+
});
|
|
3565
|
+
const printer = new StreamPrinter(env.stdout);
|
|
3566
|
+
const stderrTTY = env.stderr.isTTY === true;
|
|
3567
|
+
const progress = new StreamProgress(env.stderr, stderrTTY, client.modelRegistry);
|
|
3568
|
+
const estimatedInputTokens = Math.round(prompt.length / FALLBACK_CHARS_PER_TOKEN);
|
|
3569
|
+
progress.startCall(model, estimatedInputTokens);
|
|
3570
|
+
let finishReason;
|
|
3571
|
+
let usage;
|
|
3572
|
+
let accumulatedResponse = "";
|
|
3573
|
+
for await (const chunk of stream) {
|
|
3574
|
+
if (chunk.usage) {
|
|
3575
|
+
usage = chunk.usage;
|
|
3576
|
+
if (chunk.usage.inputTokens) {
|
|
3577
|
+
progress.setInputTokens(chunk.usage.inputTokens, false);
|
|
3578
|
+
}
|
|
3579
|
+
if (chunk.usage.outputTokens) {
|
|
3580
|
+
progress.setOutputTokens(chunk.usage.outputTokens, false);
|
|
3581
|
+
}
|
|
3582
|
+
}
|
|
3583
|
+
if (chunk.text) {
|
|
3584
|
+
progress.pause();
|
|
3585
|
+
accumulatedResponse += chunk.text;
|
|
3586
|
+
progress.update(accumulatedResponse.length);
|
|
3587
|
+
printer.write(chunk.text);
|
|
3588
|
+
}
|
|
3589
|
+
if (chunk.finishReason !== void 0) {
|
|
3590
|
+
finishReason = chunk.finishReason;
|
|
3057
3591
|
}
|
|
3058
|
-
delete merged["gadget"];
|
|
3059
|
-
delete merged["gadget-add"];
|
|
3060
|
-
delete merged["gadget-remove"];
|
|
3061
|
-
resolving.delete(name);
|
|
3062
|
-
resolved[name] = merged;
|
|
3063
|
-
return merged;
|
|
3064
3592
|
}
|
|
3065
|
-
|
|
3066
|
-
|
|
3593
|
+
progress.endCall(usage);
|
|
3594
|
+
progress.complete();
|
|
3595
|
+
printer.ensureNewline();
|
|
3596
|
+
if (llmResponsesDir) {
|
|
3597
|
+
const filename = `${timestamp}_complete.response.txt`;
|
|
3598
|
+
await writeLogFile(llmResponsesDir, filename, accumulatedResponse);
|
|
3067
3599
|
}
|
|
3068
|
-
|
|
3600
|
+
if (stderrTTY && !options.quiet) {
|
|
3601
|
+
const summary = renderSummary({ finishReason, usage, cost: progress.getTotalCost() });
|
|
3602
|
+
if (summary) {
|
|
3603
|
+
env.stderr.write(`${summary}
|
|
3604
|
+
`);
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3607
|
+
}
|
|
3608
|
+
function registerCompleteCommand(program, env, config) {
|
|
3609
|
+
const cmd = program.command(COMMANDS.complete).description("Stream a single completion from a specified model.").argument("[prompt]", "Prompt to send to the LLM. If omitted, stdin is used when available.");
|
|
3610
|
+
addCompleteOptions(cmd, config);
|
|
3611
|
+
cmd.action(
|
|
3612
|
+
(prompt, options) => executeAction(() => executeComplete(prompt, options, env), env)
|
|
3613
|
+
);
|
|
3069
3614
|
}
|
|
3070
3615
|
|
|
3071
3616
|
// src/cli/gadget-command.ts
|
|
@@ -3755,7 +4300,11 @@ function createCommandEnvironment(baseEnv, config) {
|
|
|
3755
4300
|
logFile: config["log-file"] ?? baseEnv.loggerConfig?.logFile,
|
|
3756
4301
|
logReset: config["log-reset"] ?? baseEnv.loggerConfig?.logReset
|
|
3757
4302
|
};
|
|
3758
|
-
return
|
|
4303
|
+
return {
|
|
4304
|
+
...baseEnv,
|
|
4305
|
+
loggerConfig,
|
|
4306
|
+
createLogger: createLoggerFactory(loggerConfig)
|
|
4307
|
+
};
|
|
3759
4308
|
}
|
|
3760
4309
|
function registerCustomCommand(program, name, config, env) {
|
|
3761
4310
|
const type = config.type ?? "agent";
|
|
@@ -3831,7 +4380,12 @@ async function runCLI(overrides = {}) {
|
|
|
3831
4380
|
logReset: globalOpts.logReset ?? config.global?.["log-reset"]
|
|
3832
4381
|
};
|
|
3833
4382
|
const defaultEnv = createDefaultEnvironment(loggerConfig);
|
|
3834
|
-
const env = {
|
|
4383
|
+
const env = {
|
|
4384
|
+
...defaultEnv,
|
|
4385
|
+
...envOverrides,
|
|
4386
|
+
// Pass Docker config from [docker] section
|
|
4387
|
+
dockerConfig: config.docker
|
|
4388
|
+
};
|
|
3835
4389
|
const program = createProgram(env, config);
|
|
3836
4390
|
await program.parseAsync(env.argv);
|
|
3837
4391
|
}
|