llmist 1.4.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/{chunk-UEEESLOA.js → chunk-QR5IQXEM.js} +2 -2
- package/dist/{chunk-VGZCFUPX.js → chunk-X5XQ6M5P.js} +84 -31
- package/dist/chunk-X5XQ6M5P.js.map +1 -0
- package/dist/cli.cjs +1667 -1060
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1581 -1027
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +83 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -3
- package/dist/index.d.ts +7 -3
- package/dist/index.js +2 -2
- package/dist/{mock-stream-DD5yJM44.d.cts → mock-stream-Cc47j12U.d.cts} +6 -2
- package/dist/{mock-stream-DD5yJM44.d.ts → mock-stream-Cc47j12U.d.ts} +6 -2
- package/dist/testing/index.cjs +83 -30
- package/dist/testing/index.cjs.map +1 -1
- package/dist/testing/index.d.cts +2 -2
- package/dist/testing/index.d.ts +2 -2
- package/dist/testing/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-VGZCFUPX.js.map +0 -1
- /package/dist/{chunk-UEEESLOA.js.map → chunk-QR5IQXEM.js.map} +0 -0
package/dist/cli.cjs
CHANGED
|
@@ -830,38 +830,83 @@ function formatParamsAsBlock(params, prefix = "", argPrefix = GADGET_ARG_PREFIX)
|
|
|
830
830
|
}
|
|
831
831
|
return lines.join("\n");
|
|
832
832
|
}
|
|
833
|
-
function
|
|
833
|
+
function formatParamLine(key, propObj, isRequired, indent = "") {
|
|
834
|
+
const type = propObj.type;
|
|
835
|
+
const description = propObj.description;
|
|
836
|
+
const enumValues = propObj.enum;
|
|
837
|
+
let line = `${indent}- ${key}`;
|
|
838
|
+
if (type === "array") {
|
|
839
|
+
const items = propObj.items;
|
|
840
|
+
const itemType = items?.type || "any";
|
|
841
|
+
line += ` (array of ${itemType})`;
|
|
842
|
+
} else if (type === "object" && propObj.properties) {
|
|
843
|
+
line += " (object)";
|
|
844
|
+
} else {
|
|
845
|
+
line += ` (${type})`;
|
|
846
|
+
}
|
|
847
|
+
if (isRequired && indent !== "") {
|
|
848
|
+
line += " [required]";
|
|
849
|
+
}
|
|
850
|
+
if (description) {
|
|
851
|
+
line += `: ${description}`;
|
|
852
|
+
}
|
|
853
|
+
if (enumValues) {
|
|
854
|
+
line += ` - one of: ${enumValues.map((v) => `"${v}"`).join(", ")}`;
|
|
855
|
+
}
|
|
856
|
+
return line;
|
|
857
|
+
}
|
|
858
|
+
function formatSchemaAsPlainText(schema, indent = "", atRoot = true) {
|
|
834
859
|
const lines = [];
|
|
835
860
|
const properties = schema.properties || {};
|
|
836
861
|
const required = schema.required || [];
|
|
837
|
-
|
|
838
|
-
const
|
|
839
|
-
const
|
|
840
|
-
const
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
const itemType = items?.type || "any";
|
|
847
|
-
line += ` (array of ${itemType})`;
|
|
848
|
-
} else if (type === "object" && propObj.properties) {
|
|
849
|
-
line += " (object)";
|
|
850
|
-
} else {
|
|
851
|
-
line += ` (${type})`;
|
|
862
|
+
if (atRoot && indent === "") {
|
|
863
|
+
const requiredProps = [];
|
|
864
|
+
const optionalProps = [];
|
|
865
|
+
for (const [key, prop] of Object.entries(properties)) {
|
|
866
|
+
if (required.includes(key)) {
|
|
867
|
+
requiredProps.push([key, prop]);
|
|
868
|
+
} else {
|
|
869
|
+
optionalProps.push([key, prop]);
|
|
870
|
+
}
|
|
852
871
|
}
|
|
853
|
-
|
|
854
|
-
|
|
872
|
+
const reqCount = requiredProps.length;
|
|
873
|
+
const optCount = optionalProps.length;
|
|
874
|
+
if (reqCount > 0 || optCount > 0) {
|
|
875
|
+
const parts = [];
|
|
876
|
+
if (reqCount > 0) parts.push(`${reqCount} required`);
|
|
877
|
+
if (optCount > 0) parts.push(`${optCount} optional`);
|
|
878
|
+
lines.push(parts.join(", "));
|
|
879
|
+
lines.push("");
|
|
855
880
|
}
|
|
856
|
-
if (
|
|
857
|
-
|
|
881
|
+
if (reqCount > 0) {
|
|
882
|
+
lines.push("REQUIRED Parameters:");
|
|
883
|
+
for (const [key, prop] of requiredProps) {
|
|
884
|
+
lines.push(formatParamLine(key, prop, true, ""));
|
|
885
|
+
const propObj = prop;
|
|
886
|
+
if (propObj.type === "object" && propObj.properties) {
|
|
887
|
+
lines.push(formatSchemaAsPlainText(propObj, " ", false));
|
|
888
|
+
}
|
|
889
|
+
}
|
|
858
890
|
}
|
|
859
|
-
if (
|
|
860
|
-
|
|
891
|
+
if (optCount > 0) {
|
|
892
|
+
if (reqCount > 0) lines.push("");
|
|
893
|
+
lines.push("OPTIONAL Parameters:");
|
|
894
|
+
for (const [key, prop] of optionalProps) {
|
|
895
|
+
lines.push(formatParamLine(key, prop, false, ""));
|
|
896
|
+
const propObj = prop;
|
|
897
|
+
if (propObj.type === "object" && propObj.properties) {
|
|
898
|
+
lines.push(formatSchemaAsPlainText(propObj, " ", false));
|
|
899
|
+
}
|
|
900
|
+
}
|
|
861
901
|
}
|
|
862
|
-
lines.
|
|
863
|
-
|
|
864
|
-
|
|
902
|
+
return lines.join("\n");
|
|
903
|
+
}
|
|
904
|
+
for (const [key, prop] of Object.entries(properties)) {
|
|
905
|
+
const isRequired = required.includes(key);
|
|
906
|
+
lines.push(formatParamLine(key, prop, isRequired, indent));
|
|
907
|
+
const propObj = prop;
|
|
908
|
+
if (propObj.type === "object" && propObj.properties) {
|
|
909
|
+
lines.push(formatSchemaAsPlainText(propObj, indent + " ", false));
|
|
865
910
|
}
|
|
866
911
|
}
|
|
867
912
|
return lines.join("\n");
|
|
@@ -912,10 +957,11 @@ var init_gadget = __esm({
|
|
|
912
957
|
* Generate instruction text for the LLM.
|
|
913
958
|
* Combines name, description, and parameter schema into a formatted instruction.
|
|
914
959
|
*
|
|
915
|
-
* @param
|
|
960
|
+
* @param optionsOrArgPrefix - Optional custom prefixes for examples, or just argPrefix string for backwards compatibility
|
|
916
961
|
* @returns Formatted instruction string
|
|
917
962
|
*/
|
|
918
|
-
getInstruction(
|
|
963
|
+
getInstruction(optionsOrArgPrefix) {
|
|
964
|
+
const options = typeof optionsOrArgPrefix === "string" ? { argPrefix: optionsOrArgPrefix } : optionsOrArgPrefix;
|
|
919
965
|
const parts = [];
|
|
920
966
|
parts.push(this.description);
|
|
921
967
|
if (this.parameterSchema) {
|
|
@@ -929,18 +975,25 @@ var init_gadget = __esm({
|
|
|
929
975
|
}
|
|
930
976
|
if (this.examples && this.examples.length > 0) {
|
|
931
977
|
parts.push("\n\nExamples:");
|
|
932
|
-
const effectiveArgPrefix = argPrefix ?? GADGET_ARG_PREFIX;
|
|
978
|
+
const effectiveArgPrefix = options?.argPrefix ?? GADGET_ARG_PREFIX;
|
|
979
|
+
const effectiveStartPrefix = options?.startPrefix ?? GADGET_START_PREFIX;
|
|
980
|
+
const effectiveEndPrefix = options?.endPrefix ?? GADGET_END_PREFIX;
|
|
981
|
+
const gadgetName = this.name || this.constructor.name;
|
|
933
982
|
this.examples.forEach((example, index) => {
|
|
934
983
|
if (index > 0) {
|
|
935
984
|
parts.push("");
|
|
985
|
+
parts.push("---");
|
|
986
|
+
parts.push("");
|
|
936
987
|
}
|
|
937
988
|
if (example.comment) {
|
|
938
989
|
parts.push(`# ${example.comment}`);
|
|
939
990
|
}
|
|
940
|
-
parts.push(
|
|
991
|
+
parts.push(`${effectiveStartPrefix}${gadgetName}`);
|
|
941
992
|
parts.push(formatParamsAsBlock(example.params, "", effectiveArgPrefix));
|
|
993
|
+
parts.push(effectiveEndPrefix);
|
|
942
994
|
if (example.output !== void 0) {
|
|
943
|
-
parts.push("
|
|
995
|
+
parts.push("");
|
|
996
|
+
parts.push("Expected Output:");
|
|
944
997
|
parts.push(example.output);
|
|
945
998
|
}
|
|
946
999
|
});
|
|
@@ -6500,7 +6553,11 @@ var OPTION_FLAGS = {
|
|
|
6500
6553
|
logLlmResponses: "--log-llm-responses [dir]",
|
|
6501
6554
|
noBuiltins: "--no-builtins",
|
|
6502
6555
|
noBuiltinInteraction: "--no-builtin-interaction",
|
|
6503
|
-
quiet: "-q, --quiet"
|
|
6556
|
+
quiet: "-q, --quiet",
|
|
6557
|
+
docker: "--docker",
|
|
6558
|
+
dockerRo: "--docker-ro",
|
|
6559
|
+
noDocker: "--no-docker",
|
|
6560
|
+
dockerDev: "--docker-dev"
|
|
6504
6561
|
};
|
|
6505
6562
|
var OPTION_DESCRIPTIONS = {
|
|
6506
6563
|
model: "Model identifier, e.g. openai:gpt-5-nano or anthropic:claude-sonnet-4-5.",
|
|
@@ -6516,7 +6573,11 @@ var OPTION_DESCRIPTIONS = {
|
|
|
6516
6573
|
logLlmResponses: "Save raw LLM responses as plain text. Optional dir, defaults to ~/.llmist/logs/responses/",
|
|
6517
6574
|
noBuiltins: "Disable built-in gadgets (AskUser, TellUser).",
|
|
6518
6575
|
noBuiltinInteraction: "Disable interactive gadgets (AskUser) while keeping TellUser.",
|
|
6519
|
-
quiet: "Suppress all output except content (text and TellUser messages)."
|
|
6576
|
+
quiet: "Suppress all output except content (text and TellUser messages).",
|
|
6577
|
+
docker: "Run agent in a Docker sandbox container for security isolation.",
|
|
6578
|
+
dockerRo: "Run in Docker with current directory mounted read-only.",
|
|
6579
|
+
noDocker: "Disable Docker sandboxing (override config).",
|
|
6580
|
+
dockerDev: "Run in Docker dev mode (mount local source instead of npm install)."
|
|
6520
6581
|
};
|
|
6521
6582
|
var SUMMARY_PREFIX = "[llmist]";
|
|
6522
6583
|
|
|
@@ -6526,7 +6587,7 @@ var import_commander2 = require("commander");
|
|
|
6526
6587
|
// package.json
|
|
6527
6588
|
var package_default = {
|
|
6528
6589
|
name: "llmist",
|
|
6529
|
-
version: "1.
|
|
6590
|
+
version: "1.5.0",
|
|
6530
6591
|
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.",
|
|
6531
6592
|
type: "module",
|
|
6532
6593
|
main: "dist/index.cjs",
|
|
@@ -8336,7 +8397,7 @@ function addAgentOptions(cmd, defaults) {
|
|
|
8336
8397
|
OPTION_FLAGS.noBuiltinInteraction,
|
|
8337
8398
|
OPTION_DESCRIPTIONS.noBuiltinInteraction,
|
|
8338
8399
|
defaults?.["builtin-interaction"] !== false
|
|
8339
|
-
).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"]);
|
|
8400
|
+
).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);
|
|
8340
8401
|
}
|
|
8341
8402
|
function configToCompleteOptions(config) {
|
|
8342
8403
|
const result = {};
|
|
@@ -8371,668 +8432,791 @@ function configToAgentOptions(config) {
|
|
|
8371
8432
|
if (config.quiet !== void 0) result.quiet = config.quiet;
|
|
8372
8433
|
if (config["log-llm-requests"] !== void 0) result.logLlmRequests = config["log-llm-requests"];
|
|
8373
8434
|
if (config["log-llm-responses"] !== void 0) result.logLlmResponses = config["log-llm-responses"];
|
|
8435
|
+
if (config.docker !== void 0) result.docker = config.docker;
|
|
8436
|
+
if (config["docker-cwd-permission"] !== void 0)
|
|
8437
|
+
result.dockerCwdPermission = config["docker-cwd-permission"];
|
|
8374
8438
|
return result;
|
|
8375
8439
|
}
|
|
8376
8440
|
|
|
8377
|
-
// src/cli/
|
|
8378
|
-
|
|
8379
|
-
|
|
8380
|
-
|
|
8381
|
-
|
|
8441
|
+
// src/cli/docker/types.ts
|
|
8442
|
+
var VALID_MOUNT_PERMISSIONS = ["ro", "rw"];
|
|
8443
|
+
var DOCKER_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
8444
|
+
"enabled",
|
|
8445
|
+
"dockerfile",
|
|
8446
|
+
"cwd-permission",
|
|
8447
|
+
"config-permission",
|
|
8448
|
+
"mounts",
|
|
8449
|
+
"env-vars",
|
|
8450
|
+
"image-name",
|
|
8451
|
+
"dev-mode",
|
|
8452
|
+
"dev-source"
|
|
8453
|
+
]);
|
|
8454
|
+
var DEFAULT_IMAGE_NAME = "llmist-sandbox";
|
|
8455
|
+
var DEFAULT_CWD_PERMISSION = "rw";
|
|
8456
|
+
var DEFAULT_CONFIG_PERMISSION = "ro";
|
|
8457
|
+
var FORWARDED_API_KEYS = [
|
|
8458
|
+
"ANTHROPIC_API_KEY",
|
|
8459
|
+
"OPENAI_API_KEY",
|
|
8460
|
+
"GEMINI_API_KEY"
|
|
8461
|
+
];
|
|
8462
|
+
var DEV_IMAGE_NAME = "llmist-dev-sandbox";
|
|
8463
|
+
var DEV_SOURCE_MOUNT_TARGET = "/llmist-src";
|
|
8464
|
+
|
|
8465
|
+
// src/cli/config.ts
|
|
8466
|
+
var import_node_fs8 = require("fs");
|
|
8467
|
+
var import_node_os2 = require("os");
|
|
8468
|
+
var import_node_path8 = require("path");
|
|
8469
|
+
var import_js_toml = require("js-toml");
|
|
8470
|
+
|
|
8471
|
+
// src/cli/templates.ts
|
|
8472
|
+
var import_eta = require("eta");
|
|
8473
|
+
var TemplateError = class extends Error {
|
|
8474
|
+
constructor(message, promptName, configPath) {
|
|
8475
|
+
super(promptName ? `[prompts.${promptName}]: ${message}` : message);
|
|
8476
|
+
this.promptName = promptName;
|
|
8477
|
+
this.configPath = configPath;
|
|
8478
|
+
this.name = "TemplateError";
|
|
8382
8479
|
}
|
|
8383
|
-
|
|
8384
|
-
|
|
8385
|
-
|
|
8386
|
-
|
|
8387
|
-
|
|
8388
|
-
|
|
8389
|
-
|
|
8480
|
+
};
|
|
8481
|
+
function createTemplateEngine(prompts, configPath) {
|
|
8482
|
+
const eta = new import_eta.Eta({
|
|
8483
|
+
views: "/",
|
|
8484
|
+
// Required but we use named templates
|
|
8485
|
+
autoEscape: false,
|
|
8486
|
+
// Don't escape - these are prompts, not HTML
|
|
8487
|
+
autoTrim: false
|
|
8488
|
+
// Preserve whitespace in prompts
|
|
8489
|
+
});
|
|
8490
|
+
for (const [name, template] of Object.entries(prompts)) {
|
|
8390
8491
|
try {
|
|
8391
|
-
|
|
8392
|
-
|
|
8393
|
-
|
|
8394
|
-
|
|
8395
|
-
|
|
8396
|
-
|
|
8397
|
-
|
|
8398
|
-
isFirst = false;
|
|
8399
|
-
const answer = await rl.question(prompt);
|
|
8400
|
-
const trimmed = answer.trim();
|
|
8401
|
-
if (trimmed) {
|
|
8402
|
-
return trimmed;
|
|
8403
|
-
}
|
|
8404
|
-
}
|
|
8405
|
-
} finally {
|
|
8406
|
-
rl.close();
|
|
8407
|
-
keyboard.restore();
|
|
8408
|
-
}
|
|
8409
|
-
};
|
|
8410
|
-
}
|
|
8411
|
-
async function executeAgent(promptArg, options, env) {
|
|
8412
|
-
const prompt = await resolvePrompt(promptArg, env);
|
|
8413
|
-
const client = env.createClient();
|
|
8414
|
-
const registry = new GadgetRegistry();
|
|
8415
|
-
const stdinIsInteractive = isInteractive(env.stdin);
|
|
8416
|
-
if (options.builtins !== false) {
|
|
8417
|
-
for (const gadget of builtinGadgets) {
|
|
8418
|
-
if (gadget.name === "AskUser" && (options.builtinInteraction === false || !stdinIsInteractive)) {
|
|
8419
|
-
continue;
|
|
8420
|
-
}
|
|
8421
|
-
registry.registerByClass(gadget);
|
|
8422
|
-
}
|
|
8423
|
-
}
|
|
8424
|
-
const gadgetSpecifiers = options.gadget ?? [];
|
|
8425
|
-
if (gadgetSpecifiers.length > 0) {
|
|
8426
|
-
const gadgets2 = await loadGadgets(gadgetSpecifiers, process.cwd());
|
|
8427
|
-
for (const gadget of gadgets2) {
|
|
8428
|
-
registry.registerByClass(gadget);
|
|
8429
|
-
}
|
|
8430
|
-
}
|
|
8431
|
-
const printer = new StreamPrinter(env.stdout);
|
|
8432
|
-
const stderrTTY = env.stderr.isTTY === true;
|
|
8433
|
-
const progress = new StreamProgress(env.stderr, stderrTTY, client.modelRegistry);
|
|
8434
|
-
const abortController = new AbortController();
|
|
8435
|
-
let wasCancelled = false;
|
|
8436
|
-
let isStreaming = false;
|
|
8437
|
-
const stdinStream = env.stdin;
|
|
8438
|
-
const handleCancel = () => {
|
|
8439
|
-
if (!abortController.signal.aborted) {
|
|
8440
|
-
wasCancelled = true;
|
|
8441
|
-
abortController.abort();
|
|
8442
|
-
progress.pause();
|
|
8443
|
-
env.stderr.write(import_chalk5.default.yellow(`
|
|
8444
|
-
[Cancelled] ${progress.formatStats()}
|
|
8445
|
-
`));
|
|
8446
|
-
}
|
|
8447
|
-
};
|
|
8448
|
-
const keyboard = {
|
|
8449
|
-
cleanupEsc: null,
|
|
8450
|
-
cleanupSigint: null,
|
|
8451
|
-
restore: () => {
|
|
8452
|
-
if (stdinIsInteractive && stdinStream.isTTY && !wasCancelled) {
|
|
8453
|
-
keyboard.cleanupEsc = createEscKeyListener(stdinStream, handleCancel);
|
|
8454
|
-
}
|
|
8455
|
-
}
|
|
8456
|
-
};
|
|
8457
|
-
const handleQuit = () => {
|
|
8458
|
-
keyboard.cleanupEsc?.();
|
|
8459
|
-
keyboard.cleanupSigint?.();
|
|
8460
|
-
progress.complete();
|
|
8461
|
-
printer.ensureNewline();
|
|
8462
|
-
const summary = renderOverallSummary({
|
|
8463
|
-
totalTokens: usage?.totalTokens,
|
|
8464
|
-
iterations,
|
|
8465
|
-
elapsedSeconds: progress.getTotalElapsedSeconds(),
|
|
8466
|
-
cost: progress.getTotalCost()
|
|
8467
|
-
});
|
|
8468
|
-
if (summary) {
|
|
8469
|
-
env.stderr.write(`${import_chalk5.default.dim("\u2500".repeat(40))}
|
|
8470
|
-
`);
|
|
8471
|
-
env.stderr.write(`${summary}
|
|
8472
|
-
`);
|
|
8492
|
+
eta.loadTemplate(`@${name}`, template);
|
|
8493
|
+
} catch (error) {
|
|
8494
|
+
throw new TemplateError(
|
|
8495
|
+
error instanceof Error ? error.message : String(error),
|
|
8496
|
+
name,
|
|
8497
|
+
configPath
|
|
8498
|
+
);
|
|
8473
8499
|
}
|
|
8474
|
-
env.stderr.write(import_chalk5.default.dim("[Quit]\n"));
|
|
8475
|
-
process.exit(130);
|
|
8476
|
-
};
|
|
8477
|
-
if (stdinIsInteractive && stdinStream.isTTY) {
|
|
8478
|
-
keyboard.cleanupEsc = createEscKeyListener(stdinStream, handleCancel);
|
|
8479
8500
|
}
|
|
8480
|
-
|
|
8481
|
-
|
|
8482
|
-
|
|
8483
|
-
|
|
8484
|
-
|
|
8485
|
-
|
|
8486
|
-
|
|
8487
|
-
|
|
8488
|
-
|
|
8489
|
-
|
|
8490
|
-
|
|
8491
|
-
|
|
8492
|
-
|
|
8493
|
-
|
|
8494
|
-
|
|
8501
|
+
return eta;
|
|
8502
|
+
}
|
|
8503
|
+
function resolveTemplate(eta, template, context = {}, configPath) {
|
|
8504
|
+
try {
|
|
8505
|
+
const fullContext = {
|
|
8506
|
+
...context,
|
|
8507
|
+
env: process.env,
|
|
8508
|
+
date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
|
|
8509
|
+
// "2025-12-01"
|
|
8510
|
+
};
|
|
8511
|
+
return eta.renderString(template, fullContext);
|
|
8512
|
+
} catch (error) {
|
|
8513
|
+
throw new TemplateError(
|
|
8514
|
+
error instanceof Error ? error.message : String(error),
|
|
8515
|
+
void 0,
|
|
8516
|
+
configPath
|
|
8495
8517
|
);
|
|
8496
|
-
if (!isConfigured) {
|
|
8497
|
-
gadgetApprovals[gadget] = "approval-required";
|
|
8498
|
-
}
|
|
8499
8518
|
}
|
|
8500
|
-
|
|
8501
|
-
|
|
8502
|
-
|
|
8503
|
-
|
|
8504
|
-
const approvalManager = new ApprovalManager(approvalConfig, env, progress);
|
|
8505
|
-
let usage;
|
|
8506
|
-
let iterations = 0;
|
|
8507
|
-
const llmRequestsDir = resolveLogDir(options.logLlmRequests, "requests");
|
|
8508
|
-
const llmResponsesDir = resolveLogDir(options.logLlmResponses, "responses");
|
|
8509
|
-
let llmCallCounter = 0;
|
|
8510
|
-
const countMessagesTokens = async (model, messages) => {
|
|
8519
|
+
}
|
|
8520
|
+
function validatePrompts(prompts, configPath) {
|
|
8521
|
+
const eta = createTemplateEngine(prompts, configPath);
|
|
8522
|
+
for (const [name, template] of Object.entries(prompts)) {
|
|
8511
8523
|
try {
|
|
8512
|
-
|
|
8513
|
-
} catch {
|
|
8514
|
-
|
|
8515
|
-
|
|
8524
|
+
eta.renderString(template, { env: {} });
|
|
8525
|
+
} catch (error) {
|
|
8526
|
+
throw new TemplateError(
|
|
8527
|
+
error instanceof Error ? error.message : String(error),
|
|
8528
|
+
name,
|
|
8529
|
+
configPath
|
|
8530
|
+
);
|
|
8516
8531
|
}
|
|
8517
|
-
}
|
|
8518
|
-
|
|
8519
|
-
|
|
8520
|
-
|
|
8521
|
-
|
|
8522
|
-
|
|
8523
|
-
|
|
8524
|
-
|
|
8532
|
+
}
|
|
8533
|
+
}
|
|
8534
|
+
function validateEnvVars(template, promptName, configPath) {
|
|
8535
|
+
const envVarPattern = /<%=\s*it\.env\.(\w+)\s*%>/g;
|
|
8536
|
+
const matches = template.matchAll(envVarPattern);
|
|
8537
|
+
for (const match of matches) {
|
|
8538
|
+
const varName = match[1];
|
|
8539
|
+
if (process.env[varName] === void 0) {
|
|
8540
|
+
throw new TemplateError(
|
|
8541
|
+
`Environment variable '${varName}' is not set`,
|
|
8542
|
+
promptName,
|
|
8543
|
+
configPath
|
|
8544
|
+
);
|
|
8525
8545
|
}
|
|
8526
|
-
}
|
|
8527
|
-
|
|
8528
|
-
|
|
8529
|
-
|
|
8530
|
-
|
|
8531
|
-
onLLMCallStart: async (context) => {
|
|
8532
|
-
isStreaming = true;
|
|
8533
|
-
llmCallCounter++;
|
|
8534
|
-
const inputTokens = await countMessagesTokens(
|
|
8535
|
-
context.options.model,
|
|
8536
|
-
context.options.messages
|
|
8537
|
-
);
|
|
8538
|
-
progress.startCall(context.options.model, inputTokens);
|
|
8539
|
-
progress.setInputTokens(inputTokens, false);
|
|
8540
|
-
if (llmRequestsDir) {
|
|
8541
|
-
const filename = `${Date.now()}_call_${llmCallCounter}.request.txt`;
|
|
8542
|
-
const content = formatLlmRequest(context.options.messages);
|
|
8543
|
-
await writeLogFile(llmRequestsDir, filename, content);
|
|
8544
|
-
}
|
|
8545
|
-
},
|
|
8546
|
-
// onStreamChunk: Real-time updates as LLM generates tokens
|
|
8547
|
-
// This enables responsive UIs that show progress during generation
|
|
8548
|
-
onStreamChunk: async (context) => {
|
|
8549
|
-
progress.update(context.accumulatedText.length);
|
|
8550
|
-
if (context.usage) {
|
|
8551
|
-
if (context.usage.inputTokens) {
|
|
8552
|
-
progress.setInputTokens(context.usage.inputTokens, false);
|
|
8553
|
-
}
|
|
8554
|
-
if (context.usage.outputTokens) {
|
|
8555
|
-
progress.setOutputTokens(context.usage.outputTokens, false);
|
|
8556
|
-
}
|
|
8557
|
-
progress.setCachedTokens(
|
|
8558
|
-
context.usage.cachedInputTokens ?? 0,
|
|
8559
|
-
context.usage.cacheCreationInputTokens ?? 0
|
|
8560
|
-
);
|
|
8561
|
-
}
|
|
8562
|
-
},
|
|
8563
|
-
// onLLMCallComplete: Finalize metrics after each LLM call
|
|
8564
|
-
// This is where you'd typically log metrics or update dashboards
|
|
8565
|
-
onLLMCallComplete: async (context) => {
|
|
8566
|
-
isStreaming = false;
|
|
8567
|
-
usage = context.usage;
|
|
8568
|
-
iterations = Math.max(iterations, context.iteration + 1);
|
|
8569
|
-
if (context.usage) {
|
|
8570
|
-
if (context.usage.inputTokens) {
|
|
8571
|
-
progress.setInputTokens(context.usage.inputTokens, false);
|
|
8572
|
-
}
|
|
8573
|
-
if (context.usage.outputTokens) {
|
|
8574
|
-
progress.setOutputTokens(context.usage.outputTokens, false);
|
|
8575
|
-
}
|
|
8576
|
-
}
|
|
8577
|
-
let callCost;
|
|
8578
|
-
if (context.usage && client.modelRegistry) {
|
|
8579
|
-
try {
|
|
8580
|
-
const modelName = context.options.model.includes(":") ? context.options.model.split(":")[1] : context.options.model;
|
|
8581
|
-
const costResult = client.modelRegistry.estimateCost(
|
|
8582
|
-
modelName,
|
|
8583
|
-
context.usage.inputTokens,
|
|
8584
|
-
context.usage.outputTokens,
|
|
8585
|
-
context.usage.cachedInputTokens ?? 0,
|
|
8586
|
-
context.usage.cacheCreationInputTokens ?? 0
|
|
8587
|
-
);
|
|
8588
|
-
if (costResult) callCost = costResult.totalCost;
|
|
8589
|
-
} catch {
|
|
8590
|
-
}
|
|
8591
|
-
}
|
|
8592
|
-
const callElapsed = progress.getCallElapsedSeconds();
|
|
8593
|
-
progress.endCall(context.usage);
|
|
8594
|
-
if (!options.quiet) {
|
|
8595
|
-
const summary = renderSummary({
|
|
8596
|
-
iterations: context.iteration + 1,
|
|
8597
|
-
model: options.model,
|
|
8598
|
-
usage: context.usage,
|
|
8599
|
-
elapsedSeconds: callElapsed,
|
|
8600
|
-
cost: callCost,
|
|
8601
|
-
finishReason: context.finishReason
|
|
8602
|
-
});
|
|
8603
|
-
if (summary) {
|
|
8604
|
-
env.stderr.write(`${summary}
|
|
8605
|
-
`);
|
|
8606
|
-
}
|
|
8607
|
-
}
|
|
8608
|
-
if (llmResponsesDir) {
|
|
8609
|
-
const filename = `${Date.now()}_call_${llmCallCounter}.response.txt`;
|
|
8610
|
-
await writeLogFile(llmResponsesDir, filename, context.rawResponse);
|
|
8611
|
-
}
|
|
8612
|
-
}
|
|
8613
|
-
},
|
|
8614
|
-
// SHOWCASE: Controller-based approval gating for gadgets
|
|
8615
|
-
//
|
|
8616
|
-
// This demonstrates how to add safety layers WITHOUT modifying gadgets.
|
|
8617
|
-
// The ApprovalManager handles approval flows externally via beforeGadgetExecution.
|
|
8618
|
-
// Approval modes are configurable via cli.toml:
|
|
8619
|
-
// - "allowed": auto-proceed
|
|
8620
|
-
// - "denied": auto-reject, return message to LLM
|
|
8621
|
-
// - "approval-required": prompt user interactively
|
|
8622
|
-
//
|
|
8623
|
-
// Default: RunCommand, WriteFile, EditFile require approval unless overridden.
|
|
8624
|
-
controllers: {
|
|
8625
|
-
beforeGadgetExecution: async (ctx) => {
|
|
8626
|
-
const mode = approvalManager.getApprovalMode(ctx.gadgetName);
|
|
8627
|
-
if (mode === "allowed") {
|
|
8628
|
-
return { action: "proceed" };
|
|
8629
|
-
}
|
|
8630
|
-
const stdinTTY = isInteractive(env.stdin);
|
|
8631
|
-
const stderrTTY2 = env.stderr.isTTY === true;
|
|
8632
|
-
const canPrompt = stdinTTY && stderrTTY2;
|
|
8633
|
-
if (!canPrompt) {
|
|
8634
|
-
if (mode === "approval-required") {
|
|
8635
|
-
return {
|
|
8636
|
-
action: "skip",
|
|
8637
|
-
syntheticResult: `status=denied
|
|
8638
|
-
|
|
8639
|
-
${ctx.gadgetName} requires interactive approval. Run in a terminal to approve.`
|
|
8640
|
-
};
|
|
8641
|
-
}
|
|
8642
|
-
if (mode === "denied") {
|
|
8643
|
-
return {
|
|
8644
|
-
action: "skip",
|
|
8645
|
-
syntheticResult: `status=denied
|
|
8646
|
-
|
|
8647
|
-
${ctx.gadgetName} is denied by configuration.`
|
|
8648
|
-
};
|
|
8649
|
-
}
|
|
8650
|
-
return { action: "proceed" };
|
|
8651
|
-
}
|
|
8652
|
-
const result = await approvalManager.requestApproval(ctx.gadgetName, ctx.parameters);
|
|
8653
|
-
if (!result.approved) {
|
|
8654
|
-
return {
|
|
8655
|
-
action: "skip",
|
|
8656
|
-
syntheticResult: `status=denied
|
|
8546
|
+
}
|
|
8547
|
+
}
|
|
8548
|
+
function hasTemplateSyntax(str) {
|
|
8549
|
+
return str.includes("<%");
|
|
8550
|
+
}
|
|
8657
8551
|
|
|
8658
|
-
|
|
8659
|
-
|
|
8660
|
-
|
|
8661
|
-
|
|
8552
|
+
// src/cli/config.ts
|
|
8553
|
+
var VALID_APPROVAL_MODES = ["allowed", "denied", "approval-required"];
|
|
8554
|
+
var GLOBAL_CONFIG_KEYS = /* @__PURE__ */ new Set(["log-level", "log-file", "log-reset"]);
|
|
8555
|
+
var VALID_LOG_LEVELS = ["silly", "trace", "debug", "info", "warn", "error", "fatal"];
|
|
8556
|
+
var COMPLETE_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
8557
|
+
"model",
|
|
8558
|
+
"system",
|
|
8559
|
+
"temperature",
|
|
8560
|
+
"max-tokens",
|
|
8561
|
+
"quiet",
|
|
8562
|
+
"inherits",
|
|
8563
|
+
"log-level",
|
|
8564
|
+
"log-file",
|
|
8565
|
+
"log-reset",
|
|
8566
|
+
"log-llm-requests",
|
|
8567
|
+
"log-llm-responses",
|
|
8568
|
+
"type",
|
|
8569
|
+
// Allowed for inheritance compatibility, ignored for built-in commands
|
|
8570
|
+
"docker",
|
|
8571
|
+
// Enable Docker sandboxing (only effective for agent type)
|
|
8572
|
+
"docker-cwd-permission"
|
|
8573
|
+
// Override CWD mount permission for this profile
|
|
8574
|
+
]);
|
|
8575
|
+
var AGENT_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
8576
|
+
"model",
|
|
8577
|
+
"system",
|
|
8578
|
+
"temperature",
|
|
8579
|
+
"max-iterations",
|
|
8580
|
+
"gadgets",
|
|
8581
|
+
// Full replacement (preferred)
|
|
8582
|
+
"gadget-add",
|
|
8583
|
+
// Add to inherited gadgets
|
|
8584
|
+
"gadget-remove",
|
|
8585
|
+
// Remove from inherited gadgets
|
|
8586
|
+
"gadget",
|
|
8587
|
+
// DEPRECATED: alias for gadgets
|
|
8588
|
+
"builtins",
|
|
8589
|
+
"builtin-interaction",
|
|
8590
|
+
"gadget-start-prefix",
|
|
8591
|
+
"gadget-end-prefix",
|
|
8592
|
+
"gadget-arg-prefix",
|
|
8593
|
+
"gadget-approval",
|
|
8594
|
+
"quiet",
|
|
8595
|
+
"inherits",
|
|
8596
|
+
"log-level",
|
|
8597
|
+
"log-file",
|
|
8598
|
+
"log-reset",
|
|
8599
|
+
"log-llm-requests",
|
|
8600
|
+
"log-llm-responses",
|
|
8601
|
+
"type",
|
|
8602
|
+
// Allowed for inheritance compatibility, ignored for built-in commands
|
|
8603
|
+
"docker",
|
|
8604
|
+
// Enable Docker sandboxing for this profile
|
|
8605
|
+
"docker-cwd-permission"
|
|
8606
|
+
// Override CWD mount permission for this profile
|
|
8607
|
+
]);
|
|
8608
|
+
var CUSTOM_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
8609
|
+
...COMPLETE_CONFIG_KEYS,
|
|
8610
|
+
...AGENT_CONFIG_KEYS,
|
|
8611
|
+
"type",
|
|
8612
|
+
"description"
|
|
8613
|
+
]);
|
|
8614
|
+
function getConfigPath() {
|
|
8615
|
+
return (0, import_node_path8.join)((0, import_node_os2.homedir)(), ".llmist", "cli.toml");
|
|
8616
|
+
}
|
|
8617
|
+
var ConfigError = class extends Error {
|
|
8618
|
+
constructor(message, path5) {
|
|
8619
|
+
super(path5 ? `${path5}: ${message}` : message);
|
|
8620
|
+
this.path = path5;
|
|
8621
|
+
this.name = "ConfigError";
|
|
8622
|
+
}
|
|
8623
|
+
};
|
|
8624
|
+
function validateString(value, key, section) {
|
|
8625
|
+
if (typeof value !== "string") {
|
|
8626
|
+
throw new ConfigError(`[${section}].${key} must be a string`);
|
|
8627
|
+
}
|
|
8628
|
+
return value;
|
|
8629
|
+
}
|
|
8630
|
+
function validateNumber(value, key, section, opts) {
|
|
8631
|
+
if (typeof value !== "number") {
|
|
8632
|
+
throw new ConfigError(`[${section}].${key} must be a number`);
|
|
8633
|
+
}
|
|
8634
|
+
if (opts?.integer && !Number.isInteger(value)) {
|
|
8635
|
+
throw new ConfigError(`[${section}].${key} must be an integer`);
|
|
8636
|
+
}
|
|
8637
|
+
if (opts?.min !== void 0 && value < opts.min) {
|
|
8638
|
+
throw new ConfigError(`[${section}].${key} must be >= ${opts.min}`);
|
|
8639
|
+
}
|
|
8640
|
+
if (opts?.max !== void 0 && value > opts.max) {
|
|
8641
|
+
throw new ConfigError(`[${section}].${key} must be <= ${opts.max}`);
|
|
8642
|
+
}
|
|
8643
|
+
return value;
|
|
8644
|
+
}
|
|
8645
|
+
function validateBoolean(value, key, section) {
|
|
8646
|
+
if (typeof value !== "boolean") {
|
|
8647
|
+
throw new ConfigError(`[${section}].${key} must be a boolean`);
|
|
8648
|
+
}
|
|
8649
|
+
return value;
|
|
8650
|
+
}
|
|
8651
|
+
function validateStringArray(value, key, section) {
|
|
8652
|
+
if (!Array.isArray(value)) {
|
|
8653
|
+
throw new ConfigError(`[${section}].${key} must be an array`);
|
|
8654
|
+
}
|
|
8655
|
+
for (let i = 0; i < value.length; i++) {
|
|
8656
|
+
if (typeof value[i] !== "string") {
|
|
8657
|
+
throw new ConfigError(`[${section}].${key}[${i}] must be a string`);
|
|
8658
|
+
}
|
|
8659
|
+
}
|
|
8660
|
+
return value;
|
|
8661
|
+
}
|
|
8662
|
+
function validateInherits(value, section) {
|
|
8663
|
+
if (typeof value === "string") {
|
|
8664
|
+
return value;
|
|
8665
|
+
}
|
|
8666
|
+
if (Array.isArray(value)) {
|
|
8667
|
+
for (let i = 0; i < value.length; i++) {
|
|
8668
|
+
if (typeof value[i] !== "string") {
|
|
8669
|
+
throw new ConfigError(`[${section}].inherits[${i}] must be a string`);
|
|
8662
8670
|
}
|
|
8663
8671
|
}
|
|
8664
|
-
|
|
8665
|
-
if (options.system) {
|
|
8666
|
-
builder.withSystem(options.system);
|
|
8672
|
+
return value;
|
|
8667
8673
|
}
|
|
8668
|
-
|
|
8669
|
-
|
|
8674
|
+
throw new ConfigError(`[${section}].inherits must be a string or array of strings`);
|
|
8675
|
+
}
|
|
8676
|
+
function validateGadgetApproval(value, section) {
|
|
8677
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
8678
|
+
throw new ConfigError(
|
|
8679
|
+
`[${section}].gadget-approval must be a table (e.g., { WriteFile = "approval-required" })`
|
|
8680
|
+
);
|
|
8681
|
+
}
|
|
8682
|
+
const result = {};
|
|
8683
|
+
for (const [gadgetName, mode] of Object.entries(value)) {
|
|
8684
|
+
if (typeof mode !== "string") {
|
|
8685
|
+
throw new ConfigError(
|
|
8686
|
+
`[${section}].gadget-approval.${gadgetName} must be a string`
|
|
8687
|
+
);
|
|
8688
|
+
}
|
|
8689
|
+
if (!VALID_APPROVAL_MODES.includes(mode)) {
|
|
8690
|
+
throw new ConfigError(
|
|
8691
|
+
`[${section}].gadget-approval.${gadgetName} must be one of: ${VALID_APPROVAL_MODES.join(", ")}`
|
|
8692
|
+
);
|
|
8693
|
+
}
|
|
8694
|
+
result[gadgetName] = mode;
|
|
8695
|
+
}
|
|
8696
|
+
return result;
|
|
8697
|
+
}
|
|
8698
|
+
function validateLoggingConfig(raw, section) {
|
|
8699
|
+
const result = {};
|
|
8700
|
+
if ("log-level" in raw) {
|
|
8701
|
+
const level = validateString(raw["log-level"], "log-level", section);
|
|
8702
|
+
if (!VALID_LOG_LEVELS.includes(level)) {
|
|
8703
|
+
throw new ConfigError(
|
|
8704
|
+
`[${section}].log-level must be one of: ${VALID_LOG_LEVELS.join(", ")}`
|
|
8705
|
+
);
|
|
8706
|
+
}
|
|
8707
|
+
result["log-level"] = level;
|
|
8708
|
+
}
|
|
8709
|
+
if ("log-file" in raw) {
|
|
8710
|
+
result["log-file"] = validateString(raw["log-file"], "log-file", section);
|
|
8711
|
+
}
|
|
8712
|
+
if ("log-reset" in raw) {
|
|
8713
|
+
result["log-reset"] = validateBoolean(raw["log-reset"], "log-reset", section);
|
|
8714
|
+
}
|
|
8715
|
+
return result;
|
|
8716
|
+
}
|
|
8717
|
+
function validateBaseConfig(raw, section) {
|
|
8718
|
+
const result = {};
|
|
8719
|
+
if ("model" in raw) {
|
|
8720
|
+
result.model = validateString(raw.model, "model", section);
|
|
8721
|
+
}
|
|
8722
|
+
if ("system" in raw) {
|
|
8723
|
+
result.system = validateString(raw.system, "system", section);
|
|
8724
|
+
}
|
|
8725
|
+
if ("temperature" in raw) {
|
|
8726
|
+
result.temperature = validateNumber(raw.temperature, "temperature", section, {
|
|
8727
|
+
min: 0,
|
|
8728
|
+
max: 2
|
|
8729
|
+
});
|
|
8730
|
+
}
|
|
8731
|
+
if ("inherits" in raw) {
|
|
8732
|
+
result.inherits = validateInherits(raw.inherits, section);
|
|
8733
|
+
}
|
|
8734
|
+
if ("docker" in raw) {
|
|
8735
|
+
result.docker = validateBoolean(raw.docker, "docker", section);
|
|
8736
|
+
}
|
|
8737
|
+
if ("docker-cwd-permission" in raw) {
|
|
8738
|
+
const perm = validateString(raw["docker-cwd-permission"], "docker-cwd-permission", section);
|
|
8739
|
+
if (perm !== "ro" && perm !== "rw") {
|
|
8740
|
+
throw new ConfigError(`[${section}].docker-cwd-permission must be "ro" or "rw"`);
|
|
8741
|
+
}
|
|
8742
|
+
result["docker-cwd-permission"] = perm;
|
|
8743
|
+
}
|
|
8744
|
+
return result;
|
|
8745
|
+
}
|
|
8746
|
+
function validateGlobalConfig(raw, section) {
|
|
8747
|
+
if (typeof raw !== "object" || raw === null) {
|
|
8748
|
+
throw new ConfigError(`[${section}] must be a table`);
|
|
8749
|
+
}
|
|
8750
|
+
const rawObj = raw;
|
|
8751
|
+
for (const key of Object.keys(rawObj)) {
|
|
8752
|
+
if (!GLOBAL_CONFIG_KEYS.has(key)) {
|
|
8753
|
+
throw new ConfigError(`[${section}].${key} is not a valid option`);
|
|
8754
|
+
}
|
|
8755
|
+
}
|
|
8756
|
+
return validateLoggingConfig(rawObj, section);
|
|
8757
|
+
}
|
|
8758
|
+
function validateCompleteConfig(raw, section) {
|
|
8759
|
+
if (typeof raw !== "object" || raw === null) {
|
|
8760
|
+
throw new ConfigError(`[${section}] must be a table`);
|
|
8761
|
+
}
|
|
8762
|
+
const rawObj = raw;
|
|
8763
|
+
for (const key of Object.keys(rawObj)) {
|
|
8764
|
+
if (!COMPLETE_CONFIG_KEYS.has(key)) {
|
|
8765
|
+
throw new ConfigError(`[${section}].${key} is not a valid option`);
|
|
8766
|
+
}
|
|
8767
|
+
}
|
|
8768
|
+
const result = {
|
|
8769
|
+
...validateBaseConfig(rawObj, section),
|
|
8770
|
+
...validateLoggingConfig(rawObj, section)
|
|
8771
|
+
};
|
|
8772
|
+
if ("max-tokens" in rawObj) {
|
|
8773
|
+
result["max-tokens"] = validateNumber(rawObj["max-tokens"], "max-tokens", section, {
|
|
8774
|
+
integer: true,
|
|
8775
|
+
min: 1
|
|
8776
|
+
});
|
|
8777
|
+
}
|
|
8778
|
+
if ("quiet" in rawObj) {
|
|
8779
|
+
result.quiet = validateBoolean(rawObj.quiet, "quiet", section);
|
|
8780
|
+
}
|
|
8781
|
+
if ("log-llm-requests" in rawObj) {
|
|
8782
|
+
result["log-llm-requests"] = validateStringOrBoolean(
|
|
8783
|
+
rawObj["log-llm-requests"],
|
|
8784
|
+
"log-llm-requests",
|
|
8785
|
+
section
|
|
8786
|
+
);
|
|
8787
|
+
}
|
|
8788
|
+
if ("log-llm-responses" in rawObj) {
|
|
8789
|
+
result["log-llm-responses"] = validateStringOrBoolean(
|
|
8790
|
+
rawObj["log-llm-responses"],
|
|
8791
|
+
"log-llm-responses",
|
|
8792
|
+
section
|
|
8793
|
+
);
|
|
8794
|
+
}
|
|
8795
|
+
return result;
|
|
8796
|
+
}
|
|
8797
|
+
function validateAgentConfig(raw, section) {
|
|
8798
|
+
if (typeof raw !== "object" || raw === null) {
|
|
8799
|
+
throw new ConfigError(`[${section}] must be a table`);
|
|
8800
|
+
}
|
|
8801
|
+
const rawObj = raw;
|
|
8802
|
+
for (const key of Object.keys(rawObj)) {
|
|
8803
|
+
if (!AGENT_CONFIG_KEYS.has(key)) {
|
|
8804
|
+
throw new ConfigError(`[${section}].${key} is not a valid option`);
|
|
8805
|
+
}
|
|
8806
|
+
}
|
|
8807
|
+
const result = {
|
|
8808
|
+
...validateBaseConfig(rawObj, section),
|
|
8809
|
+
...validateLoggingConfig(rawObj, section)
|
|
8810
|
+
};
|
|
8811
|
+
if ("max-iterations" in rawObj) {
|
|
8812
|
+
result["max-iterations"] = validateNumber(rawObj["max-iterations"], "max-iterations", section, {
|
|
8813
|
+
integer: true,
|
|
8814
|
+
min: 1
|
|
8815
|
+
});
|
|
8816
|
+
}
|
|
8817
|
+
if ("gadgets" in rawObj) {
|
|
8818
|
+
result.gadgets = validateStringArray(rawObj.gadgets, "gadgets", section);
|
|
8819
|
+
}
|
|
8820
|
+
if ("gadget-add" in rawObj) {
|
|
8821
|
+
result["gadget-add"] = validateStringArray(rawObj["gadget-add"], "gadget-add", section);
|
|
8822
|
+
}
|
|
8823
|
+
if ("gadget-remove" in rawObj) {
|
|
8824
|
+
result["gadget-remove"] = validateStringArray(rawObj["gadget-remove"], "gadget-remove", section);
|
|
8825
|
+
}
|
|
8826
|
+
if ("gadget" in rawObj) {
|
|
8827
|
+
result.gadget = validateStringArray(rawObj.gadget, "gadget", section);
|
|
8828
|
+
}
|
|
8829
|
+
if ("builtins" in rawObj) {
|
|
8830
|
+
result.builtins = validateBoolean(rawObj.builtins, "builtins", section);
|
|
8831
|
+
}
|
|
8832
|
+
if ("builtin-interaction" in rawObj) {
|
|
8833
|
+
result["builtin-interaction"] = validateBoolean(
|
|
8834
|
+
rawObj["builtin-interaction"],
|
|
8835
|
+
"builtin-interaction",
|
|
8836
|
+
section
|
|
8837
|
+
);
|
|
8838
|
+
}
|
|
8839
|
+
if ("gadget-start-prefix" in rawObj) {
|
|
8840
|
+
result["gadget-start-prefix"] = validateString(
|
|
8841
|
+
rawObj["gadget-start-prefix"],
|
|
8842
|
+
"gadget-start-prefix",
|
|
8843
|
+
section
|
|
8844
|
+
);
|
|
8845
|
+
}
|
|
8846
|
+
if ("gadget-end-prefix" in rawObj) {
|
|
8847
|
+
result["gadget-end-prefix"] = validateString(
|
|
8848
|
+
rawObj["gadget-end-prefix"],
|
|
8849
|
+
"gadget-end-prefix",
|
|
8850
|
+
section
|
|
8851
|
+
);
|
|
8852
|
+
}
|
|
8853
|
+
if ("gadget-arg-prefix" in rawObj) {
|
|
8854
|
+
result["gadget-arg-prefix"] = validateString(
|
|
8855
|
+
rawObj["gadget-arg-prefix"],
|
|
8856
|
+
"gadget-arg-prefix",
|
|
8857
|
+
section
|
|
8858
|
+
);
|
|
8859
|
+
}
|
|
8860
|
+
if ("gadget-approval" in rawObj) {
|
|
8861
|
+
result["gadget-approval"] = validateGadgetApproval(rawObj["gadget-approval"], section);
|
|
8862
|
+
}
|
|
8863
|
+
if ("quiet" in rawObj) {
|
|
8864
|
+
result.quiet = validateBoolean(rawObj.quiet, "quiet", section);
|
|
8865
|
+
}
|
|
8866
|
+
if ("log-llm-requests" in rawObj) {
|
|
8867
|
+
result["log-llm-requests"] = validateStringOrBoolean(
|
|
8868
|
+
rawObj["log-llm-requests"],
|
|
8869
|
+
"log-llm-requests",
|
|
8870
|
+
section
|
|
8871
|
+
);
|
|
8872
|
+
}
|
|
8873
|
+
if ("log-llm-responses" in rawObj) {
|
|
8874
|
+
result["log-llm-responses"] = validateStringOrBoolean(
|
|
8875
|
+
rawObj["log-llm-responses"],
|
|
8876
|
+
"log-llm-responses",
|
|
8877
|
+
section
|
|
8878
|
+
);
|
|
8879
|
+
}
|
|
8880
|
+
return result;
|
|
8881
|
+
}
|
|
8882
|
+
function validateStringOrBoolean(value, field, section) {
|
|
8883
|
+
if (typeof value === "string" || typeof value === "boolean") {
|
|
8884
|
+
return value;
|
|
8885
|
+
}
|
|
8886
|
+
throw new ConfigError(`[${section}].${field} must be a string or boolean`);
|
|
8887
|
+
}
|
|
8888
|
+
function validateCustomConfig(raw, section) {
|
|
8889
|
+
if (typeof raw !== "object" || raw === null) {
|
|
8890
|
+
throw new ConfigError(`[${section}] must be a table`);
|
|
8891
|
+
}
|
|
8892
|
+
const rawObj = raw;
|
|
8893
|
+
for (const key of Object.keys(rawObj)) {
|
|
8894
|
+
if (!CUSTOM_CONFIG_KEYS.has(key)) {
|
|
8895
|
+
throw new ConfigError(`[${section}].${key} is not a valid option`);
|
|
8896
|
+
}
|
|
8897
|
+
}
|
|
8898
|
+
let type = "agent";
|
|
8899
|
+
if ("type" in rawObj) {
|
|
8900
|
+
const typeValue = validateString(rawObj.type, "type", section);
|
|
8901
|
+
if (typeValue !== "agent" && typeValue !== "complete") {
|
|
8902
|
+
throw new ConfigError(`[${section}].type must be "agent" or "complete"`);
|
|
8903
|
+
}
|
|
8904
|
+
type = typeValue;
|
|
8905
|
+
}
|
|
8906
|
+
const result = {
|
|
8907
|
+
...validateBaseConfig(rawObj, section),
|
|
8908
|
+
type
|
|
8909
|
+
};
|
|
8910
|
+
if ("description" in rawObj) {
|
|
8911
|
+
result.description = validateString(rawObj.description, "description", section);
|
|
8670
8912
|
}
|
|
8671
|
-
if (
|
|
8672
|
-
|
|
8913
|
+
if ("max-iterations" in rawObj) {
|
|
8914
|
+
result["max-iterations"] = validateNumber(rawObj["max-iterations"], "max-iterations", section, {
|
|
8915
|
+
integer: true,
|
|
8916
|
+
min: 1
|
|
8917
|
+
});
|
|
8673
8918
|
}
|
|
8674
|
-
|
|
8675
|
-
|
|
8676
|
-
builder.onHumanInput(humanInputHandler);
|
|
8919
|
+
if ("gadgets" in rawObj) {
|
|
8920
|
+
result.gadgets = validateStringArray(rawObj.gadgets, "gadgets", section);
|
|
8677
8921
|
}
|
|
8678
|
-
|
|
8679
|
-
|
|
8680
|
-
if (gadgets.length > 0) {
|
|
8681
|
-
builder.withGadgets(...gadgets);
|
|
8922
|
+
if ("gadget-add" in rawObj) {
|
|
8923
|
+
result["gadget-add"] = validateStringArray(rawObj["gadget-add"], "gadget-add", section);
|
|
8682
8924
|
}
|
|
8683
|
-
if (
|
|
8684
|
-
|
|
8925
|
+
if ("gadget-remove" in rawObj) {
|
|
8926
|
+
result["gadget-remove"] = validateStringArray(rawObj["gadget-remove"], "gadget-remove", section);
|
|
8685
8927
|
}
|
|
8686
|
-
if (
|
|
8687
|
-
|
|
8928
|
+
if ("gadget" in rawObj) {
|
|
8929
|
+
result.gadget = validateStringArray(rawObj.gadget, "gadget", section);
|
|
8688
8930
|
}
|
|
8689
|
-
if (
|
|
8690
|
-
|
|
8931
|
+
if ("builtins" in rawObj) {
|
|
8932
|
+
result.builtins = validateBoolean(rawObj.builtins, "builtins", section);
|
|
8691
8933
|
}
|
|
8692
|
-
|
|
8693
|
-
"
|
|
8694
|
-
|
|
8695
|
-
|
|
8696
|
-
|
|
8697
|
-
|
|
8698
|
-
},
|
|
8699
|
-
"\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?"
|
|
8700
|
-
);
|
|
8701
|
-
builder.withTextOnlyHandler("acknowledge");
|
|
8702
|
-
builder.withTextWithGadgetsHandler({
|
|
8703
|
-
gadgetName: "TellUser",
|
|
8704
|
-
parameterMapping: (text) => ({ message: text, done: false, type: "info" }),
|
|
8705
|
-
resultMapping: (text) => `\u2139\uFE0F ${text}`
|
|
8706
|
-
});
|
|
8707
|
-
const agent = builder.ask(prompt);
|
|
8708
|
-
let textBuffer = "";
|
|
8709
|
-
const flushTextBuffer = () => {
|
|
8710
|
-
if (textBuffer) {
|
|
8711
|
-
const output = options.quiet ? textBuffer : renderMarkdownWithSeparators(textBuffer);
|
|
8712
|
-
printer.write(output);
|
|
8713
|
-
textBuffer = "";
|
|
8714
|
-
}
|
|
8715
|
-
};
|
|
8716
|
-
try {
|
|
8717
|
-
for await (const event of agent.run()) {
|
|
8718
|
-
if (event.type === "text") {
|
|
8719
|
-
progress.pause();
|
|
8720
|
-
textBuffer += event.content;
|
|
8721
|
-
} else if (event.type === "gadget_result") {
|
|
8722
|
-
flushTextBuffer();
|
|
8723
|
-
progress.pause();
|
|
8724
|
-
if (options.quiet) {
|
|
8725
|
-
if (event.result.gadgetName === "TellUser" && event.result.parameters?.message) {
|
|
8726
|
-
const message = String(event.result.parameters.message);
|
|
8727
|
-
env.stdout.write(`${message}
|
|
8728
|
-
`);
|
|
8729
|
-
}
|
|
8730
|
-
} else {
|
|
8731
|
-
const tokenCount = await countGadgetOutputTokens(event.result.result);
|
|
8732
|
-
env.stderr.write(`${formatGadgetSummary({ ...event.result, tokenCount })}
|
|
8733
|
-
`);
|
|
8734
|
-
}
|
|
8735
|
-
}
|
|
8736
|
-
}
|
|
8737
|
-
} catch (error) {
|
|
8738
|
-
if (!isAbortError(error)) {
|
|
8739
|
-
throw error;
|
|
8740
|
-
}
|
|
8741
|
-
} finally {
|
|
8742
|
-
isStreaming = false;
|
|
8743
|
-
keyboard.cleanupEsc?.();
|
|
8744
|
-
keyboard.cleanupSigint?.();
|
|
8934
|
+
if ("builtin-interaction" in rawObj) {
|
|
8935
|
+
result["builtin-interaction"] = validateBoolean(
|
|
8936
|
+
rawObj["builtin-interaction"],
|
|
8937
|
+
"builtin-interaction",
|
|
8938
|
+
section
|
|
8939
|
+
);
|
|
8745
8940
|
}
|
|
8746
|
-
|
|
8747
|
-
|
|
8748
|
-
|
|
8749
|
-
|
|
8750
|
-
|
|
8751
|
-
|
|
8752
|
-
const summary = renderOverallSummary({
|
|
8753
|
-
totalTokens: usage?.totalTokens,
|
|
8754
|
-
iterations,
|
|
8755
|
-
elapsedSeconds: progress.getTotalElapsedSeconds(),
|
|
8756
|
-
cost: progress.getTotalCost()
|
|
8757
|
-
});
|
|
8758
|
-
if (summary) {
|
|
8759
|
-
env.stderr.write(`${summary}
|
|
8760
|
-
`);
|
|
8761
|
-
}
|
|
8941
|
+
if ("gadget-start-prefix" in rawObj) {
|
|
8942
|
+
result["gadget-start-prefix"] = validateString(
|
|
8943
|
+
rawObj["gadget-start-prefix"],
|
|
8944
|
+
"gadget-start-prefix",
|
|
8945
|
+
section
|
|
8946
|
+
);
|
|
8762
8947
|
}
|
|
8763
|
-
|
|
8764
|
-
|
|
8765
|
-
|
|
8766
|
-
|
|
8767
|
-
|
|
8768
|
-
|
|
8769
|
-
const mergedOptions = {
|
|
8770
|
-
...options,
|
|
8771
|
-
gadgetApproval: config?.["gadget-approval"]
|
|
8772
|
-
};
|
|
8773
|
-
return executeAgent(prompt, mergedOptions, env);
|
|
8774
|
-
}, env)
|
|
8775
|
-
);
|
|
8776
|
-
}
|
|
8777
|
-
|
|
8778
|
-
// src/cli/complete-command.ts
|
|
8779
|
-
init_messages();
|
|
8780
|
-
init_model_shortcuts();
|
|
8781
|
-
init_constants2();
|
|
8782
|
-
async function executeComplete(promptArg, options, env) {
|
|
8783
|
-
const prompt = await resolvePrompt(promptArg, env);
|
|
8784
|
-
const client = env.createClient();
|
|
8785
|
-
const model = resolveModel(options.model);
|
|
8786
|
-
const builder = new LLMMessageBuilder();
|
|
8787
|
-
if (options.system) {
|
|
8788
|
-
builder.addSystem(options.system);
|
|
8948
|
+
if ("gadget-end-prefix" in rawObj) {
|
|
8949
|
+
result["gadget-end-prefix"] = validateString(
|
|
8950
|
+
rawObj["gadget-end-prefix"],
|
|
8951
|
+
"gadget-end-prefix",
|
|
8952
|
+
section
|
|
8953
|
+
);
|
|
8789
8954
|
}
|
|
8790
|
-
|
|
8791
|
-
|
|
8792
|
-
|
|
8793
|
-
|
|
8794
|
-
|
|
8795
|
-
|
|
8796
|
-
const filename = `${timestamp}_complete.request.txt`;
|
|
8797
|
-
const content = formatLlmRequest(messages);
|
|
8798
|
-
await writeLogFile(llmRequestsDir, filename, content);
|
|
8955
|
+
if ("gadget-arg-prefix" in rawObj) {
|
|
8956
|
+
result["gadget-arg-prefix"] = validateString(
|
|
8957
|
+
rawObj["gadget-arg-prefix"],
|
|
8958
|
+
"gadget-arg-prefix",
|
|
8959
|
+
section
|
|
8960
|
+
);
|
|
8799
8961
|
}
|
|
8800
|
-
|
|
8801
|
-
|
|
8802
|
-
messages,
|
|
8803
|
-
temperature: options.temperature,
|
|
8804
|
-
maxTokens: options.maxTokens
|
|
8805
|
-
});
|
|
8806
|
-
const printer = new StreamPrinter(env.stdout);
|
|
8807
|
-
const stderrTTY = env.stderr.isTTY === true;
|
|
8808
|
-
const progress = new StreamProgress(env.stderr, stderrTTY, client.modelRegistry);
|
|
8809
|
-
const estimatedInputTokens = Math.round(prompt.length / FALLBACK_CHARS_PER_TOKEN);
|
|
8810
|
-
progress.startCall(model, estimatedInputTokens);
|
|
8811
|
-
let finishReason;
|
|
8812
|
-
let usage;
|
|
8813
|
-
let accumulatedResponse = "";
|
|
8814
|
-
for await (const chunk of stream2) {
|
|
8815
|
-
if (chunk.usage) {
|
|
8816
|
-
usage = chunk.usage;
|
|
8817
|
-
if (chunk.usage.inputTokens) {
|
|
8818
|
-
progress.setInputTokens(chunk.usage.inputTokens, false);
|
|
8819
|
-
}
|
|
8820
|
-
if (chunk.usage.outputTokens) {
|
|
8821
|
-
progress.setOutputTokens(chunk.usage.outputTokens, false);
|
|
8822
|
-
}
|
|
8823
|
-
}
|
|
8824
|
-
if (chunk.text) {
|
|
8825
|
-
progress.pause();
|
|
8826
|
-
accumulatedResponse += chunk.text;
|
|
8827
|
-
progress.update(accumulatedResponse.length);
|
|
8828
|
-
printer.write(chunk.text);
|
|
8829
|
-
}
|
|
8830
|
-
if (chunk.finishReason !== void 0) {
|
|
8831
|
-
finishReason = chunk.finishReason;
|
|
8832
|
-
}
|
|
8962
|
+
if ("gadget-approval" in rawObj) {
|
|
8963
|
+
result["gadget-approval"] = validateGadgetApproval(rawObj["gadget-approval"], section);
|
|
8833
8964
|
}
|
|
8834
|
-
|
|
8835
|
-
|
|
8836
|
-
|
|
8837
|
-
|
|
8838
|
-
|
|
8839
|
-
await writeLogFile(llmResponsesDir, filename, accumulatedResponse);
|
|
8965
|
+
if ("max-tokens" in rawObj) {
|
|
8966
|
+
result["max-tokens"] = validateNumber(rawObj["max-tokens"], "max-tokens", section, {
|
|
8967
|
+
integer: true,
|
|
8968
|
+
min: 1
|
|
8969
|
+
});
|
|
8840
8970
|
}
|
|
8841
|
-
if (
|
|
8842
|
-
|
|
8843
|
-
if (summary) {
|
|
8844
|
-
env.stderr.write(`${summary}
|
|
8845
|
-
`);
|
|
8846
|
-
}
|
|
8971
|
+
if ("quiet" in rawObj) {
|
|
8972
|
+
result.quiet = validateBoolean(rawObj.quiet, "quiet", section);
|
|
8847
8973
|
}
|
|
8974
|
+
Object.assign(result, validateLoggingConfig(rawObj, section));
|
|
8975
|
+
return result;
|
|
8848
8976
|
}
|
|
8849
|
-
function
|
|
8850
|
-
|
|
8851
|
-
|
|
8852
|
-
cmd.action(
|
|
8853
|
-
(prompt, options) => executeAction(() => executeComplete(prompt, options, env), env)
|
|
8854
|
-
);
|
|
8855
|
-
}
|
|
8856
|
-
|
|
8857
|
-
// src/cli/config.ts
|
|
8858
|
-
var import_node_fs8 = require("fs");
|
|
8859
|
-
var import_node_os2 = require("os");
|
|
8860
|
-
var import_node_path8 = require("path");
|
|
8861
|
-
var import_js_toml = require("js-toml");
|
|
8862
|
-
|
|
8863
|
-
// src/cli/templates.ts
|
|
8864
|
-
var import_eta = require("eta");
|
|
8865
|
-
var TemplateError = class extends Error {
|
|
8866
|
-
constructor(message, promptName, configPath) {
|
|
8867
|
-
super(promptName ? `[prompts.${promptName}]: ${message}` : message);
|
|
8868
|
-
this.promptName = promptName;
|
|
8869
|
-
this.configPath = configPath;
|
|
8870
|
-
this.name = "TemplateError";
|
|
8977
|
+
function validatePromptsConfig(raw, section) {
|
|
8978
|
+
if (typeof raw !== "object" || raw === null) {
|
|
8979
|
+
throw new ConfigError(`[${section}] must be a table`);
|
|
8871
8980
|
}
|
|
8872
|
-
};
|
|
8873
|
-
|
|
8874
|
-
|
|
8875
|
-
|
|
8876
|
-
|
|
8877
|
-
|
|
8878
|
-
|
|
8879
|
-
|
|
8880
|
-
|
|
8881
|
-
|
|
8882
|
-
|
|
8981
|
+
const result = {};
|
|
8982
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
8983
|
+
if (typeof value !== "string") {
|
|
8984
|
+
throw new ConfigError(`[${section}].${key} must be a string`);
|
|
8985
|
+
}
|
|
8986
|
+
result[key] = value;
|
|
8987
|
+
}
|
|
8988
|
+
return result;
|
|
8989
|
+
}
|
|
8990
|
+
function validateConfig(raw, configPath) {
|
|
8991
|
+
if (typeof raw !== "object" || raw === null) {
|
|
8992
|
+
throw new ConfigError("Config must be a TOML table", configPath);
|
|
8993
|
+
}
|
|
8994
|
+
const rawObj = raw;
|
|
8995
|
+
const result = {};
|
|
8996
|
+
for (const [key, value] of Object.entries(rawObj)) {
|
|
8883
8997
|
try {
|
|
8884
|
-
|
|
8998
|
+
if (key === "global") {
|
|
8999
|
+
result.global = validateGlobalConfig(value, key);
|
|
9000
|
+
} else if (key === "complete") {
|
|
9001
|
+
result.complete = validateCompleteConfig(value, key);
|
|
9002
|
+
} else if (key === "agent") {
|
|
9003
|
+
result.agent = validateAgentConfig(value, key);
|
|
9004
|
+
} else if (key === "prompts") {
|
|
9005
|
+
result.prompts = validatePromptsConfig(value, key);
|
|
9006
|
+
} else if (key === "docker") {
|
|
9007
|
+
result.docker = validateDockerConfig(value, key);
|
|
9008
|
+
} else {
|
|
9009
|
+
result[key] = validateCustomConfig(value, key);
|
|
9010
|
+
}
|
|
8885
9011
|
} catch (error) {
|
|
8886
|
-
|
|
8887
|
-
|
|
8888
|
-
|
|
8889
|
-
|
|
8890
|
-
);
|
|
9012
|
+
if (error instanceof ConfigError) {
|
|
9013
|
+
throw new ConfigError(error.message, configPath);
|
|
9014
|
+
}
|
|
9015
|
+
throw error;
|
|
8891
9016
|
}
|
|
8892
9017
|
}
|
|
8893
|
-
return
|
|
9018
|
+
return result;
|
|
8894
9019
|
}
|
|
8895
|
-
function
|
|
9020
|
+
function loadConfig() {
|
|
9021
|
+
const configPath = getConfigPath();
|
|
9022
|
+
if (!(0, import_node_fs8.existsSync)(configPath)) {
|
|
9023
|
+
return {};
|
|
9024
|
+
}
|
|
9025
|
+
let content;
|
|
8896
9026
|
try {
|
|
8897
|
-
|
|
8898
|
-
...context,
|
|
8899
|
-
env: process.env,
|
|
8900
|
-
date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
|
|
8901
|
-
// "2025-12-01"
|
|
8902
|
-
};
|
|
8903
|
-
return eta.renderString(template, fullContext);
|
|
9027
|
+
content = (0, import_node_fs8.readFileSync)(configPath, "utf-8");
|
|
8904
9028
|
} catch (error) {
|
|
8905
|
-
throw new
|
|
8906
|
-
error instanceof Error ? error.message :
|
|
8907
|
-
|
|
9029
|
+
throw new ConfigError(
|
|
9030
|
+
`Failed to read config file: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
9031
|
+
configPath
|
|
9032
|
+
);
|
|
9033
|
+
}
|
|
9034
|
+
let raw;
|
|
9035
|
+
try {
|
|
9036
|
+
raw = (0, import_js_toml.load)(content);
|
|
9037
|
+
} catch (error) {
|
|
9038
|
+
throw new ConfigError(
|
|
9039
|
+
`Invalid TOML syntax: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
8908
9040
|
configPath
|
|
8909
9041
|
);
|
|
8910
9042
|
}
|
|
9043
|
+
const validated = validateConfig(raw, configPath);
|
|
9044
|
+
const inherited = resolveInheritance(validated, configPath);
|
|
9045
|
+
return resolveTemplatesInConfig(inherited, configPath);
|
|
8911
9046
|
}
|
|
8912
|
-
function
|
|
8913
|
-
const
|
|
9047
|
+
function getCustomCommandNames(config) {
|
|
9048
|
+
const reserved = /* @__PURE__ */ new Set(["global", "complete", "agent", "prompts", "docker"]);
|
|
9049
|
+
return Object.keys(config).filter((key) => !reserved.has(key));
|
|
9050
|
+
}
|
|
9051
|
+
function resolveTemplatesInConfig(config, configPath) {
|
|
9052
|
+
const prompts = config.prompts ?? {};
|
|
9053
|
+
const hasPrompts = Object.keys(prompts).length > 0;
|
|
9054
|
+
let hasTemplates = false;
|
|
9055
|
+
for (const [sectionName, section] of Object.entries(config)) {
|
|
9056
|
+
if (sectionName === "global" || sectionName === "prompts") continue;
|
|
9057
|
+
if (!section || typeof section !== "object") continue;
|
|
9058
|
+
const sectionObj = section;
|
|
9059
|
+
if (typeof sectionObj.system === "string" && hasTemplateSyntax(sectionObj.system)) {
|
|
9060
|
+
hasTemplates = true;
|
|
9061
|
+
break;
|
|
9062
|
+
}
|
|
9063
|
+
}
|
|
9064
|
+
for (const template of Object.values(prompts)) {
|
|
9065
|
+
if (hasTemplateSyntax(template)) {
|
|
9066
|
+
hasTemplates = true;
|
|
9067
|
+
break;
|
|
9068
|
+
}
|
|
9069
|
+
}
|
|
9070
|
+
if (!hasPrompts && !hasTemplates) {
|
|
9071
|
+
return config;
|
|
9072
|
+
}
|
|
9073
|
+
try {
|
|
9074
|
+
validatePrompts(prompts, configPath);
|
|
9075
|
+
} catch (error) {
|
|
9076
|
+
if (error instanceof TemplateError) {
|
|
9077
|
+
throw new ConfigError(error.message, configPath);
|
|
9078
|
+
}
|
|
9079
|
+
throw error;
|
|
9080
|
+
}
|
|
8914
9081
|
for (const [name, template] of Object.entries(prompts)) {
|
|
8915
9082
|
try {
|
|
8916
|
-
|
|
9083
|
+
validateEnvVars(template, name, configPath);
|
|
8917
9084
|
} catch (error) {
|
|
8918
|
-
|
|
8919
|
-
|
|
8920
|
-
|
|
8921
|
-
|
|
8922
|
-
);
|
|
9085
|
+
if (error instanceof TemplateError) {
|
|
9086
|
+
throw new ConfigError(error.message, configPath);
|
|
9087
|
+
}
|
|
9088
|
+
throw error;
|
|
8923
9089
|
}
|
|
8924
9090
|
}
|
|
8925
|
-
|
|
8926
|
-
|
|
8927
|
-
const
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
const
|
|
8931
|
-
if (
|
|
8932
|
-
|
|
8933
|
-
|
|
8934
|
-
|
|
8935
|
-
|
|
8936
|
-
|
|
9091
|
+
const eta = createTemplateEngine(prompts, configPath);
|
|
9092
|
+
const result = { ...config };
|
|
9093
|
+
for (const [sectionName, section] of Object.entries(config)) {
|
|
9094
|
+
if (sectionName === "global" || sectionName === "prompts") continue;
|
|
9095
|
+
if (!section || typeof section !== "object") continue;
|
|
9096
|
+
const sectionObj = section;
|
|
9097
|
+
if (typeof sectionObj.system === "string" && hasTemplateSyntax(sectionObj.system)) {
|
|
9098
|
+
try {
|
|
9099
|
+
validateEnvVars(sectionObj.system, void 0, configPath);
|
|
9100
|
+
} catch (error) {
|
|
9101
|
+
if (error instanceof TemplateError) {
|
|
9102
|
+
throw new ConfigError(`[${sectionName}].system: ${error.message}`, configPath);
|
|
9103
|
+
}
|
|
9104
|
+
throw error;
|
|
9105
|
+
}
|
|
9106
|
+
try {
|
|
9107
|
+
const resolved = resolveTemplate(eta, sectionObj.system, {}, configPath);
|
|
9108
|
+
result[sectionName] = {
|
|
9109
|
+
...sectionObj,
|
|
9110
|
+
system: resolved
|
|
9111
|
+
};
|
|
9112
|
+
} catch (error) {
|
|
9113
|
+
if (error instanceof TemplateError) {
|
|
9114
|
+
throw new ConfigError(`[${sectionName}].system: ${error.message}`, configPath);
|
|
9115
|
+
}
|
|
9116
|
+
throw error;
|
|
9117
|
+
}
|
|
8937
9118
|
}
|
|
8938
9119
|
}
|
|
9120
|
+
return result;
|
|
8939
9121
|
}
|
|
8940
|
-
function
|
|
8941
|
-
|
|
8942
|
-
|
|
8943
|
-
|
|
8944
|
-
|
|
8945
|
-
|
|
8946
|
-
|
|
8947
|
-
|
|
8948
|
-
|
|
8949
|
-
|
|
8950
|
-
|
|
8951
|
-
|
|
8952
|
-
|
|
8953
|
-
|
|
8954
|
-
|
|
8955
|
-
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
|
|
8959
|
-
|
|
8960
|
-
|
|
8961
|
-
|
|
8962
|
-
]
|
|
8963
|
-
|
|
8964
|
-
|
|
8965
|
-
|
|
8966
|
-
|
|
8967
|
-
|
|
8968
|
-
|
|
8969
|
-
|
|
8970
|
-
|
|
8971
|
-
|
|
8972
|
-
"gadget-remove",
|
|
8973
|
-
// Remove from inherited gadgets
|
|
8974
|
-
"gadget",
|
|
8975
|
-
// DEPRECATED: alias for gadgets
|
|
8976
|
-
"builtins",
|
|
8977
|
-
"builtin-interaction",
|
|
8978
|
-
"gadget-start-prefix",
|
|
8979
|
-
"gadget-end-prefix",
|
|
8980
|
-
"gadget-arg-prefix",
|
|
8981
|
-
"gadget-approval",
|
|
8982
|
-
"quiet",
|
|
8983
|
-
"inherits",
|
|
8984
|
-
"log-level",
|
|
8985
|
-
"log-file",
|
|
8986
|
-
"log-reset",
|
|
8987
|
-
"log-llm-requests",
|
|
8988
|
-
"log-llm-responses",
|
|
8989
|
-
"type"
|
|
8990
|
-
// Allowed for inheritance compatibility, ignored for built-in commands
|
|
8991
|
-
]);
|
|
8992
|
-
var CUSTOM_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
8993
|
-
...COMPLETE_CONFIG_KEYS,
|
|
8994
|
-
...AGENT_CONFIG_KEYS,
|
|
8995
|
-
"type",
|
|
8996
|
-
"description"
|
|
8997
|
-
]);
|
|
8998
|
-
function getConfigPath() {
|
|
8999
|
-
return (0, import_node_path8.join)((0, import_node_os2.homedir)(), ".llmist", "cli.toml");
|
|
9122
|
+
function resolveGadgets(section, inheritedGadgets, sectionName, configPath) {
|
|
9123
|
+
const hasGadgets = "gadgets" in section;
|
|
9124
|
+
const hasGadgetLegacy = "gadget" in section;
|
|
9125
|
+
const hasGadgetAdd = "gadget-add" in section;
|
|
9126
|
+
const hasGadgetRemove = "gadget-remove" in section;
|
|
9127
|
+
if (hasGadgetLegacy && !hasGadgets) {
|
|
9128
|
+
console.warn(
|
|
9129
|
+
`[config] Warning: [${sectionName}].gadget is deprecated, use 'gadgets' (plural) instead`
|
|
9130
|
+
);
|
|
9131
|
+
}
|
|
9132
|
+
if ((hasGadgets || hasGadgetLegacy) && (hasGadgetAdd || hasGadgetRemove)) {
|
|
9133
|
+
throw new ConfigError(
|
|
9134
|
+
`[${sectionName}] Cannot use 'gadgets' with 'gadget-add'/'gadget-remove'. Use either full replacement (gadgets) OR modification (gadget-add/gadget-remove).`,
|
|
9135
|
+
configPath
|
|
9136
|
+
);
|
|
9137
|
+
}
|
|
9138
|
+
if (hasGadgets) {
|
|
9139
|
+
return section.gadgets;
|
|
9140
|
+
}
|
|
9141
|
+
if (hasGadgetLegacy) {
|
|
9142
|
+
return section.gadget;
|
|
9143
|
+
}
|
|
9144
|
+
let result = [...inheritedGadgets];
|
|
9145
|
+
if (hasGadgetRemove) {
|
|
9146
|
+
const toRemove = new Set(section["gadget-remove"]);
|
|
9147
|
+
result = result.filter((g) => !toRemove.has(g));
|
|
9148
|
+
}
|
|
9149
|
+
if (hasGadgetAdd) {
|
|
9150
|
+
const toAdd = section["gadget-add"];
|
|
9151
|
+
result.push(...toAdd);
|
|
9152
|
+
}
|
|
9153
|
+
return result;
|
|
9000
9154
|
}
|
|
9001
|
-
|
|
9002
|
-
|
|
9003
|
-
|
|
9004
|
-
|
|
9005
|
-
|
|
9155
|
+
function resolveInheritance(config, configPath) {
|
|
9156
|
+
const resolved = {};
|
|
9157
|
+
const resolving = /* @__PURE__ */ new Set();
|
|
9158
|
+
function resolveSection(name) {
|
|
9159
|
+
if (name in resolved) {
|
|
9160
|
+
return resolved[name];
|
|
9161
|
+
}
|
|
9162
|
+
if (resolving.has(name)) {
|
|
9163
|
+
throw new ConfigError(`Circular inheritance detected: ${name}`, configPath);
|
|
9164
|
+
}
|
|
9165
|
+
const section = config[name];
|
|
9166
|
+
if (section === void 0 || typeof section !== "object") {
|
|
9167
|
+
throw new ConfigError(`Cannot inherit from unknown section: ${name}`, configPath);
|
|
9168
|
+
}
|
|
9169
|
+
resolving.add(name);
|
|
9170
|
+
const sectionObj = section;
|
|
9171
|
+
const inheritsRaw = sectionObj.inherits;
|
|
9172
|
+
const inheritsList = inheritsRaw ? Array.isArray(inheritsRaw) ? inheritsRaw : [inheritsRaw] : [];
|
|
9173
|
+
let merged = {};
|
|
9174
|
+
for (const parent of inheritsList) {
|
|
9175
|
+
const parentResolved = resolveSection(parent);
|
|
9176
|
+
merged = { ...merged, ...parentResolved };
|
|
9177
|
+
}
|
|
9178
|
+
const inheritedGadgets = merged.gadgets ?? [];
|
|
9179
|
+
const {
|
|
9180
|
+
inherits: _inherits,
|
|
9181
|
+
gadgets: _gadgets,
|
|
9182
|
+
gadget: _gadget,
|
|
9183
|
+
"gadget-add": _gadgetAdd,
|
|
9184
|
+
"gadget-remove": _gadgetRemove,
|
|
9185
|
+
...ownValues
|
|
9186
|
+
} = sectionObj;
|
|
9187
|
+
merged = { ...merged, ...ownValues };
|
|
9188
|
+
const resolvedGadgets = resolveGadgets(sectionObj, inheritedGadgets, name, configPath);
|
|
9189
|
+
if (resolvedGadgets.length > 0) {
|
|
9190
|
+
merged.gadgets = resolvedGadgets;
|
|
9191
|
+
}
|
|
9192
|
+
delete merged["gadget"];
|
|
9193
|
+
delete merged["gadget-add"];
|
|
9194
|
+
delete merged["gadget-remove"];
|
|
9195
|
+
resolving.delete(name);
|
|
9196
|
+
resolved[name] = merged;
|
|
9197
|
+
return merged;
|
|
9006
9198
|
}
|
|
9007
|
-
|
|
9008
|
-
|
|
9009
|
-
if (typeof value !== "string") {
|
|
9010
|
-
throw new ConfigError(`[${section}].${key} must be a string`);
|
|
9199
|
+
for (const name of Object.keys(config)) {
|
|
9200
|
+
resolveSection(name);
|
|
9011
9201
|
}
|
|
9012
|
-
return
|
|
9202
|
+
return resolved;
|
|
9013
9203
|
}
|
|
9014
|
-
|
|
9015
|
-
|
|
9016
|
-
|
|
9017
|
-
|
|
9018
|
-
if (
|
|
9019
|
-
throw new ConfigError(`[${section}].${key} must be
|
|
9020
|
-
}
|
|
9021
|
-
if (opts?.min !== void 0 && value < opts.min) {
|
|
9022
|
-
throw new ConfigError(`[${section}].${key} must be >= ${opts.min}`);
|
|
9023
|
-
}
|
|
9024
|
-
if (opts?.max !== void 0 && value > opts.max) {
|
|
9025
|
-
throw new ConfigError(`[${section}].${key} must be <= ${opts.max}`);
|
|
9204
|
+
|
|
9205
|
+
// src/cli/docker/docker-config.ts
|
|
9206
|
+
var MOUNT_CONFIG_KEYS = /* @__PURE__ */ new Set(["source", "target", "permission"]);
|
|
9207
|
+
function validateString2(value, key, section) {
|
|
9208
|
+
if (typeof value !== "string") {
|
|
9209
|
+
throw new ConfigError(`[${section}].${key} must be a string`);
|
|
9026
9210
|
}
|
|
9027
9211
|
return value;
|
|
9028
9212
|
}
|
|
9029
|
-
function
|
|
9213
|
+
function validateBoolean2(value, key, section) {
|
|
9030
9214
|
if (typeof value !== "boolean") {
|
|
9031
9215
|
throw new ConfigError(`[${section}].${key} must be a boolean`);
|
|
9032
9216
|
}
|
|
9033
9217
|
return value;
|
|
9034
9218
|
}
|
|
9035
|
-
function
|
|
9219
|
+
function validateStringArray2(value, key, section) {
|
|
9036
9220
|
if (!Array.isArray(value)) {
|
|
9037
9221
|
throw new ConfigError(`[${section}].${key} must be an array`);
|
|
9038
9222
|
}
|
|
@@ -9043,535 +9227,949 @@ function validateStringArray(value, key, section) {
|
|
|
9043
9227
|
}
|
|
9044
9228
|
return value;
|
|
9045
9229
|
}
|
|
9046
|
-
function
|
|
9047
|
-
|
|
9048
|
-
|
|
9049
|
-
|
|
9050
|
-
|
|
9051
|
-
|
|
9052
|
-
if (typeof value[i] !== "string") {
|
|
9053
|
-
throw new ConfigError(`[${section}].inherits[${i}] must be a string`);
|
|
9054
|
-
}
|
|
9055
|
-
}
|
|
9056
|
-
return value;
|
|
9230
|
+
function validateMountPermission(value, key, section) {
|
|
9231
|
+
const str = validateString2(value, key, section);
|
|
9232
|
+
if (!VALID_MOUNT_PERMISSIONS.includes(str)) {
|
|
9233
|
+
throw new ConfigError(
|
|
9234
|
+
`[${section}].${key} must be one of: ${VALID_MOUNT_PERMISSIONS.join(", ")}`
|
|
9235
|
+
);
|
|
9057
9236
|
}
|
|
9058
|
-
|
|
9237
|
+
return str;
|
|
9059
9238
|
}
|
|
9060
|
-
function
|
|
9239
|
+
function validateMountConfig(value, index, section) {
|
|
9061
9240
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
9062
|
-
throw new ConfigError(
|
|
9063
|
-
`[${section}].gadget-approval must be a table (e.g., { WriteFile = "approval-required" })`
|
|
9064
|
-
);
|
|
9241
|
+
throw new ConfigError(`[${section}].mounts[${index}] must be a table`);
|
|
9065
9242
|
}
|
|
9066
|
-
const
|
|
9067
|
-
|
|
9068
|
-
|
|
9069
|
-
|
|
9070
|
-
|
|
9071
|
-
);
|
|
9072
|
-
}
|
|
9073
|
-
if (!VALID_APPROVAL_MODES.includes(mode)) {
|
|
9074
|
-
throw new ConfigError(
|
|
9075
|
-
`[${section}].gadget-approval.${gadgetName} must be one of: ${VALID_APPROVAL_MODES.join(", ")}`
|
|
9076
|
-
);
|
|
9243
|
+
const rawObj = value;
|
|
9244
|
+
const mountSection = `${section}.mounts[${index}]`;
|
|
9245
|
+
for (const key of Object.keys(rawObj)) {
|
|
9246
|
+
if (!MOUNT_CONFIG_KEYS.has(key)) {
|
|
9247
|
+
throw new ConfigError(`[${mountSection}].${key} is not a valid mount option`);
|
|
9077
9248
|
}
|
|
9078
|
-
result[gadgetName] = mode;
|
|
9079
9249
|
}
|
|
9080
|
-
|
|
9081
|
-
}
|
|
9082
|
-
function validateLoggingConfig(raw, section) {
|
|
9083
|
-
const result = {};
|
|
9084
|
-
if ("log-level" in raw) {
|
|
9085
|
-
const level = validateString(raw["log-level"], "log-level", section);
|
|
9086
|
-
if (!VALID_LOG_LEVELS.includes(level)) {
|
|
9087
|
-
throw new ConfigError(
|
|
9088
|
-
`[${section}].log-level must be one of: ${VALID_LOG_LEVELS.join(", ")}`
|
|
9089
|
-
);
|
|
9090
|
-
}
|
|
9091
|
-
result["log-level"] = level;
|
|
9250
|
+
if (!("source" in rawObj)) {
|
|
9251
|
+
throw new ConfigError(`[${mountSection}] missing required field 'source'`);
|
|
9092
9252
|
}
|
|
9093
|
-
if ("
|
|
9094
|
-
|
|
9253
|
+
if (!("target" in rawObj)) {
|
|
9254
|
+
throw new ConfigError(`[${mountSection}] missing required field 'target'`);
|
|
9095
9255
|
}
|
|
9096
|
-
if ("
|
|
9097
|
-
|
|
9256
|
+
if (!("permission" in rawObj)) {
|
|
9257
|
+
throw new ConfigError(`[${mountSection}] missing required field 'permission'`);
|
|
9098
9258
|
}
|
|
9099
|
-
return
|
|
9259
|
+
return {
|
|
9260
|
+
source: validateString2(rawObj.source, "source", mountSection),
|
|
9261
|
+
target: validateString2(rawObj.target, "target", mountSection),
|
|
9262
|
+
permission: validateMountPermission(rawObj.permission, "permission", mountSection)
|
|
9263
|
+
};
|
|
9100
9264
|
}
|
|
9101
|
-
function
|
|
9102
|
-
|
|
9103
|
-
|
|
9104
|
-
result.model = validateString(raw.model, "model", section);
|
|
9105
|
-
}
|
|
9106
|
-
if ("system" in raw) {
|
|
9107
|
-
result.system = validateString(raw.system, "system", section);
|
|
9108
|
-
}
|
|
9109
|
-
if ("temperature" in raw) {
|
|
9110
|
-
result.temperature = validateNumber(raw.temperature, "temperature", section, {
|
|
9111
|
-
min: 0,
|
|
9112
|
-
max: 2
|
|
9113
|
-
});
|
|
9265
|
+
function validateMountsArray(value, section) {
|
|
9266
|
+
if (!Array.isArray(value)) {
|
|
9267
|
+
throw new ConfigError(`[${section}].mounts must be an array of tables`);
|
|
9114
9268
|
}
|
|
9115
|
-
|
|
9116
|
-
|
|
9269
|
+
const result = [];
|
|
9270
|
+
for (let i = 0; i < value.length; i++) {
|
|
9271
|
+
result.push(validateMountConfig(value[i], i, section));
|
|
9117
9272
|
}
|
|
9118
9273
|
return result;
|
|
9119
9274
|
}
|
|
9120
|
-
function
|
|
9121
|
-
if (typeof raw !== "object" || raw === null) {
|
|
9122
|
-
throw new ConfigError(`[${section}] must be a table`);
|
|
9123
|
-
}
|
|
9124
|
-
const rawObj = raw;
|
|
9125
|
-
for (const key of Object.keys(rawObj)) {
|
|
9126
|
-
if (!GLOBAL_CONFIG_KEYS.has(key)) {
|
|
9127
|
-
throw new ConfigError(`[${section}].${key} is not a valid option`);
|
|
9128
|
-
}
|
|
9129
|
-
}
|
|
9130
|
-
return validateLoggingConfig(rawObj, section);
|
|
9131
|
-
}
|
|
9132
|
-
function validateCompleteConfig(raw, section) {
|
|
9275
|
+
function validateDockerConfig(raw, section) {
|
|
9133
9276
|
if (typeof raw !== "object" || raw === null) {
|
|
9134
9277
|
throw new ConfigError(`[${section}] must be a table`);
|
|
9135
9278
|
}
|
|
9136
9279
|
const rawObj = raw;
|
|
9137
9280
|
for (const key of Object.keys(rawObj)) {
|
|
9138
|
-
if (!
|
|
9281
|
+
if (!DOCKER_CONFIG_KEYS.has(key)) {
|
|
9139
9282
|
throw new ConfigError(`[${section}].${key} is not a valid option`);
|
|
9140
9283
|
}
|
|
9141
9284
|
}
|
|
9142
|
-
const result = {
|
|
9143
|
-
|
|
9144
|
-
|
|
9145
|
-
};
|
|
9146
|
-
if ("max-tokens" in rawObj) {
|
|
9147
|
-
result["max-tokens"] = validateNumber(rawObj["max-tokens"], "max-tokens", section, {
|
|
9148
|
-
integer: true,
|
|
9149
|
-
min: 1
|
|
9150
|
-
});
|
|
9285
|
+
const result = {};
|
|
9286
|
+
if ("enabled" in rawObj) {
|
|
9287
|
+
result.enabled = validateBoolean2(rawObj.enabled, "enabled", section);
|
|
9151
9288
|
}
|
|
9152
|
-
if ("
|
|
9153
|
-
result.
|
|
9289
|
+
if ("dockerfile" in rawObj) {
|
|
9290
|
+
result.dockerfile = validateString2(rawObj.dockerfile, "dockerfile", section);
|
|
9154
9291
|
}
|
|
9155
|
-
if ("
|
|
9156
|
-
result["
|
|
9157
|
-
rawObj["
|
|
9158
|
-
"
|
|
9292
|
+
if ("cwd-permission" in rawObj) {
|
|
9293
|
+
result["cwd-permission"] = validateMountPermission(
|
|
9294
|
+
rawObj["cwd-permission"],
|
|
9295
|
+
"cwd-permission",
|
|
9159
9296
|
section
|
|
9160
9297
|
);
|
|
9161
9298
|
}
|
|
9162
|
-
if ("
|
|
9163
|
-
result["
|
|
9164
|
-
rawObj["
|
|
9165
|
-
"
|
|
9299
|
+
if ("config-permission" in rawObj) {
|
|
9300
|
+
result["config-permission"] = validateMountPermission(
|
|
9301
|
+
rawObj["config-permission"],
|
|
9302
|
+
"config-permission",
|
|
9166
9303
|
section
|
|
9167
9304
|
);
|
|
9168
9305
|
}
|
|
9169
|
-
|
|
9170
|
-
|
|
9171
|
-
function validateAgentConfig(raw, section) {
|
|
9172
|
-
if (typeof raw !== "object" || raw === null) {
|
|
9173
|
-
throw new ConfigError(`[${section}] must be a table`);
|
|
9174
|
-
}
|
|
9175
|
-
const rawObj = raw;
|
|
9176
|
-
for (const key of Object.keys(rawObj)) {
|
|
9177
|
-
if (!AGENT_CONFIG_KEYS.has(key)) {
|
|
9178
|
-
throw new ConfigError(`[${section}].${key} is not a valid option`);
|
|
9179
|
-
}
|
|
9180
|
-
}
|
|
9181
|
-
const result = {
|
|
9182
|
-
...validateBaseConfig(rawObj, section),
|
|
9183
|
-
...validateLoggingConfig(rawObj, section)
|
|
9184
|
-
};
|
|
9185
|
-
if ("max-iterations" in rawObj) {
|
|
9186
|
-
result["max-iterations"] = validateNumber(rawObj["max-iterations"], "max-iterations", section, {
|
|
9187
|
-
integer: true,
|
|
9188
|
-
min: 1
|
|
9189
|
-
});
|
|
9190
|
-
}
|
|
9191
|
-
if ("gadgets" in rawObj) {
|
|
9192
|
-
result.gadgets = validateStringArray(rawObj.gadgets, "gadgets", section);
|
|
9193
|
-
}
|
|
9194
|
-
if ("gadget-add" in rawObj) {
|
|
9195
|
-
result["gadget-add"] = validateStringArray(rawObj["gadget-add"], "gadget-add", section);
|
|
9306
|
+
if ("mounts" in rawObj) {
|
|
9307
|
+
result.mounts = validateMountsArray(rawObj.mounts, section);
|
|
9196
9308
|
}
|
|
9197
|
-
if ("
|
|
9198
|
-
result["
|
|
9309
|
+
if ("env-vars" in rawObj) {
|
|
9310
|
+
result["env-vars"] = validateStringArray2(rawObj["env-vars"], "env-vars", section);
|
|
9199
9311
|
}
|
|
9200
|
-
if ("
|
|
9201
|
-
result
|
|
9312
|
+
if ("image-name" in rawObj) {
|
|
9313
|
+
result["image-name"] = validateString2(rawObj["image-name"], "image-name", section);
|
|
9202
9314
|
}
|
|
9203
|
-
if ("
|
|
9204
|
-
result
|
|
9315
|
+
if ("dev-mode" in rawObj) {
|
|
9316
|
+
result["dev-mode"] = validateBoolean2(rawObj["dev-mode"], "dev-mode", section);
|
|
9205
9317
|
}
|
|
9206
|
-
if ("
|
|
9207
|
-
result["
|
|
9208
|
-
rawObj["builtin-interaction"],
|
|
9209
|
-
"builtin-interaction",
|
|
9210
|
-
section
|
|
9211
|
-
);
|
|
9318
|
+
if ("dev-source" in rawObj) {
|
|
9319
|
+
result["dev-source"] = validateString2(rawObj["dev-source"], "dev-source", section);
|
|
9212
9320
|
}
|
|
9213
|
-
|
|
9214
|
-
|
|
9215
|
-
|
|
9216
|
-
|
|
9217
|
-
|
|
9218
|
-
|
|
9321
|
+
return result;
|
|
9322
|
+
}
|
|
9323
|
+
|
|
9324
|
+
// src/cli/docker/dockerfile.ts
|
|
9325
|
+
var DEFAULT_DOCKERFILE = `# llmist sandbox image
|
|
9326
|
+
# Auto-generated - customize via [docker].dockerfile in cli.toml
|
|
9327
|
+
|
|
9328
|
+
FROM oven/bun:1-debian
|
|
9329
|
+
|
|
9330
|
+
# Install essential tools
|
|
9331
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \\
|
|
9332
|
+
# ripgrep for fast file searching
|
|
9333
|
+
ripgrep \\
|
|
9334
|
+
# git for version control operations
|
|
9335
|
+
git \\
|
|
9336
|
+
# curl for downloads and API calls
|
|
9337
|
+
curl \\
|
|
9338
|
+
# ca-certificates for HTTPS
|
|
9339
|
+
ca-certificates \\
|
|
9340
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
9341
|
+
|
|
9342
|
+
# Install ast-grep for code search/refactoring
|
|
9343
|
+
# Using the official install script
|
|
9344
|
+
RUN curl -fsSL https://raw.githubusercontent.com/ast-grep/ast-grep/main/install.sh | bash \\
|
|
9345
|
+
&& mv /root/.local/bin/ast-grep /usr/local/bin/ 2>/dev/null || true \\
|
|
9346
|
+
&& mv /root/.local/bin/sg /usr/local/bin/ 2>/dev/null || true
|
|
9347
|
+
|
|
9348
|
+
# Install llmist globally via bun
|
|
9349
|
+
RUN bun add -g llmist
|
|
9350
|
+
|
|
9351
|
+
# Working directory (host CWD will be mounted here)
|
|
9352
|
+
WORKDIR /workspace
|
|
9353
|
+
|
|
9354
|
+
# Entry point - llmist with all arguments forwarded
|
|
9355
|
+
ENTRYPOINT ["llmist"]
|
|
9356
|
+
`;
|
|
9357
|
+
var DEV_DOCKERFILE = `# llmist DEV sandbox image
|
|
9358
|
+
# For development/testing with local source code
|
|
9359
|
+
|
|
9360
|
+
FROM oven/bun:1-debian
|
|
9361
|
+
|
|
9362
|
+
# Install essential tools (same as production)
|
|
9363
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \\
|
|
9364
|
+
ripgrep \\
|
|
9365
|
+
git \\
|
|
9366
|
+
curl \\
|
|
9367
|
+
ca-certificates \\
|
|
9368
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
9369
|
+
|
|
9370
|
+
# Install ast-grep for code search/refactoring
|
|
9371
|
+
RUN curl -fsSL https://raw.githubusercontent.com/ast-grep/ast-grep/main/install.sh | bash \\
|
|
9372
|
+
&& mv /root/.local/bin/ast-grep /usr/local/bin/ 2>/dev/null || true \\
|
|
9373
|
+
&& mv /root/.local/bin/sg /usr/local/bin/ 2>/dev/null || true
|
|
9374
|
+
|
|
9375
|
+
# Working directory (host CWD will be mounted here)
|
|
9376
|
+
WORKDIR /workspace
|
|
9377
|
+
|
|
9378
|
+
# Entry point - run llmist from mounted source
|
|
9379
|
+
# Source is mounted at ${DEV_SOURCE_MOUNT_TARGET}
|
|
9380
|
+
ENTRYPOINT ["bun", "run", "${DEV_SOURCE_MOUNT_TARGET}/src/cli.ts"]
|
|
9381
|
+
`;
|
|
9382
|
+
function resolveDockerfile(config, devMode = false) {
|
|
9383
|
+
if (config.dockerfile) {
|
|
9384
|
+
return config.dockerfile;
|
|
9219
9385
|
}
|
|
9220
|
-
|
|
9221
|
-
|
|
9222
|
-
|
|
9223
|
-
|
|
9224
|
-
|
|
9225
|
-
|
|
9386
|
+
return devMode ? DEV_DOCKERFILE : DEFAULT_DOCKERFILE;
|
|
9387
|
+
}
|
|
9388
|
+
function computeDockerfileHash(dockerfile) {
|
|
9389
|
+
const encoder = new TextEncoder();
|
|
9390
|
+
const data = encoder.encode(dockerfile);
|
|
9391
|
+
return Bun.hash(data).toString(16);
|
|
9392
|
+
}
|
|
9393
|
+
|
|
9394
|
+
// src/cli/docker/image-manager.ts
|
|
9395
|
+
var import_node_fs9 = require("fs");
|
|
9396
|
+
var import_node_os3 = require("os");
|
|
9397
|
+
var import_node_path9 = require("path");
|
|
9398
|
+
var CACHE_DIR = (0, import_node_path9.join)((0, import_node_os3.homedir)(), ".llmist", "docker-cache");
|
|
9399
|
+
var HASH_FILE = "image-hash.json";
|
|
9400
|
+
function ensureCacheDir() {
|
|
9401
|
+
if (!(0, import_node_fs9.existsSync)(CACHE_DIR)) {
|
|
9402
|
+
(0, import_node_fs9.mkdirSync)(CACHE_DIR, { recursive: true });
|
|
9226
9403
|
}
|
|
9227
|
-
|
|
9228
|
-
|
|
9229
|
-
|
|
9230
|
-
|
|
9231
|
-
|
|
9232
|
-
);
|
|
9404
|
+
}
|
|
9405
|
+
function getCachedHash(imageName) {
|
|
9406
|
+
const hashPath = (0, import_node_path9.join)(CACHE_DIR, HASH_FILE);
|
|
9407
|
+
if (!(0, import_node_fs9.existsSync)(hashPath)) {
|
|
9408
|
+
return void 0;
|
|
9233
9409
|
}
|
|
9234
|
-
|
|
9235
|
-
|
|
9410
|
+
try {
|
|
9411
|
+
const content = (0, import_node_fs9.readFileSync)(hashPath, "utf-8");
|
|
9412
|
+
const cache = JSON.parse(content);
|
|
9413
|
+
return cache[imageName]?.dockerfileHash;
|
|
9414
|
+
} catch {
|
|
9415
|
+
return void 0;
|
|
9236
9416
|
}
|
|
9237
|
-
|
|
9238
|
-
|
|
9417
|
+
}
|
|
9418
|
+
function setCachedHash(imageName, hash) {
|
|
9419
|
+
ensureCacheDir();
|
|
9420
|
+
const hashPath = (0, import_node_path9.join)(CACHE_DIR, HASH_FILE);
|
|
9421
|
+
let cache = {};
|
|
9422
|
+
if ((0, import_node_fs9.existsSync)(hashPath)) {
|
|
9423
|
+
try {
|
|
9424
|
+
const content = (0, import_node_fs9.readFileSync)(hashPath, "utf-8");
|
|
9425
|
+
cache = JSON.parse(content);
|
|
9426
|
+
} catch {
|
|
9427
|
+
cache = {};
|
|
9428
|
+
}
|
|
9239
9429
|
}
|
|
9240
|
-
|
|
9241
|
-
|
|
9242
|
-
|
|
9243
|
-
|
|
9244
|
-
|
|
9245
|
-
|
|
9430
|
+
cache[imageName] = {
|
|
9431
|
+
imageName,
|
|
9432
|
+
dockerfileHash: hash,
|
|
9433
|
+
builtAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
9434
|
+
};
|
|
9435
|
+
(0, import_node_fs9.writeFileSync)(hashPath, JSON.stringify(cache, null, 2));
|
|
9436
|
+
}
|
|
9437
|
+
var DockerBuildError = class extends Error {
|
|
9438
|
+
constructor(message, output) {
|
|
9439
|
+
super(message);
|
|
9440
|
+
this.output = output;
|
|
9441
|
+
this.name = "DockerBuildError";
|
|
9246
9442
|
}
|
|
9247
|
-
|
|
9248
|
-
|
|
9249
|
-
|
|
9250
|
-
|
|
9251
|
-
|
|
9443
|
+
};
|
|
9444
|
+
async function buildImage(imageName, dockerfile) {
|
|
9445
|
+
ensureCacheDir();
|
|
9446
|
+
const dockerfilePath = (0, import_node_path9.join)(CACHE_DIR, "Dockerfile");
|
|
9447
|
+
(0, import_node_fs9.writeFileSync)(dockerfilePath, dockerfile);
|
|
9448
|
+
const proc = Bun.spawn(
|
|
9449
|
+
["docker", "build", "-t", imageName, "-f", dockerfilePath, CACHE_DIR],
|
|
9450
|
+
{
|
|
9451
|
+
stdout: "pipe",
|
|
9452
|
+
stderr: "pipe"
|
|
9453
|
+
}
|
|
9454
|
+
);
|
|
9455
|
+
const exitCode = await proc.exited;
|
|
9456
|
+
const stdout = await new Response(proc.stdout).text();
|
|
9457
|
+
const stderr = await new Response(proc.stderr).text();
|
|
9458
|
+
if (exitCode !== 0) {
|
|
9459
|
+
const output = [stdout, stderr].filter(Boolean).join("\n");
|
|
9460
|
+
throw new DockerBuildError(
|
|
9461
|
+
`Docker build failed with exit code ${exitCode}`,
|
|
9462
|
+
output
|
|
9252
9463
|
);
|
|
9253
9464
|
}
|
|
9254
|
-
return result;
|
|
9255
9465
|
}
|
|
9256
|
-
function
|
|
9257
|
-
|
|
9258
|
-
|
|
9466
|
+
async function ensureImage(imageName = DEFAULT_IMAGE_NAME, dockerfile) {
|
|
9467
|
+
const hash = computeDockerfileHash(dockerfile);
|
|
9468
|
+
const cachedHash = getCachedHash(imageName);
|
|
9469
|
+
if (cachedHash === hash) {
|
|
9470
|
+
return imageName;
|
|
9259
9471
|
}
|
|
9260
|
-
|
|
9472
|
+
console.error(`Building Docker image '${imageName}'...`);
|
|
9473
|
+
await buildImage(imageName, dockerfile);
|
|
9474
|
+
setCachedHash(imageName, hash);
|
|
9475
|
+
console.error(`Docker image '${imageName}' built successfully.`);
|
|
9476
|
+
return imageName;
|
|
9261
9477
|
}
|
|
9262
|
-
|
|
9263
|
-
|
|
9264
|
-
|
|
9478
|
+
|
|
9479
|
+
// src/cli/docker/docker-wrapper.ts
|
|
9480
|
+
var import_node_fs10 = require("fs");
|
|
9481
|
+
var import_node_path10 = require("path");
|
|
9482
|
+
var import_node_os4 = require("os");
|
|
9483
|
+
var DockerUnavailableError = class extends Error {
|
|
9484
|
+
constructor() {
|
|
9485
|
+
super(
|
|
9486
|
+
"Docker is required but not available. Install Docker or disable Docker sandboxing in your configuration."
|
|
9487
|
+
);
|
|
9488
|
+
this.name = "DockerUnavailableError";
|
|
9265
9489
|
}
|
|
9266
|
-
|
|
9267
|
-
|
|
9268
|
-
|
|
9269
|
-
|
|
9270
|
-
|
|
9490
|
+
};
|
|
9491
|
+
var DockerSkipError = class extends Error {
|
|
9492
|
+
constructor() {
|
|
9493
|
+
super("Docker execution skipped - already inside container");
|
|
9494
|
+
this.name = "DockerSkipError";
|
|
9271
9495
|
}
|
|
9272
|
-
|
|
9273
|
-
|
|
9274
|
-
|
|
9275
|
-
|
|
9276
|
-
|
|
9496
|
+
};
|
|
9497
|
+
async function checkDockerAvailable() {
|
|
9498
|
+
try {
|
|
9499
|
+
const proc = Bun.spawn(["docker", "info"], {
|
|
9500
|
+
stdout: "pipe",
|
|
9501
|
+
stderr: "pipe"
|
|
9502
|
+
});
|
|
9503
|
+
await proc.exited;
|
|
9504
|
+
return proc.exitCode === 0;
|
|
9505
|
+
} catch {
|
|
9506
|
+
return false;
|
|
9507
|
+
}
|
|
9508
|
+
}
|
|
9509
|
+
function isInsideContainer() {
|
|
9510
|
+
if ((0, import_node_fs10.existsSync)("/.dockerenv")) {
|
|
9511
|
+
return true;
|
|
9512
|
+
}
|
|
9513
|
+
try {
|
|
9514
|
+
const cgroup = (0, import_node_fs10.readFileSync)("/proc/1/cgroup", "utf-8");
|
|
9515
|
+
if (cgroup.includes("docker") || cgroup.includes("containerd")) {
|
|
9516
|
+
return true;
|
|
9277
9517
|
}
|
|
9278
|
-
|
|
9518
|
+
} catch {
|
|
9279
9519
|
}
|
|
9280
|
-
|
|
9281
|
-
|
|
9282
|
-
|
|
9283
|
-
|
|
9284
|
-
if ("
|
|
9285
|
-
|
|
9520
|
+
return false;
|
|
9521
|
+
}
|
|
9522
|
+
function autoDetectDevSource() {
|
|
9523
|
+
const scriptPath = process.argv[1];
|
|
9524
|
+
if (!scriptPath || !scriptPath.endsWith("src/cli.ts")) {
|
|
9525
|
+
return void 0;
|
|
9286
9526
|
}
|
|
9287
|
-
|
|
9288
|
-
|
|
9289
|
-
|
|
9290
|
-
|
|
9291
|
-
|
|
9527
|
+
const srcDir = (0, import_node_path10.dirname)(scriptPath);
|
|
9528
|
+
const projectDir = (0, import_node_path10.dirname)(srcDir);
|
|
9529
|
+
const packageJsonPath = (0, import_node_path10.join)(projectDir, "package.json");
|
|
9530
|
+
if (!(0, import_node_fs10.existsSync)(packageJsonPath)) {
|
|
9531
|
+
return void 0;
|
|
9292
9532
|
}
|
|
9293
|
-
|
|
9294
|
-
|
|
9533
|
+
try {
|
|
9534
|
+
const pkg = JSON.parse((0, import_node_fs10.readFileSync)(packageJsonPath, "utf-8"));
|
|
9535
|
+
if (pkg.name === "llmist") {
|
|
9536
|
+
return projectDir;
|
|
9537
|
+
}
|
|
9538
|
+
} catch {
|
|
9295
9539
|
}
|
|
9296
|
-
|
|
9297
|
-
|
|
9540
|
+
return void 0;
|
|
9541
|
+
}
|
|
9542
|
+
function resolveDevMode(config, cliDevMode) {
|
|
9543
|
+
const enabled = cliDevMode || config?.["dev-mode"] || process.env.LLMIST_DEV_MODE === "1";
|
|
9544
|
+
if (!enabled) {
|
|
9545
|
+
return { enabled: false, sourcePath: void 0 };
|
|
9298
9546
|
}
|
|
9299
|
-
|
|
9300
|
-
|
|
9547
|
+
const sourcePath = config?.["dev-source"] || process.env.LLMIST_DEV_SOURCE || autoDetectDevSource();
|
|
9548
|
+
if (!sourcePath) {
|
|
9549
|
+
throw new Error(
|
|
9550
|
+
"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)."
|
|
9551
|
+
);
|
|
9301
9552
|
}
|
|
9302
|
-
|
|
9303
|
-
|
|
9553
|
+
return { enabled: true, sourcePath };
|
|
9554
|
+
}
|
|
9555
|
+
function expandHome(path5) {
|
|
9556
|
+
if (path5.startsWith("~")) {
|
|
9557
|
+
return path5.replace(/^~/, (0, import_node_os4.homedir)());
|
|
9304
9558
|
}
|
|
9305
|
-
|
|
9306
|
-
|
|
9559
|
+
return path5;
|
|
9560
|
+
}
|
|
9561
|
+
function buildDockerRunArgs(ctx, imageName, devMode) {
|
|
9562
|
+
const args = ["run", "--rm"];
|
|
9563
|
+
const timestamp = Date.now();
|
|
9564
|
+
const random = Math.random().toString(36).slice(2, 8);
|
|
9565
|
+
const containerName = `llmist-${timestamp}-${random}`;
|
|
9566
|
+
args.push("--name", containerName);
|
|
9567
|
+
if (process.stdin.isTTY) {
|
|
9568
|
+
args.push("-it");
|
|
9307
9569
|
}
|
|
9308
|
-
|
|
9309
|
-
|
|
9310
|
-
|
|
9311
|
-
|
|
9312
|
-
|
|
9313
|
-
|
|
9570
|
+
const cwdPermission = ctx.options.dockerRo ? "ro" : ctx.profileCwdPermission ?? ctx.config["cwd-permission"] ?? DEFAULT_CWD_PERMISSION;
|
|
9571
|
+
args.push("-v", `${ctx.cwd}:/workspace:${cwdPermission}`);
|
|
9572
|
+
args.push("-w", "/workspace");
|
|
9573
|
+
const configPermission = ctx.config["config-permission"] ?? DEFAULT_CONFIG_PERMISSION;
|
|
9574
|
+
const llmistDir = expandHome("~/.llmist");
|
|
9575
|
+
args.push("-v", `${llmistDir}:/root/.llmist:${configPermission}`);
|
|
9576
|
+
if (devMode.enabled && devMode.sourcePath) {
|
|
9577
|
+
const expandedSource = expandHome(devMode.sourcePath);
|
|
9578
|
+
args.push("-v", `${expandedSource}:${DEV_SOURCE_MOUNT_TARGET}:ro`);
|
|
9314
9579
|
}
|
|
9315
|
-
if (
|
|
9316
|
-
|
|
9317
|
-
|
|
9318
|
-
"
|
|
9319
|
-
|
|
9320
|
-
);
|
|
9580
|
+
if (ctx.config.mounts) {
|
|
9581
|
+
for (const mount of ctx.config.mounts) {
|
|
9582
|
+
const source = expandHome(mount.source);
|
|
9583
|
+
args.push("-v", `${source}:${mount.target}:${mount.permission}`);
|
|
9584
|
+
}
|
|
9321
9585
|
}
|
|
9322
|
-
|
|
9323
|
-
|
|
9324
|
-
|
|
9325
|
-
|
|
9326
|
-
section
|
|
9327
|
-
);
|
|
9586
|
+
for (const key of FORWARDED_API_KEYS) {
|
|
9587
|
+
if (process.env[key]) {
|
|
9588
|
+
args.push("-e", key);
|
|
9589
|
+
}
|
|
9328
9590
|
}
|
|
9329
|
-
if ("
|
|
9330
|
-
|
|
9331
|
-
|
|
9332
|
-
|
|
9333
|
-
|
|
9334
|
-
|
|
9591
|
+
if (ctx.config["env-vars"]) {
|
|
9592
|
+
for (const key of ctx.config["env-vars"]) {
|
|
9593
|
+
if (process.env[key]) {
|
|
9594
|
+
args.push("-e", key);
|
|
9595
|
+
}
|
|
9596
|
+
}
|
|
9335
9597
|
}
|
|
9336
|
-
|
|
9337
|
-
|
|
9598
|
+
args.push(imageName);
|
|
9599
|
+
args.push(...ctx.forwardArgs);
|
|
9600
|
+
return args;
|
|
9601
|
+
}
|
|
9602
|
+
function filterDockerArgs(argv) {
|
|
9603
|
+
const dockerFlags = /* @__PURE__ */ new Set(["--docker", "--docker-ro", "--no-docker", "--docker-dev"]);
|
|
9604
|
+
return argv.filter((arg) => !dockerFlags.has(arg));
|
|
9605
|
+
}
|
|
9606
|
+
function resolveDockerEnabled(config, options, profileDocker) {
|
|
9607
|
+
if (options.noDocker) {
|
|
9608
|
+
return false;
|
|
9338
9609
|
}
|
|
9339
|
-
if (
|
|
9340
|
-
|
|
9341
|
-
integer: true,
|
|
9342
|
-
min: 1
|
|
9343
|
-
});
|
|
9610
|
+
if (options.docker || options.dockerRo) {
|
|
9611
|
+
return true;
|
|
9344
9612
|
}
|
|
9345
|
-
if (
|
|
9346
|
-
|
|
9613
|
+
if (profileDocker !== void 0) {
|
|
9614
|
+
return profileDocker;
|
|
9347
9615
|
}
|
|
9348
|
-
|
|
9349
|
-
return result;
|
|
9616
|
+
return config?.enabled ?? false;
|
|
9350
9617
|
}
|
|
9351
|
-
function
|
|
9352
|
-
if (
|
|
9353
|
-
|
|
9618
|
+
async function executeInDocker(ctx, devMode) {
|
|
9619
|
+
if (isInsideContainer()) {
|
|
9620
|
+
console.error(
|
|
9621
|
+
"Warning: Docker mode requested but already inside a container. Proceeding without re-containerization."
|
|
9622
|
+
);
|
|
9623
|
+
throw new DockerSkipError();
|
|
9354
9624
|
}
|
|
9355
|
-
const
|
|
9356
|
-
|
|
9357
|
-
|
|
9358
|
-
|
|
9625
|
+
const available = await checkDockerAvailable();
|
|
9626
|
+
if (!available) {
|
|
9627
|
+
throw new DockerUnavailableError();
|
|
9628
|
+
}
|
|
9629
|
+
const dockerfile = resolveDockerfile(ctx.config, devMode.enabled);
|
|
9630
|
+
const imageName = devMode.enabled ? DEV_IMAGE_NAME : ctx.config["image-name"] ?? DEFAULT_IMAGE_NAME;
|
|
9631
|
+
if (devMode.enabled) {
|
|
9632
|
+
console.error(`[dev mode] Mounting source from ${devMode.sourcePath}`);
|
|
9633
|
+
}
|
|
9634
|
+
try {
|
|
9635
|
+
await ensureImage(imageName, dockerfile);
|
|
9636
|
+
} catch (error) {
|
|
9637
|
+
if (error instanceof DockerBuildError) {
|
|
9638
|
+
console.error("Docker build failed:");
|
|
9639
|
+
console.error(error.output);
|
|
9640
|
+
throw error;
|
|
9359
9641
|
}
|
|
9360
|
-
|
|
9642
|
+
throw error;
|
|
9361
9643
|
}
|
|
9362
|
-
|
|
9644
|
+
const dockerArgs = buildDockerRunArgs(ctx, imageName, devMode);
|
|
9645
|
+
const proc = Bun.spawn(["docker", ...dockerArgs], {
|
|
9646
|
+
stdin: "inherit",
|
|
9647
|
+
stdout: "inherit",
|
|
9648
|
+
stderr: "inherit"
|
|
9649
|
+
});
|
|
9650
|
+
const exitCode = await proc.exited;
|
|
9651
|
+
process.exit(exitCode);
|
|
9363
9652
|
}
|
|
9364
|
-
function
|
|
9365
|
-
|
|
9366
|
-
|
|
9653
|
+
function createDockerContext(config, options, argv, cwd, profileCwdPermission) {
|
|
9654
|
+
return {
|
|
9655
|
+
config: config ?? {},
|
|
9656
|
+
options,
|
|
9657
|
+
forwardArgs: filterDockerArgs(argv),
|
|
9658
|
+
cwd,
|
|
9659
|
+
profileCwdPermission
|
|
9660
|
+
};
|
|
9661
|
+
}
|
|
9662
|
+
|
|
9663
|
+
// src/cli/agent-command.ts
|
|
9664
|
+
function createHumanInputHandler(env, progress, keyboard) {
|
|
9665
|
+
const stdout = env.stdout;
|
|
9666
|
+
if (!isInteractive(env.stdin) || typeof stdout.isTTY !== "boolean" || !stdout.isTTY) {
|
|
9667
|
+
return void 0;
|
|
9367
9668
|
}
|
|
9368
|
-
|
|
9369
|
-
|
|
9370
|
-
|
|
9669
|
+
return async (question) => {
|
|
9670
|
+
progress.pause();
|
|
9671
|
+
if (keyboard.cleanupEsc) {
|
|
9672
|
+
keyboard.cleanupEsc();
|
|
9673
|
+
keyboard.cleanupEsc = null;
|
|
9674
|
+
}
|
|
9675
|
+
const rl = (0, import_promises3.createInterface)({ input: env.stdin, output: env.stdout });
|
|
9371
9676
|
try {
|
|
9372
|
-
|
|
9373
|
-
|
|
9374
|
-
|
|
9375
|
-
|
|
9376
|
-
|
|
9377
|
-
|
|
9378
|
-
|
|
9379
|
-
|
|
9380
|
-
|
|
9381
|
-
|
|
9677
|
+
const questionLine = question.trim() ? `
|
|
9678
|
+
${renderMarkdownWithSeparators(question.trim())}` : "";
|
|
9679
|
+
let isFirst = true;
|
|
9680
|
+
while (true) {
|
|
9681
|
+
const statsPrompt = progress.formatPrompt();
|
|
9682
|
+
const prompt = isFirst ? `${questionLine}
|
|
9683
|
+
${statsPrompt}` : statsPrompt;
|
|
9684
|
+
isFirst = false;
|
|
9685
|
+
const answer = await rl.question(prompt);
|
|
9686
|
+
const trimmed = answer.trim();
|
|
9687
|
+
if (trimmed) {
|
|
9688
|
+
return trimmed;
|
|
9689
|
+
}
|
|
9382
9690
|
}
|
|
9691
|
+
} finally {
|
|
9692
|
+
rl.close();
|
|
9693
|
+
keyboard.restore();
|
|
9694
|
+
}
|
|
9695
|
+
};
|
|
9696
|
+
}
|
|
9697
|
+
async function executeAgent(promptArg, options, env) {
|
|
9698
|
+
const dockerOptions = {
|
|
9699
|
+
docker: options.docker ?? false,
|
|
9700
|
+
dockerRo: options.dockerRo ?? false,
|
|
9701
|
+
noDocker: options.noDocker ?? false,
|
|
9702
|
+
dockerDev: options.dockerDev ?? false
|
|
9703
|
+
};
|
|
9704
|
+
const dockerEnabled = resolveDockerEnabled(
|
|
9705
|
+
env.dockerConfig,
|
|
9706
|
+
dockerOptions,
|
|
9707
|
+
options.docker
|
|
9708
|
+
// Profile-level docker: true/false
|
|
9709
|
+
);
|
|
9710
|
+
if (dockerEnabled) {
|
|
9711
|
+
const devMode = resolveDevMode(env.dockerConfig, dockerOptions.dockerDev);
|
|
9712
|
+
const ctx = createDockerContext(
|
|
9713
|
+
env.dockerConfig,
|
|
9714
|
+
dockerOptions,
|
|
9715
|
+
env.argv.slice(2),
|
|
9716
|
+
// Remove 'node' and script path
|
|
9717
|
+
process.cwd(),
|
|
9718
|
+
options.dockerCwdPermission
|
|
9719
|
+
// Profile-level CWD permission override
|
|
9720
|
+
);
|
|
9721
|
+
try {
|
|
9722
|
+
await executeInDocker(ctx, devMode);
|
|
9383
9723
|
} catch (error) {
|
|
9384
|
-
if (error instanceof
|
|
9385
|
-
|
|
9724
|
+
if (error instanceof Error && error.message === "SKIP_DOCKER") {
|
|
9725
|
+
} else {
|
|
9726
|
+
throw error;
|
|
9386
9727
|
}
|
|
9387
|
-
throw error;
|
|
9388
9728
|
}
|
|
9389
9729
|
}
|
|
9390
|
-
|
|
9391
|
-
|
|
9392
|
-
|
|
9393
|
-
const
|
|
9394
|
-
if (
|
|
9395
|
-
|
|
9396
|
-
|
|
9397
|
-
|
|
9398
|
-
|
|
9399
|
-
|
|
9400
|
-
|
|
9401
|
-
throw new ConfigError(
|
|
9402
|
-
`Failed to read config file: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
9403
|
-
configPath
|
|
9404
|
-
);
|
|
9730
|
+
const prompt = await resolvePrompt(promptArg, env);
|
|
9731
|
+
const client = env.createClient();
|
|
9732
|
+
const registry = new GadgetRegistry();
|
|
9733
|
+
const stdinIsInteractive = isInteractive(env.stdin);
|
|
9734
|
+
if (options.builtins !== false) {
|
|
9735
|
+
for (const gadget of builtinGadgets) {
|
|
9736
|
+
if (gadget.name === "AskUser" && (options.builtinInteraction === false || !stdinIsInteractive)) {
|
|
9737
|
+
continue;
|
|
9738
|
+
}
|
|
9739
|
+
registry.registerByClass(gadget);
|
|
9740
|
+
}
|
|
9405
9741
|
}
|
|
9406
|
-
|
|
9407
|
-
|
|
9408
|
-
|
|
9409
|
-
|
|
9410
|
-
|
|
9411
|
-
|
|
9412
|
-
configPath
|
|
9413
|
-
);
|
|
9742
|
+
const gadgetSpecifiers = options.gadget ?? [];
|
|
9743
|
+
if (gadgetSpecifiers.length > 0) {
|
|
9744
|
+
const gadgets2 = await loadGadgets(gadgetSpecifiers, process.cwd());
|
|
9745
|
+
for (const gadget of gadgets2) {
|
|
9746
|
+
registry.registerByClass(gadget);
|
|
9747
|
+
}
|
|
9414
9748
|
}
|
|
9415
|
-
const
|
|
9416
|
-
const
|
|
9417
|
-
|
|
9418
|
-
|
|
9419
|
-
|
|
9420
|
-
|
|
9421
|
-
|
|
9422
|
-
|
|
9423
|
-
|
|
9424
|
-
|
|
9425
|
-
|
|
9426
|
-
|
|
9427
|
-
|
|
9428
|
-
|
|
9429
|
-
|
|
9430
|
-
const sectionObj = section;
|
|
9431
|
-
if (typeof sectionObj.system === "string" && hasTemplateSyntax(sectionObj.system)) {
|
|
9432
|
-
hasTemplates = true;
|
|
9433
|
-
break;
|
|
9749
|
+
const printer = new StreamPrinter(env.stdout);
|
|
9750
|
+
const stderrTTY = env.stderr.isTTY === true;
|
|
9751
|
+
const progress = new StreamProgress(env.stderr, stderrTTY, client.modelRegistry);
|
|
9752
|
+
const abortController = new AbortController();
|
|
9753
|
+
let wasCancelled = false;
|
|
9754
|
+
let isStreaming = false;
|
|
9755
|
+
const stdinStream = env.stdin;
|
|
9756
|
+
const handleCancel = () => {
|
|
9757
|
+
if (!abortController.signal.aborted) {
|
|
9758
|
+
wasCancelled = true;
|
|
9759
|
+
abortController.abort();
|
|
9760
|
+
progress.pause();
|
|
9761
|
+
env.stderr.write(import_chalk5.default.yellow(`
|
|
9762
|
+
[Cancelled] ${progress.formatStats()}
|
|
9763
|
+
`));
|
|
9434
9764
|
}
|
|
9435
|
-
}
|
|
9436
|
-
|
|
9437
|
-
|
|
9438
|
-
|
|
9439
|
-
|
|
9765
|
+
};
|
|
9766
|
+
const keyboard = {
|
|
9767
|
+
cleanupEsc: null,
|
|
9768
|
+
cleanupSigint: null,
|
|
9769
|
+
restore: () => {
|
|
9770
|
+
if (stdinIsInteractive && stdinStream.isTTY && !wasCancelled) {
|
|
9771
|
+
keyboard.cleanupEsc = createEscKeyListener(stdinStream, handleCancel);
|
|
9772
|
+
}
|
|
9440
9773
|
}
|
|
9774
|
+
};
|
|
9775
|
+
const handleQuit = () => {
|
|
9776
|
+
keyboard.cleanupEsc?.();
|
|
9777
|
+
keyboard.cleanupSigint?.();
|
|
9778
|
+
progress.complete();
|
|
9779
|
+
printer.ensureNewline();
|
|
9780
|
+
const summary = renderOverallSummary({
|
|
9781
|
+
totalTokens: usage?.totalTokens,
|
|
9782
|
+
iterations,
|
|
9783
|
+
elapsedSeconds: progress.getTotalElapsedSeconds(),
|
|
9784
|
+
cost: progress.getTotalCost()
|
|
9785
|
+
});
|
|
9786
|
+
if (summary) {
|
|
9787
|
+
env.stderr.write(`${import_chalk5.default.dim("\u2500".repeat(40))}
|
|
9788
|
+
`);
|
|
9789
|
+
env.stderr.write(`${summary}
|
|
9790
|
+
`);
|
|
9791
|
+
}
|
|
9792
|
+
env.stderr.write(import_chalk5.default.dim("[Quit]\n"));
|
|
9793
|
+
process.exit(130);
|
|
9794
|
+
};
|
|
9795
|
+
if (stdinIsInteractive && stdinStream.isTTY) {
|
|
9796
|
+
keyboard.cleanupEsc = createEscKeyListener(stdinStream, handleCancel);
|
|
9441
9797
|
}
|
|
9442
|
-
|
|
9443
|
-
|
|
9444
|
-
|
|
9445
|
-
|
|
9446
|
-
|
|
9447
|
-
|
|
9448
|
-
|
|
9449
|
-
|
|
9798
|
+
keyboard.cleanupSigint = createSigintListener(
|
|
9799
|
+
handleCancel,
|
|
9800
|
+
handleQuit,
|
|
9801
|
+
() => isStreaming && !abortController.signal.aborted,
|
|
9802
|
+
env.stderr
|
|
9803
|
+
);
|
|
9804
|
+
const DEFAULT_APPROVAL_REQUIRED = ["RunCommand", "WriteFile", "EditFile"];
|
|
9805
|
+
const userApprovals = options.gadgetApproval ?? {};
|
|
9806
|
+
const gadgetApprovals = {
|
|
9807
|
+
...userApprovals
|
|
9808
|
+
};
|
|
9809
|
+
for (const gadget of DEFAULT_APPROVAL_REQUIRED) {
|
|
9810
|
+
const normalizedGadget = gadget.toLowerCase();
|
|
9811
|
+
const isConfigured = Object.keys(userApprovals).some(
|
|
9812
|
+
(key) => key.toLowerCase() === normalizedGadget
|
|
9813
|
+
);
|
|
9814
|
+
if (!isConfigured) {
|
|
9815
|
+
gadgetApprovals[gadget] = "approval-required";
|
|
9450
9816
|
}
|
|
9451
|
-
throw error;
|
|
9452
9817
|
}
|
|
9453
|
-
|
|
9818
|
+
const approvalConfig = {
|
|
9819
|
+
gadgetApprovals,
|
|
9820
|
+
defaultMode: "allowed"
|
|
9821
|
+
};
|
|
9822
|
+
const approvalManager = new ApprovalManager(approvalConfig, env, progress);
|
|
9823
|
+
let usage;
|
|
9824
|
+
let iterations = 0;
|
|
9825
|
+
const llmRequestsDir = resolveLogDir(options.logLlmRequests, "requests");
|
|
9826
|
+
const llmResponsesDir = resolveLogDir(options.logLlmResponses, "responses");
|
|
9827
|
+
let llmCallCounter = 0;
|
|
9828
|
+
const countMessagesTokens = async (model, messages) => {
|
|
9454
9829
|
try {
|
|
9455
|
-
|
|
9456
|
-
} catch
|
|
9457
|
-
|
|
9458
|
-
|
|
9459
|
-
}
|
|
9460
|
-
throw error;
|
|
9830
|
+
return await client.countTokens(model, messages);
|
|
9831
|
+
} catch {
|
|
9832
|
+
const totalChars = messages.reduce((sum, m) => sum + (m.content?.length ?? 0), 0);
|
|
9833
|
+
return Math.round(totalChars / FALLBACK_CHARS_PER_TOKEN);
|
|
9461
9834
|
}
|
|
9462
|
-
}
|
|
9463
|
-
const
|
|
9464
|
-
|
|
9465
|
-
|
|
9466
|
-
|
|
9467
|
-
|
|
9468
|
-
|
|
9469
|
-
|
|
9470
|
-
|
|
9471
|
-
|
|
9472
|
-
|
|
9473
|
-
|
|
9474
|
-
|
|
9835
|
+
};
|
|
9836
|
+
const countGadgetOutputTokens = async (output) => {
|
|
9837
|
+
if (!output) return void 0;
|
|
9838
|
+
try {
|
|
9839
|
+
const messages = [{ role: "assistant", content: output }];
|
|
9840
|
+
return await client.countTokens(options.model, messages);
|
|
9841
|
+
} catch {
|
|
9842
|
+
return void 0;
|
|
9843
|
+
}
|
|
9844
|
+
};
|
|
9845
|
+
const builder = new AgentBuilder(client).withModel(options.model).withLogger(env.createLogger("llmist:cli:agent")).withHooks({
|
|
9846
|
+
observers: {
|
|
9847
|
+
// onLLMCallStart: Start progress indicator for each LLM call
|
|
9848
|
+
// This showcases how to react to agent lifecycle events
|
|
9849
|
+
onLLMCallStart: async (context) => {
|
|
9850
|
+
isStreaming = true;
|
|
9851
|
+
llmCallCounter++;
|
|
9852
|
+
const inputTokens = await countMessagesTokens(
|
|
9853
|
+
context.options.model,
|
|
9854
|
+
context.options.messages
|
|
9855
|
+
);
|
|
9856
|
+
progress.startCall(context.options.model, inputTokens);
|
|
9857
|
+
progress.setInputTokens(inputTokens, false);
|
|
9858
|
+
if (llmRequestsDir) {
|
|
9859
|
+
const filename = `${Date.now()}_call_${llmCallCounter}.request.txt`;
|
|
9860
|
+
const content = formatLlmRequest(context.options.messages);
|
|
9861
|
+
await writeLogFile(llmRequestsDir, filename, content);
|
|
9862
|
+
}
|
|
9863
|
+
},
|
|
9864
|
+
// onStreamChunk: Real-time updates as LLM generates tokens
|
|
9865
|
+
// This enables responsive UIs that show progress during generation
|
|
9866
|
+
onStreamChunk: async (context) => {
|
|
9867
|
+
progress.update(context.accumulatedText.length);
|
|
9868
|
+
if (context.usage) {
|
|
9869
|
+
if (context.usage.inputTokens) {
|
|
9870
|
+
progress.setInputTokens(context.usage.inputTokens, false);
|
|
9871
|
+
}
|
|
9872
|
+
if (context.usage.outputTokens) {
|
|
9873
|
+
progress.setOutputTokens(context.usage.outputTokens, false);
|
|
9874
|
+
}
|
|
9875
|
+
progress.setCachedTokens(
|
|
9876
|
+
context.usage.cachedInputTokens ?? 0,
|
|
9877
|
+
context.usage.cacheCreationInputTokens ?? 0
|
|
9878
|
+
);
|
|
9879
|
+
}
|
|
9880
|
+
},
|
|
9881
|
+
// onLLMCallComplete: Finalize metrics after each LLM call
|
|
9882
|
+
// This is where you'd typically log metrics or update dashboards
|
|
9883
|
+
onLLMCallComplete: async (context) => {
|
|
9884
|
+
isStreaming = false;
|
|
9885
|
+
usage = context.usage;
|
|
9886
|
+
iterations = Math.max(iterations, context.iteration + 1);
|
|
9887
|
+
if (context.usage) {
|
|
9888
|
+
if (context.usage.inputTokens) {
|
|
9889
|
+
progress.setInputTokens(context.usage.inputTokens, false);
|
|
9890
|
+
}
|
|
9891
|
+
if (context.usage.outputTokens) {
|
|
9892
|
+
progress.setOutputTokens(context.usage.outputTokens, false);
|
|
9893
|
+
}
|
|
9894
|
+
}
|
|
9895
|
+
let callCost;
|
|
9896
|
+
if (context.usage && client.modelRegistry) {
|
|
9897
|
+
try {
|
|
9898
|
+
const modelName = context.options.model.includes(":") ? context.options.model.split(":")[1] : context.options.model;
|
|
9899
|
+
const costResult = client.modelRegistry.estimateCost(
|
|
9900
|
+
modelName,
|
|
9901
|
+
context.usage.inputTokens,
|
|
9902
|
+
context.usage.outputTokens,
|
|
9903
|
+
context.usage.cachedInputTokens ?? 0,
|
|
9904
|
+
context.usage.cacheCreationInputTokens ?? 0
|
|
9905
|
+
);
|
|
9906
|
+
if (costResult) callCost = costResult.totalCost;
|
|
9907
|
+
} catch {
|
|
9908
|
+
}
|
|
9909
|
+
}
|
|
9910
|
+
const callElapsed = progress.getCallElapsedSeconds();
|
|
9911
|
+
progress.endCall(context.usage);
|
|
9912
|
+
if (!options.quiet) {
|
|
9913
|
+
const summary = renderSummary({
|
|
9914
|
+
iterations: context.iteration + 1,
|
|
9915
|
+
model: options.model,
|
|
9916
|
+
usage: context.usage,
|
|
9917
|
+
elapsedSeconds: callElapsed,
|
|
9918
|
+
cost: callCost,
|
|
9919
|
+
finishReason: context.finishReason
|
|
9920
|
+
});
|
|
9921
|
+
if (summary) {
|
|
9922
|
+
env.stderr.write(`${summary}
|
|
9923
|
+
`);
|
|
9924
|
+
}
|
|
9925
|
+
}
|
|
9926
|
+
if (llmResponsesDir) {
|
|
9927
|
+
const filename = `${Date.now()}_call_${llmCallCounter}.response.txt`;
|
|
9928
|
+
await writeLogFile(llmResponsesDir, filename, context.rawResponse);
|
|
9475
9929
|
}
|
|
9476
|
-
throw error;
|
|
9477
9930
|
}
|
|
9478
|
-
|
|
9479
|
-
|
|
9480
|
-
|
|
9481
|
-
|
|
9482
|
-
|
|
9483
|
-
|
|
9484
|
-
|
|
9485
|
-
|
|
9486
|
-
|
|
9931
|
+
},
|
|
9932
|
+
// SHOWCASE: Controller-based approval gating for gadgets
|
|
9933
|
+
//
|
|
9934
|
+
// This demonstrates how to add safety layers WITHOUT modifying gadgets.
|
|
9935
|
+
// The ApprovalManager handles approval flows externally via beforeGadgetExecution.
|
|
9936
|
+
// Approval modes are configurable via cli.toml:
|
|
9937
|
+
// - "allowed": auto-proceed
|
|
9938
|
+
// - "denied": auto-reject, return message to LLM
|
|
9939
|
+
// - "approval-required": prompt user interactively
|
|
9940
|
+
//
|
|
9941
|
+
// Default: RunCommand, WriteFile, EditFile require approval unless overridden.
|
|
9942
|
+
controllers: {
|
|
9943
|
+
beforeGadgetExecution: async (ctx) => {
|
|
9944
|
+
const mode = approvalManager.getApprovalMode(ctx.gadgetName);
|
|
9945
|
+
if (mode === "allowed") {
|
|
9946
|
+
return { action: "proceed" };
|
|
9947
|
+
}
|
|
9948
|
+
const stdinTTY = isInteractive(env.stdin);
|
|
9949
|
+
const stderrTTY2 = env.stderr.isTTY === true;
|
|
9950
|
+
const canPrompt = stdinTTY && stderrTTY2;
|
|
9951
|
+
if (!canPrompt) {
|
|
9952
|
+
if (mode === "approval-required") {
|
|
9953
|
+
return {
|
|
9954
|
+
action: "skip",
|
|
9955
|
+
syntheticResult: `status=denied
|
|
9956
|
+
|
|
9957
|
+
${ctx.gadgetName} requires interactive approval. Run in a terminal to approve.`
|
|
9958
|
+
};
|
|
9959
|
+
}
|
|
9960
|
+
if (mode === "denied") {
|
|
9961
|
+
return {
|
|
9962
|
+
action: "skip",
|
|
9963
|
+
syntheticResult: `status=denied
|
|
9964
|
+
|
|
9965
|
+
${ctx.gadgetName} is denied by configuration.`
|
|
9966
|
+
};
|
|
9967
|
+
}
|
|
9968
|
+
return { action: "proceed" };
|
|
9969
|
+
}
|
|
9970
|
+
const result = await approvalManager.requestApproval(ctx.gadgetName, ctx.parameters);
|
|
9971
|
+
if (!result.approved) {
|
|
9972
|
+
return {
|
|
9973
|
+
action: "skip",
|
|
9974
|
+
syntheticResult: `status=denied
|
|
9975
|
+
|
|
9976
|
+
Denied: ${result.reason ?? "by user"}`
|
|
9977
|
+
};
|
|
9487
9978
|
}
|
|
9488
|
-
|
|
9979
|
+
return { action: "proceed" };
|
|
9489
9980
|
}
|
|
9490
9981
|
}
|
|
9982
|
+
});
|
|
9983
|
+
if (options.system) {
|
|
9984
|
+
builder.withSystem(options.system);
|
|
9491
9985
|
}
|
|
9492
|
-
|
|
9493
|
-
|
|
9494
|
-
function resolveGadgets(section, inheritedGadgets, sectionName, configPath) {
|
|
9495
|
-
const hasGadgets = "gadgets" in section;
|
|
9496
|
-
const hasGadgetLegacy = "gadget" in section;
|
|
9497
|
-
const hasGadgetAdd = "gadget-add" in section;
|
|
9498
|
-
const hasGadgetRemove = "gadget-remove" in section;
|
|
9499
|
-
if (hasGadgetLegacy && !hasGadgets) {
|
|
9500
|
-
console.warn(
|
|
9501
|
-
`[config] Warning: [${sectionName}].gadget is deprecated, use 'gadgets' (plural) instead`
|
|
9502
|
-
);
|
|
9986
|
+
if (options.maxIterations !== void 0) {
|
|
9987
|
+
builder.withMaxIterations(options.maxIterations);
|
|
9503
9988
|
}
|
|
9504
|
-
if (
|
|
9505
|
-
|
|
9506
|
-
`[${sectionName}] Cannot use 'gadgets' with 'gadget-add'/'gadget-remove'. Use either full replacement (gadgets) OR modification (gadget-add/gadget-remove).`,
|
|
9507
|
-
configPath
|
|
9508
|
-
);
|
|
9989
|
+
if (options.temperature !== void 0) {
|
|
9990
|
+
builder.withTemperature(options.temperature);
|
|
9509
9991
|
}
|
|
9510
|
-
|
|
9511
|
-
|
|
9992
|
+
const humanInputHandler = createHumanInputHandler(env, progress, keyboard);
|
|
9993
|
+
if (humanInputHandler) {
|
|
9994
|
+
builder.onHumanInput(humanInputHandler);
|
|
9512
9995
|
}
|
|
9513
|
-
|
|
9514
|
-
|
|
9996
|
+
builder.withSignal(abortController.signal);
|
|
9997
|
+
const gadgets = registry.getAll();
|
|
9998
|
+
if (gadgets.length > 0) {
|
|
9999
|
+
builder.withGadgets(...gadgets);
|
|
9515
10000
|
}
|
|
9516
|
-
|
|
9517
|
-
|
|
9518
|
-
const toRemove = new Set(section["gadget-remove"]);
|
|
9519
|
-
result = result.filter((g) => !toRemove.has(g));
|
|
10001
|
+
if (options.gadgetStartPrefix) {
|
|
10002
|
+
builder.withGadgetStartPrefix(options.gadgetStartPrefix);
|
|
9520
10003
|
}
|
|
9521
|
-
if (
|
|
9522
|
-
|
|
9523
|
-
result.push(...toAdd);
|
|
10004
|
+
if (options.gadgetEndPrefix) {
|
|
10005
|
+
builder.withGadgetEndPrefix(options.gadgetEndPrefix);
|
|
9524
10006
|
}
|
|
9525
|
-
|
|
9526
|
-
|
|
9527
|
-
|
|
9528
|
-
|
|
9529
|
-
|
|
9530
|
-
|
|
9531
|
-
|
|
9532
|
-
|
|
10007
|
+
if (options.gadgetArgPrefix) {
|
|
10008
|
+
builder.withGadgetArgPrefix(options.gadgetArgPrefix);
|
|
10009
|
+
}
|
|
10010
|
+
builder.withSyntheticGadgetCall(
|
|
10011
|
+
"TellUser",
|
|
10012
|
+
{
|
|
10013
|
+
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?",
|
|
10014
|
+
done: false,
|
|
10015
|
+
type: "info"
|
|
10016
|
+
},
|
|
10017
|
+
"\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?"
|
|
10018
|
+
);
|
|
10019
|
+
builder.withTextOnlyHandler("acknowledge");
|
|
10020
|
+
builder.withTextWithGadgetsHandler({
|
|
10021
|
+
gadgetName: "TellUser",
|
|
10022
|
+
parameterMapping: (text) => ({ message: text, done: false, type: "info" }),
|
|
10023
|
+
resultMapping: (text) => `\u2139\uFE0F ${text}`
|
|
10024
|
+
});
|
|
10025
|
+
const agent = builder.ask(prompt);
|
|
10026
|
+
let textBuffer = "";
|
|
10027
|
+
const flushTextBuffer = () => {
|
|
10028
|
+
if (textBuffer) {
|
|
10029
|
+
const output = options.quiet ? textBuffer : renderMarkdownWithSeparators(textBuffer);
|
|
10030
|
+
printer.write(output);
|
|
10031
|
+
textBuffer = "";
|
|
9533
10032
|
}
|
|
9534
|
-
|
|
9535
|
-
|
|
10033
|
+
};
|
|
10034
|
+
try {
|
|
10035
|
+
for await (const event of agent.run()) {
|
|
10036
|
+
if (event.type === "text") {
|
|
10037
|
+
progress.pause();
|
|
10038
|
+
textBuffer += event.content;
|
|
10039
|
+
} else if (event.type === "gadget_result") {
|
|
10040
|
+
flushTextBuffer();
|
|
10041
|
+
progress.pause();
|
|
10042
|
+
if (options.quiet) {
|
|
10043
|
+
if (event.result.gadgetName === "TellUser" && event.result.parameters?.message) {
|
|
10044
|
+
const message = String(event.result.parameters.message);
|
|
10045
|
+
env.stdout.write(`${message}
|
|
10046
|
+
`);
|
|
10047
|
+
}
|
|
10048
|
+
} else {
|
|
10049
|
+
const tokenCount = await countGadgetOutputTokens(event.result.result);
|
|
10050
|
+
env.stderr.write(`${formatGadgetSummary({ ...event.result, tokenCount })}
|
|
10051
|
+
`);
|
|
10052
|
+
}
|
|
10053
|
+
}
|
|
9536
10054
|
}
|
|
9537
|
-
|
|
9538
|
-
if (
|
|
9539
|
-
throw
|
|
10055
|
+
} catch (error) {
|
|
10056
|
+
if (!isAbortError(error)) {
|
|
10057
|
+
throw error;
|
|
9540
10058
|
}
|
|
9541
|
-
|
|
9542
|
-
|
|
9543
|
-
|
|
9544
|
-
|
|
9545
|
-
|
|
9546
|
-
|
|
9547
|
-
|
|
9548
|
-
|
|
10059
|
+
} finally {
|
|
10060
|
+
isStreaming = false;
|
|
10061
|
+
keyboard.cleanupEsc?.();
|
|
10062
|
+
keyboard.cleanupSigint?.();
|
|
10063
|
+
}
|
|
10064
|
+
flushTextBuffer();
|
|
10065
|
+
progress.complete();
|
|
10066
|
+
printer.ensureNewline();
|
|
10067
|
+
if (!options.quiet && iterations > 1) {
|
|
10068
|
+
env.stderr.write(`${import_chalk5.default.dim("\u2500".repeat(40))}
|
|
10069
|
+
`);
|
|
10070
|
+
const summary = renderOverallSummary({
|
|
10071
|
+
totalTokens: usage?.totalTokens,
|
|
10072
|
+
iterations,
|
|
10073
|
+
elapsedSeconds: progress.getTotalElapsedSeconds(),
|
|
10074
|
+
cost: progress.getTotalCost()
|
|
10075
|
+
});
|
|
10076
|
+
if (summary) {
|
|
10077
|
+
env.stderr.write(`${summary}
|
|
10078
|
+
`);
|
|
9549
10079
|
}
|
|
9550
|
-
|
|
9551
|
-
|
|
9552
|
-
|
|
9553
|
-
|
|
9554
|
-
|
|
9555
|
-
|
|
9556
|
-
|
|
9557
|
-
|
|
9558
|
-
|
|
9559
|
-
|
|
9560
|
-
|
|
9561
|
-
|
|
9562
|
-
|
|
10080
|
+
}
|
|
10081
|
+
}
|
|
10082
|
+
function registerAgentCommand(program, env, config) {
|
|
10083
|
+
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.");
|
|
10084
|
+
addAgentOptions(cmd, config);
|
|
10085
|
+
cmd.action(
|
|
10086
|
+
(prompt, options) => executeAction(() => {
|
|
10087
|
+
const mergedOptions = {
|
|
10088
|
+
...options,
|
|
10089
|
+
gadgetApproval: config?.["gadget-approval"]
|
|
10090
|
+
};
|
|
10091
|
+
return executeAgent(prompt, mergedOptions, env);
|
|
10092
|
+
}, env)
|
|
10093
|
+
);
|
|
10094
|
+
}
|
|
10095
|
+
|
|
10096
|
+
// src/cli/complete-command.ts
|
|
10097
|
+
init_messages();
|
|
10098
|
+
init_model_shortcuts();
|
|
10099
|
+
init_constants2();
|
|
10100
|
+
async function executeComplete(promptArg, options, env) {
|
|
10101
|
+
const prompt = await resolvePrompt(promptArg, env);
|
|
10102
|
+
const client = env.createClient();
|
|
10103
|
+
const model = resolveModel(options.model);
|
|
10104
|
+
const builder = new LLMMessageBuilder();
|
|
10105
|
+
if (options.system) {
|
|
10106
|
+
builder.addSystem(options.system);
|
|
10107
|
+
}
|
|
10108
|
+
builder.addUser(prompt);
|
|
10109
|
+
const messages = builder.build();
|
|
10110
|
+
const llmRequestsDir = resolveLogDir(options.logLlmRequests, "requests");
|
|
10111
|
+
const llmResponsesDir = resolveLogDir(options.logLlmResponses, "responses");
|
|
10112
|
+
const timestamp = Date.now();
|
|
10113
|
+
if (llmRequestsDir) {
|
|
10114
|
+
const filename = `${timestamp}_complete.request.txt`;
|
|
10115
|
+
const content = formatLlmRequest(messages);
|
|
10116
|
+
await writeLogFile(llmRequestsDir, filename, content);
|
|
10117
|
+
}
|
|
10118
|
+
const stream2 = client.stream({
|
|
10119
|
+
model,
|
|
10120
|
+
messages,
|
|
10121
|
+
temperature: options.temperature,
|
|
10122
|
+
maxTokens: options.maxTokens
|
|
10123
|
+
});
|
|
10124
|
+
const printer = new StreamPrinter(env.stdout);
|
|
10125
|
+
const stderrTTY = env.stderr.isTTY === true;
|
|
10126
|
+
const progress = new StreamProgress(env.stderr, stderrTTY, client.modelRegistry);
|
|
10127
|
+
const estimatedInputTokens = Math.round(prompt.length / FALLBACK_CHARS_PER_TOKEN);
|
|
10128
|
+
progress.startCall(model, estimatedInputTokens);
|
|
10129
|
+
let finishReason;
|
|
10130
|
+
let usage;
|
|
10131
|
+
let accumulatedResponse = "";
|
|
10132
|
+
for await (const chunk of stream2) {
|
|
10133
|
+
if (chunk.usage) {
|
|
10134
|
+
usage = chunk.usage;
|
|
10135
|
+
if (chunk.usage.inputTokens) {
|
|
10136
|
+
progress.setInputTokens(chunk.usage.inputTokens, false);
|
|
10137
|
+
}
|
|
10138
|
+
if (chunk.usage.outputTokens) {
|
|
10139
|
+
progress.setOutputTokens(chunk.usage.outputTokens, false);
|
|
10140
|
+
}
|
|
10141
|
+
}
|
|
10142
|
+
if (chunk.text) {
|
|
10143
|
+
progress.pause();
|
|
10144
|
+
accumulatedResponse += chunk.text;
|
|
10145
|
+
progress.update(accumulatedResponse.length);
|
|
10146
|
+
printer.write(chunk.text);
|
|
10147
|
+
}
|
|
10148
|
+
if (chunk.finishReason !== void 0) {
|
|
10149
|
+
finishReason = chunk.finishReason;
|
|
9563
10150
|
}
|
|
9564
|
-
delete merged["gadget"];
|
|
9565
|
-
delete merged["gadget-add"];
|
|
9566
|
-
delete merged["gadget-remove"];
|
|
9567
|
-
resolving.delete(name);
|
|
9568
|
-
resolved[name] = merged;
|
|
9569
|
-
return merged;
|
|
9570
10151
|
}
|
|
9571
|
-
|
|
9572
|
-
|
|
10152
|
+
progress.endCall(usage);
|
|
10153
|
+
progress.complete();
|
|
10154
|
+
printer.ensureNewline();
|
|
10155
|
+
if (llmResponsesDir) {
|
|
10156
|
+
const filename = `${timestamp}_complete.response.txt`;
|
|
10157
|
+
await writeLogFile(llmResponsesDir, filename, accumulatedResponse);
|
|
9573
10158
|
}
|
|
9574
|
-
|
|
10159
|
+
if (stderrTTY && !options.quiet) {
|
|
10160
|
+
const summary = renderSummary({ finishReason, usage, cost: progress.getTotalCost() });
|
|
10161
|
+
if (summary) {
|
|
10162
|
+
env.stderr.write(`${summary}
|
|
10163
|
+
`);
|
|
10164
|
+
}
|
|
10165
|
+
}
|
|
10166
|
+
}
|
|
10167
|
+
function registerCompleteCommand(program, env, config) {
|
|
10168
|
+
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.");
|
|
10169
|
+
addCompleteOptions(cmd, config);
|
|
10170
|
+
cmd.action(
|
|
10171
|
+
(prompt, options) => executeAction(() => executeComplete(prompt, options, env), env)
|
|
10172
|
+
);
|
|
9575
10173
|
}
|
|
9576
10174
|
|
|
9577
10175
|
// src/cli/gadget-command.ts
|
|
@@ -10261,7 +10859,11 @@ function createCommandEnvironment(baseEnv, config) {
|
|
|
10261
10859
|
logFile: config["log-file"] ?? baseEnv.loggerConfig?.logFile,
|
|
10262
10860
|
logReset: config["log-reset"] ?? baseEnv.loggerConfig?.logReset
|
|
10263
10861
|
};
|
|
10264
|
-
return
|
|
10862
|
+
return {
|
|
10863
|
+
...baseEnv,
|
|
10864
|
+
loggerConfig,
|
|
10865
|
+
createLogger: createLoggerFactory(loggerConfig)
|
|
10866
|
+
};
|
|
10265
10867
|
}
|
|
10266
10868
|
function registerCustomCommand(program, name, config, env) {
|
|
10267
10869
|
const type = config.type ?? "agent";
|
|
@@ -10337,7 +10939,12 @@ async function runCLI(overrides = {}) {
|
|
|
10337
10939
|
logReset: globalOpts.logReset ?? config.global?.["log-reset"]
|
|
10338
10940
|
};
|
|
10339
10941
|
const defaultEnv = createDefaultEnvironment(loggerConfig);
|
|
10340
|
-
const env = {
|
|
10942
|
+
const env = {
|
|
10943
|
+
...defaultEnv,
|
|
10944
|
+
...envOverrides,
|
|
10945
|
+
// Pass Docker config from [docker] section
|
|
10946
|
+
dockerConfig: config.docker
|
|
10947
|
+
};
|
|
10341
10948
|
const program = createProgram(env, config);
|
|
10342
10949
|
await program.parseAsync(env.argv);
|
|
10343
10950
|
}
|