llmist 1.5.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +1577 -1023
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1579 -1025
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -6553,7 +6553,11 @@ var OPTION_FLAGS = {
|
|
|
6553
6553
|
logLlmResponses: "--log-llm-responses [dir]",
|
|
6554
6554
|
noBuiltins: "--no-builtins",
|
|
6555
6555
|
noBuiltinInteraction: "--no-builtin-interaction",
|
|
6556
|
-
quiet: "-q, --quiet"
|
|
6556
|
+
quiet: "-q, --quiet",
|
|
6557
|
+
docker: "--docker",
|
|
6558
|
+
dockerRo: "--docker-ro",
|
|
6559
|
+
noDocker: "--no-docker",
|
|
6560
|
+
dockerDev: "--docker-dev"
|
|
6557
6561
|
};
|
|
6558
6562
|
var OPTION_DESCRIPTIONS = {
|
|
6559
6563
|
model: "Model identifier, e.g. openai:gpt-5-nano or anthropic:claude-sonnet-4-5.",
|
|
@@ -6569,7 +6573,11 @@ var OPTION_DESCRIPTIONS = {
|
|
|
6569
6573
|
logLlmResponses: "Save raw LLM responses as plain text. Optional dir, defaults to ~/.llmist/logs/responses/",
|
|
6570
6574
|
noBuiltins: "Disable built-in gadgets (AskUser, TellUser).",
|
|
6571
6575
|
noBuiltinInteraction: "Disable interactive gadgets (AskUser) while keeping TellUser.",
|
|
6572
|
-
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)."
|
|
6573
6581
|
};
|
|
6574
6582
|
var SUMMARY_PREFIX = "[llmist]";
|
|
6575
6583
|
|
|
@@ -6579,7 +6587,7 @@ var import_commander2 = require("commander");
|
|
|
6579
6587
|
// package.json
|
|
6580
6588
|
var package_default = {
|
|
6581
6589
|
name: "llmist",
|
|
6582
|
-
version: "1.
|
|
6590
|
+
version: "1.5.0",
|
|
6583
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.",
|
|
6584
6592
|
type: "module",
|
|
6585
6593
|
main: "dist/index.cjs",
|
|
@@ -8389,7 +8397,7 @@ function addAgentOptions(cmd, defaults) {
|
|
|
8389
8397
|
OPTION_FLAGS.noBuiltinInteraction,
|
|
8390
8398
|
OPTION_DESCRIPTIONS.noBuiltinInteraction,
|
|
8391
8399
|
defaults?.["builtin-interaction"] !== false
|
|
8392
|
-
).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);
|
|
8393
8401
|
}
|
|
8394
8402
|
function configToCompleteOptions(config) {
|
|
8395
8403
|
const result = {};
|
|
@@ -8424,668 +8432,791 @@ function configToAgentOptions(config) {
|
|
|
8424
8432
|
if (config.quiet !== void 0) result.quiet = config.quiet;
|
|
8425
8433
|
if (config["log-llm-requests"] !== void 0) result.logLlmRequests = config["log-llm-requests"];
|
|
8426
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"];
|
|
8427
8438
|
return result;
|
|
8428
8439
|
}
|
|
8429
8440
|
|
|
8430
|
-
// src/cli/
|
|
8431
|
-
|
|
8432
|
-
|
|
8433
|
-
|
|
8434
|
-
|
|
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";
|
|
8435
8479
|
}
|
|
8436
|
-
|
|
8437
|
-
|
|
8438
|
-
|
|
8439
|
-
|
|
8440
|
-
|
|
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)) {
|
|
8491
|
+
try {
|
|
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
|
+
);
|
|
8441
8499
|
}
|
|
8442
|
-
|
|
8500
|
+
}
|
|
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
|
|
8517
|
+
);
|
|
8518
|
+
}
|
|
8519
|
+
}
|
|
8520
|
+
function validatePrompts(prompts, configPath) {
|
|
8521
|
+
const eta = createTemplateEngine(prompts, configPath);
|
|
8522
|
+
for (const [name, template] of Object.entries(prompts)) {
|
|
8443
8523
|
try {
|
|
8444
|
-
|
|
8445
|
-
|
|
8446
|
-
|
|
8447
|
-
|
|
8448
|
-
|
|
8449
|
-
|
|
8450
|
-
|
|
8451
|
-
isFirst = false;
|
|
8452
|
-
const answer = await rl.question(prompt);
|
|
8453
|
-
const trimmed = answer.trim();
|
|
8454
|
-
if (trimmed) {
|
|
8455
|
-
return trimmed;
|
|
8456
|
-
}
|
|
8457
|
-
}
|
|
8458
|
-
} finally {
|
|
8459
|
-
rl.close();
|
|
8460
|
-
keyboard.restore();
|
|
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
|
+
);
|
|
8461
8531
|
}
|
|
8462
|
-
}
|
|
8532
|
+
}
|
|
8463
8533
|
}
|
|
8464
|
-
|
|
8465
|
-
const
|
|
8466
|
-
const
|
|
8467
|
-
const
|
|
8468
|
-
|
|
8469
|
-
|
|
8470
|
-
|
|
8471
|
-
|
|
8472
|
-
|
|
8473
|
-
|
|
8474
|
-
|
|
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
|
+
);
|
|
8475
8545
|
}
|
|
8476
8546
|
}
|
|
8477
|
-
|
|
8478
|
-
|
|
8479
|
-
|
|
8480
|
-
|
|
8481
|
-
|
|
8547
|
+
}
|
|
8548
|
+
function hasTemplateSyntax(str) {
|
|
8549
|
+
return str.includes("<%");
|
|
8550
|
+
}
|
|
8551
|
+
|
|
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`);
|
|
8482
8658
|
}
|
|
8483
8659
|
}
|
|
8484
|
-
|
|
8485
|
-
|
|
8486
|
-
|
|
8487
|
-
|
|
8488
|
-
|
|
8489
|
-
|
|
8490
|
-
|
|
8491
|
-
|
|
8492
|
-
|
|
8493
|
-
|
|
8494
|
-
|
|
8495
|
-
progress.pause();
|
|
8496
|
-
env.stderr.write(import_chalk5.default.yellow(`
|
|
8497
|
-
[Cancelled] ${progress.formatStats()}
|
|
8498
|
-
`));
|
|
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`);
|
|
8670
|
+
}
|
|
8499
8671
|
}
|
|
8500
|
-
|
|
8501
|
-
|
|
8502
|
-
|
|
8503
|
-
|
|
8504
|
-
|
|
8505
|
-
|
|
8506
|
-
|
|
8507
|
-
}
|
|
8672
|
+
return value;
|
|
8673
|
+
}
|
|
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
|
+
);
|
|
8508
8688
|
}
|
|
8509
|
-
|
|
8510
|
-
|
|
8511
|
-
|
|
8512
|
-
|
|
8513
|
-
|
|
8514
|
-
|
|
8515
|
-
|
|
8516
|
-
|
|
8517
|
-
|
|
8518
|
-
|
|
8519
|
-
|
|
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
|
|
8520
8729
|
});
|
|
8521
|
-
|
|
8522
|
-
|
|
8523
|
-
|
|
8524
|
-
|
|
8525
|
-
|
|
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"`);
|
|
8526
8741
|
}
|
|
8527
|
-
|
|
8528
|
-
process.exit(130);
|
|
8529
|
-
};
|
|
8530
|
-
if (stdinIsInteractive && stdinStream.isTTY) {
|
|
8531
|
-
keyboard.cleanupEsc = createEscKeyListener(stdinStream, handleCancel);
|
|
8742
|
+
result["docker-cwd-permission"] = perm;
|
|
8532
8743
|
}
|
|
8533
|
-
|
|
8534
|
-
|
|
8535
|
-
|
|
8536
|
-
|
|
8537
|
-
|
|
8538
|
-
|
|
8539
|
-
const
|
|
8540
|
-
const
|
|
8541
|
-
|
|
8542
|
-
|
|
8543
|
-
};
|
|
8544
|
-
for (const gadget of DEFAULT_APPROVAL_REQUIRED) {
|
|
8545
|
-
const normalizedGadget = gadget.toLowerCase();
|
|
8546
|
-
const isConfigured = Object.keys(userApprovals).some(
|
|
8547
|
-
(key) => key.toLowerCase() === normalizedGadget
|
|
8548
|
-
);
|
|
8549
|
-
if (!isConfigured) {
|
|
8550
|
-
gadgetApprovals[gadget] = "approval-required";
|
|
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`);
|
|
8551
8754
|
}
|
|
8552
8755
|
}
|
|
8553
|
-
|
|
8554
|
-
|
|
8555
|
-
|
|
8556
|
-
|
|
8557
|
-
|
|
8558
|
-
|
|
8559
|
-
|
|
8560
|
-
const
|
|
8561
|
-
|
|
8562
|
-
|
|
8563
|
-
const countMessagesTokens = async (model, messages) => {
|
|
8564
|
-
try {
|
|
8565
|
-
return await client.countTokens(model, messages);
|
|
8566
|
-
} catch {
|
|
8567
|
-
const totalChars = messages.reduce((sum, m) => sum + (m.content?.length ?? 0), 0);
|
|
8568
|
-
return Math.round(totalChars / FALLBACK_CHARS_PER_TOKEN);
|
|
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`);
|
|
8569
8766
|
}
|
|
8767
|
+
}
|
|
8768
|
+
const result = {
|
|
8769
|
+
...validateBaseConfig(rawObj, section),
|
|
8770
|
+
...validateLoggingConfig(rawObj, section)
|
|
8570
8771
|
};
|
|
8571
|
-
|
|
8572
|
-
|
|
8573
|
-
|
|
8574
|
-
|
|
8575
|
-
|
|
8576
|
-
|
|
8577
|
-
|
|
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`);
|
|
8578
8805
|
}
|
|
8806
|
+
}
|
|
8807
|
+
const result = {
|
|
8808
|
+
...validateBaseConfig(rawObj, section),
|
|
8809
|
+
...validateLoggingConfig(rawObj, section)
|
|
8579
8810
|
};
|
|
8580
|
-
|
|
8581
|
-
|
|
8582
|
-
|
|
8583
|
-
|
|
8584
|
-
|
|
8585
|
-
isStreaming = true;
|
|
8586
|
-
llmCallCounter++;
|
|
8587
|
-
const inputTokens = await countMessagesTokens(
|
|
8588
|
-
context.options.model,
|
|
8589
|
-
context.options.messages
|
|
8590
|
-
);
|
|
8591
|
-
progress.startCall(context.options.model, inputTokens);
|
|
8592
|
-
progress.setInputTokens(inputTokens, false);
|
|
8593
|
-
if (llmRequestsDir) {
|
|
8594
|
-
const filename = `${Date.now()}_call_${llmCallCounter}.request.txt`;
|
|
8595
|
-
const content = formatLlmRequest(context.options.messages);
|
|
8596
|
-
await writeLogFile(llmRequestsDir, filename, content);
|
|
8597
|
-
}
|
|
8598
|
-
},
|
|
8599
|
-
// onStreamChunk: Real-time updates as LLM generates tokens
|
|
8600
|
-
// This enables responsive UIs that show progress during generation
|
|
8601
|
-
onStreamChunk: async (context) => {
|
|
8602
|
-
progress.update(context.accumulatedText.length);
|
|
8603
|
-
if (context.usage) {
|
|
8604
|
-
if (context.usage.inputTokens) {
|
|
8605
|
-
progress.setInputTokens(context.usage.inputTokens, false);
|
|
8606
|
-
}
|
|
8607
|
-
if (context.usage.outputTokens) {
|
|
8608
|
-
progress.setOutputTokens(context.usage.outputTokens, false);
|
|
8609
|
-
}
|
|
8610
|
-
progress.setCachedTokens(
|
|
8611
|
-
context.usage.cachedInputTokens ?? 0,
|
|
8612
|
-
context.usage.cacheCreationInputTokens ?? 0
|
|
8613
|
-
);
|
|
8614
|
-
}
|
|
8615
|
-
},
|
|
8616
|
-
// onLLMCallComplete: Finalize metrics after each LLM call
|
|
8617
|
-
// This is where you'd typically log metrics or update dashboards
|
|
8618
|
-
onLLMCallComplete: async (context) => {
|
|
8619
|
-
isStreaming = false;
|
|
8620
|
-
usage = context.usage;
|
|
8621
|
-
iterations = Math.max(iterations, context.iteration + 1);
|
|
8622
|
-
if (context.usage) {
|
|
8623
|
-
if (context.usage.inputTokens) {
|
|
8624
|
-
progress.setInputTokens(context.usage.inputTokens, false);
|
|
8625
|
-
}
|
|
8626
|
-
if (context.usage.outputTokens) {
|
|
8627
|
-
progress.setOutputTokens(context.usage.outputTokens, false);
|
|
8628
|
-
}
|
|
8629
|
-
}
|
|
8630
|
-
let callCost;
|
|
8631
|
-
if (context.usage && client.modelRegistry) {
|
|
8632
|
-
try {
|
|
8633
|
-
const modelName = context.options.model.includes(":") ? context.options.model.split(":")[1] : context.options.model;
|
|
8634
|
-
const costResult = client.modelRegistry.estimateCost(
|
|
8635
|
-
modelName,
|
|
8636
|
-
context.usage.inputTokens,
|
|
8637
|
-
context.usage.outputTokens,
|
|
8638
|
-
context.usage.cachedInputTokens ?? 0,
|
|
8639
|
-
context.usage.cacheCreationInputTokens ?? 0
|
|
8640
|
-
);
|
|
8641
|
-
if (costResult) callCost = costResult.totalCost;
|
|
8642
|
-
} catch {
|
|
8643
|
-
}
|
|
8644
|
-
}
|
|
8645
|
-
const callElapsed = progress.getCallElapsedSeconds();
|
|
8646
|
-
progress.endCall(context.usage);
|
|
8647
|
-
if (!options.quiet) {
|
|
8648
|
-
const summary = renderSummary({
|
|
8649
|
-
iterations: context.iteration + 1,
|
|
8650
|
-
model: options.model,
|
|
8651
|
-
usage: context.usage,
|
|
8652
|
-
elapsedSeconds: callElapsed,
|
|
8653
|
-
cost: callCost,
|
|
8654
|
-
finishReason: context.finishReason
|
|
8655
|
-
});
|
|
8656
|
-
if (summary) {
|
|
8657
|
-
env.stderr.write(`${summary}
|
|
8658
|
-
`);
|
|
8659
|
-
}
|
|
8660
|
-
}
|
|
8661
|
-
if (llmResponsesDir) {
|
|
8662
|
-
const filename = `${Date.now()}_call_${llmCallCounter}.response.txt`;
|
|
8663
|
-
await writeLogFile(llmResponsesDir, filename, context.rawResponse);
|
|
8664
|
-
}
|
|
8665
|
-
}
|
|
8666
|
-
},
|
|
8667
|
-
// SHOWCASE: Controller-based approval gating for gadgets
|
|
8668
|
-
//
|
|
8669
|
-
// This demonstrates how to add safety layers WITHOUT modifying gadgets.
|
|
8670
|
-
// The ApprovalManager handles approval flows externally via beforeGadgetExecution.
|
|
8671
|
-
// Approval modes are configurable via cli.toml:
|
|
8672
|
-
// - "allowed": auto-proceed
|
|
8673
|
-
// - "denied": auto-reject, return message to LLM
|
|
8674
|
-
// - "approval-required": prompt user interactively
|
|
8675
|
-
//
|
|
8676
|
-
// Default: RunCommand, WriteFile, EditFile require approval unless overridden.
|
|
8677
|
-
controllers: {
|
|
8678
|
-
beforeGadgetExecution: async (ctx) => {
|
|
8679
|
-
const mode = approvalManager.getApprovalMode(ctx.gadgetName);
|
|
8680
|
-
if (mode === "allowed") {
|
|
8681
|
-
return { action: "proceed" };
|
|
8682
|
-
}
|
|
8683
|
-
const stdinTTY = isInteractive(env.stdin);
|
|
8684
|
-
const stderrTTY2 = env.stderr.isTTY === true;
|
|
8685
|
-
const canPrompt = stdinTTY && stderrTTY2;
|
|
8686
|
-
if (!canPrompt) {
|
|
8687
|
-
if (mode === "approval-required") {
|
|
8688
|
-
return {
|
|
8689
|
-
action: "skip",
|
|
8690
|
-
syntheticResult: `status=denied
|
|
8691
|
-
|
|
8692
|
-
${ctx.gadgetName} requires interactive approval. Run in a terminal to approve.`
|
|
8693
|
-
};
|
|
8694
|
-
}
|
|
8695
|
-
if (mode === "denied") {
|
|
8696
|
-
return {
|
|
8697
|
-
action: "skip",
|
|
8698
|
-
syntheticResult: `status=denied
|
|
8699
|
-
|
|
8700
|
-
${ctx.gadgetName} is denied by configuration.`
|
|
8701
|
-
};
|
|
8702
|
-
}
|
|
8703
|
-
return { action: "proceed" };
|
|
8704
|
-
}
|
|
8705
|
-
const result = await approvalManager.requestApproval(ctx.gadgetName, ctx.parameters);
|
|
8706
|
-
if (!result.approved) {
|
|
8707
|
-
return {
|
|
8708
|
-
action: "skip",
|
|
8709
|
-
syntheticResult: `status=denied
|
|
8710
|
-
|
|
8711
|
-
Denied: ${result.reason ?? "by user"}`
|
|
8712
|
-
};
|
|
8713
|
-
}
|
|
8714
|
-
return { action: "proceed" };
|
|
8715
|
-
}
|
|
8716
|
-
}
|
|
8717
|
-
});
|
|
8718
|
-
if (options.system) {
|
|
8719
|
-
builder.withSystem(options.system);
|
|
8811
|
+
if ("max-iterations" in rawObj) {
|
|
8812
|
+
result["max-iterations"] = validateNumber(rawObj["max-iterations"], "max-iterations", section, {
|
|
8813
|
+
integer: true,
|
|
8814
|
+
min: 1
|
|
8815
|
+
});
|
|
8720
8816
|
}
|
|
8721
|
-
if (
|
|
8722
|
-
|
|
8817
|
+
if ("gadgets" in rawObj) {
|
|
8818
|
+
result.gadgets = validateStringArray(rawObj.gadgets, "gadgets", section);
|
|
8723
8819
|
}
|
|
8724
|
-
if (
|
|
8725
|
-
|
|
8820
|
+
if ("gadget-add" in rawObj) {
|
|
8821
|
+
result["gadget-add"] = validateStringArray(rawObj["gadget-add"], "gadget-add", section);
|
|
8726
8822
|
}
|
|
8727
|
-
|
|
8728
|
-
|
|
8729
|
-
builder.onHumanInput(humanInputHandler);
|
|
8823
|
+
if ("gadget-remove" in rawObj) {
|
|
8824
|
+
result["gadget-remove"] = validateStringArray(rawObj["gadget-remove"], "gadget-remove", section);
|
|
8730
8825
|
}
|
|
8731
|
-
|
|
8732
|
-
|
|
8733
|
-
if (gadgets.length > 0) {
|
|
8734
|
-
builder.withGadgets(...gadgets);
|
|
8826
|
+
if ("gadget" in rawObj) {
|
|
8827
|
+
result.gadget = validateStringArray(rawObj.gadget, "gadget", section);
|
|
8735
8828
|
}
|
|
8736
|
-
if (
|
|
8737
|
-
|
|
8829
|
+
if ("builtins" in rawObj) {
|
|
8830
|
+
result.builtins = validateBoolean(rawObj.builtins, "builtins", section);
|
|
8738
8831
|
}
|
|
8739
|
-
if (
|
|
8740
|
-
|
|
8832
|
+
if ("builtin-interaction" in rawObj) {
|
|
8833
|
+
result["builtin-interaction"] = validateBoolean(
|
|
8834
|
+
rawObj["builtin-interaction"],
|
|
8835
|
+
"builtin-interaction",
|
|
8836
|
+
section
|
|
8837
|
+
);
|
|
8741
8838
|
}
|
|
8742
|
-
if (
|
|
8743
|
-
|
|
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
|
+
);
|
|
8744
8845
|
}
|
|
8745
|
-
|
|
8746
|
-
"
|
|
8747
|
-
|
|
8748
|
-
|
|
8749
|
-
|
|
8750
|
-
|
|
8751
|
-
},
|
|
8752
|
-
"\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?"
|
|
8753
|
-
);
|
|
8754
|
-
builder.withTextOnlyHandler("acknowledge");
|
|
8755
|
-
builder.withTextWithGadgetsHandler({
|
|
8756
|
-
gadgetName: "TellUser",
|
|
8757
|
-
parameterMapping: (text) => ({ message: text, done: false, type: "info" }),
|
|
8758
|
-
resultMapping: (text) => `\u2139\uFE0F ${text}`
|
|
8759
|
-
});
|
|
8760
|
-
const agent = builder.ask(prompt);
|
|
8761
|
-
let textBuffer = "";
|
|
8762
|
-
const flushTextBuffer = () => {
|
|
8763
|
-
if (textBuffer) {
|
|
8764
|
-
const output = options.quiet ? textBuffer : renderMarkdownWithSeparators(textBuffer);
|
|
8765
|
-
printer.write(output);
|
|
8766
|
-
textBuffer = "";
|
|
8767
|
-
}
|
|
8768
|
-
};
|
|
8769
|
-
try {
|
|
8770
|
-
for await (const event of agent.run()) {
|
|
8771
|
-
if (event.type === "text") {
|
|
8772
|
-
progress.pause();
|
|
8773
|
-
textBuffer += event.content;
|
|
8774
|
-
} else if (event.type === "gadget_result") {
|
|
8775
|
-
flushTextBuffer();
|
|
8776
|
-
progress.pause();
|
|
8777
|
-
if (options.quiet) {
|
|
8778
|
-
if (event.result.gadgetName === "TellUser" && event.result.parameters?.message) {
|
|
8779
|
-
const message = String(event.result.parameters.message);
|
|
8780
|
-
env.stdout.write(`${message}
|
|
8781
|
-
`);
|
|
8782
|
-
}
|
|
8783
|
-
} else {
|
|
8784
|
-
const tokenCount = await countGadgetOutputTokens(event.result.result);
|
|
8785
|
-
env.stderr.write(`${formatGadgetSummary({ ...event.result, tokenCount })}
|
|
8786
|
-
`);
|
|
8787
|
-
}
|
|
8788
|
-
}
|
|
8789
|
-
}
|
|
8790
|
-
} catch (error) {
|
|
8791
|
-
if (!isAbortError(error)) {
|
|
8792
|
-
throw error;
|
|
8793
|
-
}
|
|
8794
|
-
} finally {
|
|
8795
|
-
isStreaming = false;
|
|
8796
|
-
keyboard.cleanupEsc?.();
|
|
8797
|
-
keyboard.cleanupSigint?.();
|
|
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
|
+
);
|
|
8798
8852
|
}
|
|
8799
|
-
|
|
8800
|
-
|
|
8801
|
-
|
|
8802
|
-
|
|
8803
|
-
|
|
8804
|
-
|
|
8805
|
-
|
|
8806
|
-
|
|
8807
|
-
|
|
8808
|
-
|
|
8809
|
-
|
|
8810
|
-
|
|
8811
|
-
|
|
8812
|
-
|
|
8813
|
-
|
|
8814
|
-
|
|
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
|
+
);
|
|
8815
8879
|
}
|
|
8880
|
+
return result;
|
|
8816
8881
|
}
|
|
8817
|
-
function
|
|
8818
|
-
|
|
8819
|
-
|
|
8820
|
-
|
|
8821
|
-
|
|
8822
|
-
const mergedOptions = {
|
|
8823
|
-
...options,
|
|
8824
|
-
gadgetApproval: config?.["gadget-approval"]
|
|
8825
|
-
};
|
|
8826
|
-
return executeAgent(prompt, mergedOptions, env);
|
|
8827
|
-
}, env)
|
|
8828
|
-
);
|
|
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`);
|
|
8829
8887
|
}
|
|
8830
|
-
|
|
8831
|
-
|
|
8832
|
-
|
|
8833
|
-
init_model_shortcuts();
|
|
8834
|
-
init_constants2();
|
|
8835
|
-
async function executeComplete(promptArg, options, env) {
|
|
8836
|
-
const prompt = await resolvePrompt(promptArg, env);
|
|
8837
|
-
const client = env.createClient();
|
|
8838
|
-
const model = resolveModel(options.model);
|
|
8839
|
-
const builder = new LLMMessageBuilder();
|
|
8840
|
-
if (options.system) {
|
|
8841
|
-
builder.addSystem(options.system);
|
|
8888
|
+
function validateCustomConfig(raw, section) {
|
|
8889
|
+
if (typeof raw !== "object" || raw === null) {
|
|
8890
|
+
throw new ConfigError(`[${section}] must be a table`);
|
|
8842
8891
|
}
|
|
8843
|
-
|
|
8844
|
-
const
|
|
8845
|
-
|
|
8846
|
-
|
|
8847
|
-
const timestamp = Date.now();
|
|
8848
|
-
if (llmRequestsDir) {
|
|
8849
|
-
const filename = `${timestamp}_complete.request.txt`;
|
|
8850
|
-
const content = formatLlmRequest(messages);
|
|
8851
|
-
await writeLogFile(llmRequestsDir, filename, content);
|
|
8852
|
-
}
|
|
8853
|
-
const stream2 = client.stream({
|
|
8854
|
-
model,
|
|
8855
|
-
messages,
|
|
8856
|
-
temperature: options.temperature,
|
|
8857
|
-
maxTokens: options.maxTokens
|
|
8858
|
-
});
|
|
8859
|
-
const printer = new StreamPrinter(env.stdout);
|
|
8860
|
-
const stderrTTY = env.stderr.isTTY === true;
|
|
8861
|
-
const progress = new StreamProgress(env.stderr, stderrTTY, client.modelRegistry);
|
|
8862
|
-
const estimatedInputTokens = Math.round(prompt.length / FALLBACK_CHARS_PER_TOKEN);
|
|
8863
|
-
progress.startCall(model, estimatedInputTokens);
|
|
8864
|
-
let finishReason;
|
|
8865
|
-
let usage;
|
|
8866
|
-
let accumulatedResponse = "";
|
|
8867
|
-
for await (const chunk of stream2) {
|
|
8868
|
-
if (chunk.usage) {
|
|
8869
|
-
usage = chunk.usage;
|
|
8870
|
-
if (chunk.usage.inputTokens) {
|
|
8871
|
-
progress.setInputTokens(chunk.usage.inputTokens, false);
|
|
8872
|
-
}
|
|
8873
|
-
if (chunk.usage.outputTokens) {
|
|
8874
|
-
progress.setOutputTokens(chunk.usage.outputTokens, false);
|
|
8875
|
-
}
|
|
8876
|
-
}
|
|
8877
|
-
if (chunk.text) {
|
|
8878
|
-
progress.pause();
|
|
8879
|
-
accumulatedResponse += chunk.text;
|
|
8880
|
-
progress.update(accumulatedResponse.length);
|
|
8881
|
-
printer.write(chunk.text);
|
|
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`);
|
|
8882
8896
|
}
|
|
8883
|
-
|
|
8884
|
-
|
|
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"`);
|
|
8885
8903
|
}
|
|
8904
|
+
type = typeValue;
|
|
8886
8905
|
}
|
|
8887
|
-
|
|
8888
|
-
|
|
8889
|
-
|
|
8890
|
-
|
|
8891
|
-
|
|
8892
|
-
|
|
8906
|
+
const result = {
|
|
8907
|
+
...validateBaseConfig(rawObj, section),
|
|
8908
|
+
type
|
|
8909
|
+
};
|
|
8910
|
+
if ("description" in rawObj) {
|
|
8911
|
+
result.description = validateString(rawObj.description, "description", section);
|
|
8893
8912
|
}
|
|
8894
|
-
if (
|
|
8895
|
-
|
|
8896
|
-
|
|
8897
|
-
|
|
8898
|
-
|
|
8899
|
-
|
|
8913
|
+
if ("max-iterations" in rawObj) {
|
|
8914
|
+
result["max-iterations"] = validateNumber(rawObj["max-iterations"], "max-iterations", section, {
|
|
8915
|
+
integer: true,
|
|
8916
|
+
min: 1
|
|
8917
|
+
});
|
|
8918
|
+
}
|
|
8919
|
+
if ("gadgets" in rawObj) {
|
|
8920
|
+
result.gadgets = validateStringArray(rawObj.gadgets, "gadgets", section);
|
|
8921
|
+
}
|
|
8922
|
+
if ("gadget-add" in rawObj) {
|
|
8923
|
+
result["gadget-add"] = validateStringArray(rawObj["gadget-add"], "gadget-add", section);
|
|
8924
|
+
}
|
|
8925
|
+
if ("gadget-remove" in rawObj) {
|
|
8926
|
+
result["gadget-remove"] = validateStringArray(rawObj["gadget-remove"], "gadget-remove", section);
|
|
8927
|
+
}
|
|
8928
|
+
if ("gadget" in rawObj) {
|
|
8929
|
+
result.gadget = validateStringArray(rawObj.gadget, "gadget", section);
|
|
8930
|
+
}
|
|
8931
|
+
if ("builtins" in rawObj) {
|
|
8932
|
+
result.builtins = validateBoolean(rawObj.builtins, "builtins", section);
|
|
8933
|
+
}
|
|
8934
|
+
if ("builtin-interaction" in rawObj) {
|
|
8935
|
+
result["builtin-interaction"] = validateBoolean(
|
|
8936
|
+
rawObj["builtin-interaction"],
|
|
8937
|
+
"builtin-interaction",
|
|
8938
|
+
section
|
|
8939
|
+
);
|
|
8940
|
+
}
|
|
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
|
+
);
|
|
8947
|
+
}
|
|
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
|
+
);
|
|
8954
|
+
}
|
|
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
|
+
);
|
|
8961
|
+
}
|
|
8962
|
+
if ("gadget-approval" in rawObj) {
|
|
8963
|
+
result["gadget-approval"] = validateGadgetApproval(rawObj["gadget-approval"], section);
|
|
8964
|
+
}
|
|
8965
|
+
if ("max-tokens" in rawObj) {
|
|
8966
|
+
result["max-tokens"] = validateNumber(rawObj["max-tokens"], "max-tokens", section, {
|
|
8967
|
+
integer: true,
|
|
8968
|
+
min: 1
|
|
8969
|
+
});
|
|
8970
|
+
}
|
|
8971
|
+
if ("quiet" in rawObj) {
|
|
8972
|
+
result.quiet = validateBoolean(rawObj.quiet, "quiet", section);
|
|
8900
8973
|
}
|
|
8974
|
+
Object.assign(result, validateLoggingConfig(rawObj, section));
|
|
8975
|
+
return result;
|
|
8901
8976
|
}
|
|
8902
|
-
function
|
|
8903
|
-
|
|
8904
|
-
|
|
8905
|
-
|
|
8906
|
-
|
|
8907
|
-
)
|
|
8977
|
+
function validatePromptsConfig(raw, section) {
|
|
8978
|
+
if (typeof raw !== "object" || raw === null) {
|
|
8979
|
+
throw new ConfigError(`[${section}] must be a table`);
|
|
8980
|
+
}
|
|
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;
|
|
8908
8989
|
}
|
|
8909
|
-
|
|
8910
|
-
|
|
8911
|
-
|
|
8912
|
-
var import_node_os2 = require("os");
|
|
8913
|
-
var import_node_path8 = require("path");
|
|
8914
|
-
var import_js_toml = require("js-toml");
|
|
8915
|
-
|
|
8916
|
-
// src/cli/templates.ts
|
|
8917
|
-
var import_eta = require("eta");
|
|
8918
|
-
var TemplateError = class extends Error {
|
|
8919
|
-
constructor(message, promptName, configPath) {
|
|
8920
|
-
super(promptName ? `[prompts.${promptName}]: ${message}` : message);
|
|
8921
|
-
this.promptName = promptName;
|
|
8922
|
-
this.configPath = configPath;
|
|
8923
|
-
this.name = "TemplateError";
|
|
8990
|
+
function validateConfig(raw, configPath) {
|
|
8991
|
+
if (typeof raw !== "object" || raw === null) {
|
|
8992
|
+
throw new ConfigError("Config must be a TOML table", configPath);
|
|
8924
8993
|
}
|
|
8925
|
-
|
|
8926
|
-
|
|
8927
|
-
const
|
|
8928
|
-
views: "/",
|
|
8929
|
-
// Required but we use named templates
|
|
8930
|
-
autoEscape: false,
|
|
8931
|
-
// Don't escape - these are prompts, not HTML
|
|
8932
|
-
autoTrim: false
|
|
8933
|
-
// Preserve whitespace in prompts
|
|
8934
|
-
});
|
|
8935
|
-
for (const [name, template] of Object.entries(prompts)) {
|
|
8994
|
+
const rawObj = raw;
|
|
8995
|
+
const result = {};
|
|
8996
|
+
for (const [key, value] of Object.entries(rawObj)) {
|
|
8936
8997
|
try {
|
|
8937
|
-
|
|
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
|
+
}
|
|
8938
9011
|
} catch (error) {
|
|
8939
|
-
|
|
8940
|
-
|
|
8941
|
-
|
|
8942
|
-
|
|
8943
|
-
);
|
|
9012
|
+
if (error instanceof ConfigError) {
|
|
9013
|
+
throw new ConfigError(error.message, configPath);
|
|
9014
|
+
}
|
|
9015
|
+
throw error;
|
|
8944
9016
|
}
|
|
8945
9017
|
}
|
|
8946
|
-
return
|
|
9018
|
+
return result;
|
|
8947
9019
|
}
|
|
8948
|
-
function
|
|
9020
|
+
function loadConfig() {
|
|
9021
|
+
const configPath = getConfigPath();
|
|
9022
|
+
if (!(0, import_node_fs8.existsSync)(configPath)) {
|
|
9023
|
+
return {};
|
|
9024
|
+
}
|
|
9025
|
+
let content;
|
|
8949
9026
|
try {
|
|
8950
|
-
|
|
8951
|
-
...context,
|
|
8952
|
-
env: process.env,
|
|
8953
|
-
date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
|
|
8954
|
-
// "2025-12-01"
|
|
8955
|
-
};
|
|
8956
|
-
return eta.renderString(template, fullContext);
|
|
9027
|
+
content = (0, import_node_fs8.readFileSync)(configPath, "utf-8");
|
|
8957
9028
|
} catch (error) {
|
|
8958
|
-
throw new
|
|
8959
|
-
error instanceof Error ? error.message :
|
|
8960
|
-
void 0,
|
|
9029
|
+
throw new ConfigError(
|
|
9030
|
+
`Failed to read config file: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
8961
9031
|
configPath
|
|
8962
9032
|
);
|
|
8963
9033
|
}
|
|
8964
|
-
|
|
8965
|
-
|
|
8966
|
-
|
|
8967
|
-
|
|
8968
|
-
|
|
8969
|
-
|
|
8970
|
-
|
|
8971
|
-
|
|
8972
|
-
error instanceof Error ? error.message : String(error),
|
|
8973
|
-
name,
|
|
8974
|
-
configPath
|
|
8975
|
-
);
|
|
8976
|
-
}
|
|
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"}`,
|
|
9040
|
+
configPath
|
|
9041
|
+
);
|
|
8977
9042
|
}
|
|
9043
|
+
const validated = validateConfig(raw, configPath);
|
|
9044
|
+
const inherited = resolveInheritance(validated, configPath);
|
|
9045
|
+
return resolveTemplatesInConfig(inherited, configPath);
|
|
8978
9046
|
}
|
|
8979
|
-
function
|
|
8980
|
-
const
|
|
8981
|
-
|
|
8982
|
-
for (const match of matches) {
|
|
8983
|
-
const varName = match[1];
|
|
8984
|
-
if (process.env[varName] === void 0) {
|
|
8985
|
-
throw new TemplateError(
|
|
8986
|
-
`Environment variable '${varName}' is not set`,
|
|
8987
|
-
promptName,
|
|
8988
|
-
configPath
|
|
8989
|
-
);
|
|
8990
|
-
}
|
|
8991
|
-
}
|
|
8992
|
-
}
|
|
8993
|
-
function hasTemplateSyntax(str) {
|
|
8994
|
-
return str.includes("<%");
|
|
8995
|
-
}
|
|
8996
|
-
|
|
8997
|
-
// src/cli/config.ts
|
|
8998
|
-
var VALID_APPROVAL_MODES = ["allowed", "denied", "approval-required"];
|
|
8999
|
-
var GLOBAL_CONFIG_KEYS = /* @__PURE__ */ new Set(["log-level", "log-file", "log-reset"]);
|
|
9000
|
-
var VALID_LOG_LEVELS = ["silly", "trace", "debug", "info", "warn", "error", "fatal"];
|
|
9001
|
-
var COMPLETE_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
9002
|
-
"model",
|
|
9003
|
-
"system",
|
|
9004
|
-
"temperature",
|
|
9005
|
-
"max-tokens",
|
|
9006
|
-
"quiet",
|
|
9007
|
-
"inherits",
|
|
9008
|
-
"log-level",
|
|
9009
|
-
"log-file",
|
|
9010
|
-
"log-reset",
|
|
9011
|
-
"log-llm-requests",
|
|
9012
|
-
"log-llm-responses",
|
|
9013
|
-
"type"
|
|
9014
|
-
// Allowed for inheritance compatibility, ignored for built-in commands
|
|
9015
|
-
]);
|
|
9016
|
-
var AGENT_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
9017
|
-
"model",
|
|
9018
|
-
"system",
|
|
9019
|
-
"temperature",
|
|
9020
|
-
"max-iterations",
|
|
9021
|
-
"gadgets",
|
|
9022
|
-
// Full replacement (preferred)
|
|
9023
|
-
"gadget-add",
|
|
9024
|
-
// Add to inherited gadgets
|
|
9025
|
-
"gadget-remove",
|
|
9026
|
-
// Remove from inherited gadgets
|
|
9027
|
-
"gadget",
|
|
9028
|
-
// DEPRECATED: alias for gadgets
|
|
9029
|
-
"builtins",
|
|
9030
|
-
"builtin-interaction",
|
|
9031
|
-
"gadget-start-prefix",
|
|
9032
|
-
"gadget-end-prefix",
|
|
9033
|
-
"gadget-arg-prefix",
|
|
9034
|
-
"gadget-approval",
|
|
9035
|
-
"quiet",
|
|
9036
|
-
"inherits",
|
|
9037
|
-
"log-level",
|
|
9038
|
-
"log-file",
|
|
9039
|
-
"log-reset",
|
|
9040
|
-
"log-llm-requests",
|
|
9041
|
-
"log-llm-responses",
|
|
9042
|
-
"type"
|
|
9043
|
-
// Allowed for inheritance compatibility, ignored for built-in commands
|
|
9044
|
-
]);
|
|
9045
|
-
var CUSTOM_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
9046
|
-
...COMPLETE_CONFIG_KEYS,
|
|
9047
|
-
...AGENT_CONFIG_KEYS,
|
|
9048
|
-
"type",
|
|
9049
|
-
"description"
|
|
9050
|
-
]);
|
|
9051
|
-
function getConfigPath() {
|
|
9052
|
-
return (0, import_node_path8.join)((0, import_node_os2.homedir)(), ".llmist", "cli.toml");
|
|
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));
|
|
9053
9050
|
}
|
|
9054
|
-
|
|
9055
|
-
|
|
9056
|
-
|
|
9057
|
-
|
|
9058
|
-
|
|
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
|
+
}
|
|
9059
9063
|
}
|
|
9060
|
-
|
|
9061
|
-
|
|
9062
|
-
|
|
9063
|
-
|
|
9064
|
+
for (const template of Object.values(prompts)) {
|
|
9065
|
+
if (hasTemplateSyntax(template)) {
|
|
9066
|
+
hasTemplates = true;
|
|
9067
|
+
break;
|
|
9068
|
+
}
|
|
9064
9069
|
}
|
|
9065
|
-
|
|
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
|
+
}
|
|
9081
|
+
for (const [name, template] of Object.entries(prompts)) {
|
|
9082
|
+
try {
|
|
9083
|
+
validateEnvVars(template, name, configPath);
|
|
9084
|
+
} catch (error) {
|
|
9085
|
+
if (error instanceof TemplateError) {
|
|
9086
|
+
throw new ConfigError(error.message, configPath);
|
|
9087
|
+
}
|
|
9088
|
+
throw error;
|
|
9089
|
+
}
|
|
9090
|
+
}
|
|
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
|
+
}
|
|
9118
|
+
}
|
|
9119
|
+
}
|
|
9120
|
+
return result;
|
|
9066
9121
|
}
|
|
9067
|
-
function
|
|
9068
|
-
|
|
9069
|
-
|
|
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
|
+
);
|
|
9070
9131
|
}
|
|
9071
|
-
if (
|
|
9072
|
-
throw new ConfigError(
|
|
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
|
+
);
|
|
9073
9137
|
}
|
|
9074
|
-
if (
|
|
9075
|
-
|
|
9138
|
+
if (hasGadgets) {
|
|
9139
|
+
return section.gadgets;
|
|
9076
9140
|
}
|
|
9077
|
-
if (
|
|
9078
|
-
|
|
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;
|
|
9154
|
+
}
|
|
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;
|
|
9198
|
+
}
|
|
9199
|
+
for (const name of Object.keys(config)) {
|
|
9200
|
+
resolveSection(name);
|
|
9201
|
+
}
|
|
9202
|
+
return resolved;
|
|
9203
|
+
}
|
|
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`);
|
|
9079
9210
|
}
|
|
9080
9211
|
return value;
|
|
9081
9212
|
}
|
|
9082
|
-
function
|
|
9213
|
+
function validateBoolean2(value, key, section) {
|
|
9083
9214
|
if (typeof value !== "boolean") {
|
|
9084
9215
|
throw new ConfigError(`[${section}].${key} must be a boolean`);
|
|
9085
9216
|
}
|
|
9086
9217
|
return value;
|
|
9087
9218
|
}
|
|
9088
|
-
function
|
|
9219
|
+
function validateStringArray2(value, key, section) {
|
|
9089
9220
|
if (!Array.isArray(value)) {
|
|
9090
9221
|
throw new ConfigError(`[${section}].${key} must be an array`);
|
|
9091
9222
|
}
|
|
@@ -9096,535 +9227,949 @@ function validateStringArray(value, key, section) {
|
|
|
9096
9227
|
}
|
|
9097
9228
|
return value;
|
|
9098
9229
|
}
|
|
9099
|
-
function
|
|
9100
|
-
|
|
9101
|
-
|
|
9102
|
-
|
|
9103
|
-
|
|
9104
|
-
|
|
9105
|
-
if (typeof value[i] !== "string") {
|
|
9106
|
-
throw new ConfigError(`[${section}].inherits[${i}] must be a string`);
|
|
9107
|
-
}
|
|
9108
|
-
}
|
|
9109
|
-
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
|
+
);
|
|
9110
9236
|
}
|
|
9111
|
-
|
|
9237
|
+
return str;
|
|
9112
9238
|
}
|
|
9113
|
-
function
|
|
9239
|
+
function validateMountConfig(value, index, section) {
|
|
9114
9240
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
9115
|
-
throw new ConfigError(
|
|
9116
|
-
`[${section}].gadget-approval must be a table (e.g., { WriteFile = "approval-required" })`
|
|
9117
|
-
);
|
|
9241
|
+
throw new ConfigError(`[${section}].mounts[${index}] must be a table`);
|
|
9118
9242
|
}
|
|
9119
|
-
const
|
|
9120
|
-
|
|
9121
|
-
|
|
9122
|
-
|
|
9123
|
-
|
|
9124
|
-
);
|
|
9125
|
-
}
|
|
9126
|
-
if (!VALID_APPROVAL_MODES.includes(mode)) {
|
|
9127
|
-
throw new ConfigError(
|
|
9128
|
-
`[${section}].gadget-approval.${gadgetName} must be one of: ${VALID_APPROVAL_MODES.join(", ")}`
|
|
9129
|
-
);
|
|
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`);
|
|
9130
9248
|
}
|
|
9131
|
-
result[gadgetName] = mode;
|
|
9132
9249
|
}
|
|
9133
|
-
|
|
9134
|
-
}
|
|
9135
|
-
function validateLoggingConfig(raw, section) {
|
|
9136
|
-
const result = {};
|
|
9137
|
-
if ("log-level" in raw) {
|
|
9138
|
-
const level = validateString(raw["log-level"], "log-level", section);
|
|
9139
|
-
if (!VALID_LOG_LEVELS.includes(level)) {
|
|
9140
|
-
throw new ConfigError(
|
|
9141
|
-
`[${section}].log-level must be one of: ${VALID_LOG_LEVELS.join(", ")}`
|
|
9142
|
-
);
|
|
9143
|
-
}
|
|
9144
|
-
result["log-level"] = level;
|
|
9250
|
+
if (!("source" in rawObj)) {
|
|
9251
|
+
throw new ConfigError(`[${mountSection}] missing required field 'source'`);
|
|
9145
9252
|
}
|
|
9146
|
-
if ("
|
|
9147
|
-
|
|
9253
|
+
if (!("target" in rawObj)) {
|
|
9254
|
+
throw new ConfigError(`[${mountSection}] missing required field 'target'`);
|
|
9148
9255
|
}
|
|
9149
|
-
if ("
|
|
9150
|
-
|
|
9256
|
+
if (!("permission" in rawObj)) {
|
|
9257
|
+
throw new ConfigError(`[${mountSection}] missing required field 'permission'`);
|
|
9151
9258
|
}
|
|
9152
|
-
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
|
+
};
|
|
9153
9264
|
}
|
|
9154
|
-
function
|
|
9155
|
-
|
|
9156
|
-
|
|
9157
|
-
result.model = validateString(raw.model, "model", section);
|
|
9158
|
-
}
|
|
9159
|
-
if ("system" in raw) {
|
|
9160
|
-
result.system = validateString(raw.system, "system", section);
|
|
9161
|
-
}
|
|
9162
|
-
if ("temperature" in raw) {
|
|
9163
|
-
result.temperature = validateNumber(raw.temperature, "temperature", section, {
|
|
9164
|
-
min: 0,
|
|
9165
|
-
max: 2
|
|
9166
|
-
});
|
|
9265
|
+
function validateMountsArray(value, section) {
|
|
9266
|
+
if (!Array.isArray(value)) {
|
|
9267
|
+
throw new ConfigError(`[${section}].mounts must be an array of tables`);
|
|
9167
9268
|
}
|
|
9168
|
-
|
|
9169
|
-
|
|
9269
|
+
const result = [];
|
|
9270
|
+
for (let i = 0; i < value.length; i++) {
|
|
9271
|
+
result.push(validateMountConfig(value[i], i, section));
|
|
9170
9272
|
}
|
|
9171
9273
|
return result;
|
|
9172
9274
|
}
|
|
9173
|
-
function
|
|
9174
|
-
if (typeof raw !== "object" || raw === null) {
|
|
9175
|
-
throw new ConfigError(`[${section}] must be a table`);
|
|
9176
|
-
}
|
|
9177
|
-
const rawObj = raw;
|
|
9178
|
-
for (const key of Object.keys(rawObj)) {
|
|
9179
|
-
if (!GLOBAL_CONFIG_KEYS.has(key)) {
|
|
9180
|
-
throw new ConfigError(`[${section}].${key} is not a valid option`);
|
|
9181
|
-
}
|
|
9182
|
-
}
|
|
9183
|
-
return validateLoggingConfig(rawObj, section);
|
|
9184
|
-
}
|
|
9185
|
-
function validateCompleteConfig(raw, section) {
|
|
9275
|
+
function validateDockerConfig(raw, section) {
|
|
9186
9276
|
if (typeof raw !== "object" || raw === null) {
|
|
9187
9277
|
throw new ConfigError(`[${section}] must be a table`);
|
|
9188
9278
|
}
|
|
9189
9279
|
const rawObj = raw;
|
|
9190
9280
|
for (const key of Object.keys(rawObj)) {
|
|
9191
|
-
if (!
|
|
9281
|
+
if (!DOCKER_CONFIG_KEYS.has(key)) {
|
|
9192
9282
|
throw new ConfigError(`[${section}].${key} is not a valid option`);
|
|
9193
9283
|
}
|
|
9194
9284
|
}
|
|
9195
|
-
const result = {
|
|
9196
|
-
|
|
9197
|
-
|
|
9198
|
-
};
|
|
9199
|
-
if ("max-tokens" in rawObj) {
|
|
9200
|
-
result["max-tokens"] = validateNumber(rawObj["max-tokens"], "max-tokens", section, {
|
|
9201
|
-
integer: true,
|
|
9202
|
-
min: 1
|
|
9203
|
-
});
|
|
9285
|
+
const result = {};
|
|
9286
|
+
if ("enabled" in rawObj) {
|
|
9287
|
+
result.enabled = validateBoolean2(rawObj.enabled, "enabled", section);
|
|
9204
9288
|
}
|
|
9205
|
-
if ("
|
|
9206
|
-
result.
|
|
9289
|
+
if ("dockerfile" in rawObj) {
|
|
9290
|
+
result.dockerfile = validateString2(rawObj.dockerfile, "dockerfile", section);
|
|
9207
9291
|
}
|
|
9208
|
-
if ("
|
|
9209
|
-
result["
|
|
9210
|
-
rawObj["
|
|
9211
|
-
"
|
|
9292
|
+
if ("cwd-permission" in rawObj) {
|
|
9293
|
+
result["cwd-permission"] = validateMountPermission(
|
|
9294
|
+
rawObj["cwd-permission"],
|
|
9295
|
+
"cwd-permission",
|
|
9212
9296
|
section
|
|
9213
9297
|
);
|
|
9214
9298
|
}
|
|
9215
|
-
if ("
|
|
9216
|
-
result["
|
|
9217
|
-
rawObj["
|
|
9218
|
-
"
|
|
9299
|
+
if ("config-permission" in rawObj) {
|
|
9300
|
+
result["config-permission"] = validateMountPermission(
|
|
9301
|
+
rawObj["config-permission"],
|
|
9302
|
+
"config-permission",
|
|
9219
9303
|
section
|
|
9220
9304
|
);
|
|
9221
9305
|
}
|
|
9306
|
+
if ("mounts" in rawObj) {
|
|
9307
|
+
result.mounts = validateMountsArray(rawObj.mounts, section);
|
|
9308
|
+
}
|
|
9309
|
+
if ("env-vars" in rawObj) {
|
|
9310
|
+
result["env-vars"] = validateStringArray2(rawObj["env-vars"], "env-vars", section);
|
|
9311
|
+
}
|
|
9312
|
+
if ("image-name" in rawObj) {
|
|
9313
|
+
result["image-name"] = validateString2(rawObj["image-name"], "image-name", section);
|
|
9314
|
+
}
|
|
9315
|
+
if ("dev-mode" in rawObj) {
|
|
9316
|
+
result["dev-mode"] = validateBoolean2(rawObj["dev-mode"], "dev-mode", section);
|
|
9317
|
+
}
|
|
9318
|
+
if ("dev-source" in rawObj) {
|
|
9319
|
+
result["dev-source"] = validateString2(rawObj["dev-source"], "dev-source", section);
|
|
9320
|
+
}
|
|
9222
9321
|
return result;
|
|
9223
9322
|
}
|
|
9224
|
-
|
|
9225
|
-
|
|
9226
|
-
|
|
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;
|
|
9227
9385
|
}
|
|
9228
|
-
|
|
9229
|
-
|
|
9230
|
-
|
|
9231
|
-
|
|
9232
|
-
|
|
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 });
|
|
9233
9403
|
}
|
|
9234
|
-
|
|
9235
|
-
|
|
9236
|
-
|
|
9237
|
-
|
|
9238
|
-
|
|
9239
|
-
result["max-iterations"] = validateNumber(rawObj["max-iterations"], "max-iterations", section, {
|
|
9240
|
-
integer: true,
|
|
9241
|
-
min: 1
|
|
9242
|
-
});
|
|
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;
|
|
9243
9409
|
}
|
|
9244
|
-
|
|
9245
|
-
|
|
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;
|
|
9246
9416
|
}
|
|
9247
|
-
|
|
9248
|
-
|
|
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
|
+
}
|
|
9249
9429
|
}
|
|
9250
|
-
|
|
9251
|
-
|
|
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";
|
|
9252
9442
|
}
|
|
9253
|
-
|
|
9254
|
-
|
|
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
|
|
9463
|
+
);
|
|
9255
9464
|
}
|
|
9256
|
-
|
|
9257
|
-
|
|
9465
|
+
}
|
|
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;
|
|
9258
9471
|
}
|
|
9259
|
-
|
|
9260
|
-
|
|
9261
|
-
|
|
9262
|
-
|
|
9263
|
-
|
|
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;
|
|
9477
|
+
}
|
|
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."
|
|
9264
9487
|
);
|
|
9488
|
+
this.name = "DockerUnavailableError";
|
|
9265
9489
|
}
|
|
9266
|
-
|
|
9267
|
-
|
|
9268
|
-
|
|
9269
|
-
|
|
9270
|
-
|
|
9271
|
-
);
|
|
9490
|
+
};
|
|
9491
|
+
var DockerSkipError = class extends Error {
|
|
9492
|
+
constructor() {
|
|
9493
|
+
super("Docker execution skipped - already inside container");
|
|
9494
|
+
this.name = "DockerSkipError";
|
|
9272
9495
|
}
|
|
9273
|
-
|
|
9274
|
-
|
|
9275
|
-
|
|
9276
|
-
|
|
9277
|
-
|
|
9278
|
-
|
|
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;
|
|
9279
9507
|
}
|
|
9280
|
-
|
|
9281
|
-
|
|
9282
|
-
|
|
9283
|
-
|
|
9284
|
-
section
|
|
9285
|
-
);
|
|
9508
|
+
}
|
|
9509
|
+
function isInsideContainer() {
|
|
9510
|
+
if ((0, import_node_fs10.existsSync)("/.dockerenv")) {
|
|
9511
|
+
return true;
|
|
9286
9512
|
}
|
|
9287
|
-
|
|
9288
|
-
|
|
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;
|
|
9517
|
+
}
|
|
9518
|
+
} catch {
|
|
9289
9519
|
}
|
|
9290
|
-
|
|
9291
|
-
|
|
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;
|
|
9292
9526
|
}
|
|
9293
|
-
|
|
9294
|
-
|
|
9295
|
-
|
|
9296
|
-
|
|
9297
|
-
|
|
9298
|
-
);
|
|
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;
|
|
9299
9532
|
}
|
|
9300
|
-
|
|
9301
|
-
|
|
9302
|
-
|
|
9303
|
-
|
|
9304
|
-
|
|
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 {
|
|
9539
|
+
}
|
|
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 };
|
|
9546
|
+
}
|
|
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)."
|
|
9305
9551
|
);
|
|
9306
9552
|
}
|
|
9307
|
-
return
|
|
9553
|
+
return { enabled: true, sourcePath };
|
|
9308
9554
|
}
|
|
9309
|
-
function
|
|
9310
|
-
if (
|
|
9311
|
-
return
|
|
9555
|
+
function expandHome(path5) {
|
|
9556
|
+
if (path5.startsWith("~")) {
|
|
9557
|
+
return path5.replace(/^~/, (0, import_node_os4.homedir)());
|
|
9312
9558
|
}
|
|
9313
|
-
|
|
9559
|
+
return path5;
|
|
9314
9560
|
}
|
|
9315
|
-
function
|
|
9316
|
-
|
|
9317
|
-
|
|
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");
|
|
9318
9569
|
}
|
|
9319
|
-
const
|
|
9320
|
-
|
|
9321
|
-
|
|
9322
|
-
|
|
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`);
|
|
9579
|
+
}
|
|
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}`);
|
|
9323
9584
|
}
|
|
9324
9585
|
}
|
|
9325
|
-
|
|
9326
|
-
|
|
9327
|
-
|
|
9328
|
-
if (typeValue !== "agent" && typeValue !== "complete") {
|
|
9329
|
-
throw new ConfigError(`[${section}].type must be "agent" or "complete"`);
|
|
9586
|
+
for (const key of FORWARDED_API_KEYS) {
|
|
9587
|
+
if (process.env[key]) {
|
|
9588
|
+
args.push("-e", key);
|
|
9330
9589
|
}
|
|
9331
|
-
type = typeValue;
|
|
9332
9590
|
}
|
|
9333
|
-
|
|
9334
|
-
|
|
9335
|
-
|
|
9336
|
-
|
|
9337
|
-
|
|
9338
|
-
|
|
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
|
+
}
|
|
9339
9597
|
}
|
|
9340
|
-
|
|
9341
|
-
|
|
9342
|
-
|
|
9343
|
-
|
|
9344
|
-
|
|
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;
|
|
9345
9609
|
}
|
|
9346
|
-
if (
|
|
9347
|
-
|
|
9610
|
+
if (options.docker || options.dockerRo) {
|
|
9611
|
+
return true;
|
|
9348
9612
|
}
|
|
9349
|
-
if (
|
|
9350
|
-
|
|
9613
|
+
if (profileDocker !== void 0) {
|
|
9614
|
+
return profileDocker;
|
|
9351
9615
|
}
|
|
9352
|
-
|
|
9353
|
-
|
|
9616
|
+
return config?.enabled ?? false;
|
|
9617
|
+
}
|
|
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
|
-
|
|
9356
|
-
|
|
9625
|
+
const available = await checkDockerAvailable();
|
|
9626
|
+
if (!available) {
|
|
9627
|
+
throw new DockerUnavailableError();
|
|
9357
9628
|
}
|
|
9358
|
-
|
|
9359
|
-
|
|
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}`);
|
|
9360
9633
|
}
|
|
9361
|
-
|
|
9362
|
-
|
|
9363
|
-
|
|
9364
|
-
|
|
9365
|
-
|
|
9366
|
-
|
|
9367
|
-
|
|
9368
|
-
if ("gadget-start-prefix" in rawObj) {
|
|
9369
|
-
result["gadget-start-prefix"] = validateString(
|
|
9370
|
-
rawObj["gadget-start-prefix"],
|
|
9371
|
-
"gadget-start-prefix",
|
|
9372
|
-
section
|
|
9373
|
-
);
|
|
9374
|
-
}
|
|
9375
|
-
if ("gadget-end-prefix" in rawObj) {
|
|
9376
|
-
result["gadget-end-prefix"] = validateString(
|
|
9377
|
-
rawObj["gadget-end-prefix"],
|
|
9378
|
-
"gadget-end-prefix",
|
|
9379
|
-
section
|
|
9380
|
-
);
|
|
9381
|
-
}
|
|
9382
|
-
if ("gadget-arg-prefix" in rawObj) {
|
|
9383
|
-
result["gadget-arg-prefix"] = validateString(
|
|
9384
|
-
rawObj["gadget-arg-prefix"],
|
|
9385
|
-
"gadget-arg-prefix",
|
|
9386
|
-
section
|
|
9387
|
-
);
|
|
9388
|
-
}
|
|
9389
|
-
if ("gadget-approval" in rawObj) {
|
|
9390
|
-
result["gadget-approval"] = validateGadgetApproval(rawObj["gadget-approval"], section);
|
|
9391
|
-
}
|
|
9392
|
-
if ("max-tokens" in rawObj) {
|
|
9393
|
-
result["max-tokens"] = validateNumber(rawObj["max-tokens"], "max-tokens", section, {
|
|
9394
|
-
integer: true,
|
|
9395
|
-
min: 1
|
|
9396
|
-
});
|
|
9397
|
-
}
|
|
9398
|
-
if ("quiet" in rawObj) {
|
|
9399
|
-
result.quiet = validateBoolean(rawObj.quiet, "quiet", section);
|
|
9400
|
-
}
|
|
9401
|
-
Object.assign(result, validateLoggingConfig(rawObj, section));
|
|
9402
|
-
return result;
|
|
9403
|
-
}
|
|
9404
|
-
function validatePromptsConfig(raw, section) {
|
|
9405
|
-
if (typeof raw !== "object" || raw === null) {
|
|
9406
|
-
throw new ConfigError(`[${section}] must be a table`);
|
|
9407
|
-
}
|
|
9408
|
-
const result = {};
|
|
9409
|
-
for (const [key, value] of Object.entries(raw)) {
|
|
9410
|
-
if (typeof value !== "string") {
|
|
9411
|
-
throw new ConfigError(`[${section}].${key} must be a string`);
|
|
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;
|
|
9412
9641
|
}
|
|
9413
|
-
|
|
9642
|
+
throw error;
|
|
9414
9643
|
}
|
|
9415
|
-
|
|
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);
|
|
9416
9652
|
}
|
|
9417
|
-
function
|
|
9418
|
-
|
|
9419
|
-
|
|
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;
|
|
9420
9668
|
}
|
|
9421
|
-
|
|
9422
|
-
|
|
9423
|
-
|
|
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 });
|
|
9424
9676
|
try {
|
|
9425
|
-
|
|
9426
|
-
|
|
9427
|
-
|
|
9428
|
-
|
|
9429
|
-
|
|
9430
|
-
|
|
9431
|
-
|
|
9432
|
-
|
|
9433
|
-
|
|
9434
|
-
|
|
9435
|
-
|
|
9436
|
-
|
|
9437
|
-
|
|
9438
|
-
throw new ConfigError(error.message, configPath);
|
|
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
|
+
}
|
|
9439
9690
|
}
|
|
9440
|
-
|
|
9691
|
+
} finally {
|
|
9692
|
+
rl.close();
|
|
9693
|
+
keyboard.restore();
|
|
9441
9694
|
}
|
|
9442
|
-
}
|
|
9443
|
-
return result;
|
|
9695
|
+
};
|
|
9444
9696
|
}
|
|
9445
|
-
function
|
|
9446
|
-
const
|
|
9447
|
-
|
|
9448
|
-
|
|
9449
|
-
|
|
9450
|
-
|
|
9451
|
-
|
|
9452
|
-
|
|
9453
|
-
|
|
9454
|
-
|
|
9455
|
-
|
|
9456
|
-
|
|
9457
|
-
|
|
9458
|
-
|
|
9459
|
-
|
|
9460
|
-
|
|
9461
|
-
|
|
9462
|
-
|
|
9463
|
-
|
|
9464
|
-
|
|
9465
|
-
|
|
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
|
|
9466
9720
|
);
|
|
9721
|
+
try {
|
|
9722
|
+
await executeInDocker(ctx, devMode);
|
|
9723
|
+
} catch (error) {
|
|
9724
|
+
if (error instanceof Error && error.message === "SKIP_DOCKER") {
|
|
9725
|
+
} else {
|
|
9726
|
+
throw error;
|
|
9727
|
+
}
|
|
9728
|
+
}
|
|
9467
9729
|
}
|
|
9468
|
-
const
|
|
9469
|
-
const
|
|
9470
|
-
|
|
9471
|
-
|
|
9472
|
-
|
|
9473
|
-
|
|
9474
|
-
|
|
9475
|
-
|
|
9476
|
-
|
|
9477
|
-
|
|
9478
|
-
const hasPrompts = Object.keys(prompts).length > 0;
|
|
9479
|
-
let hasTemplates = false;
|
|
9480
|
-
for (const [sectionName, section] of Object.entries(config)) {
|
|
9481
|
-
if (sectionName === "global" || sectionName === "prompts") continue;
|
|
9482
|
-
if (!section || typeof section !== "object") continue;
|
|
9483
|
-
const sectionObj = section;
|
|
9484
|
-
if (typeof sectionObj.system === "string" && hasTemplateSyntax(sectionObj.system)) {
|
|
9485
|
-
hasTemplates = true;
|
|
9486
|
-
break;
|
|
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);
|
|
9487
9740
|
}
|
|
9488
9741
|
}
|
|
9489
|
-
|
|
9490
|
-
|
|
9491
|
-
|
|
9492
|
-
|
|
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);
|
|
9493
9747
|
}
|
|
9494
9748
|
}
|
|
9495
|
-
|
|
9496
|
-
|
|
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
|
+
`));
|
|
9764
|
+
}
|
|
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
|
+
}
|
|
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);
|
|
9497
9797
|
}
|
|
9498
|
-
|
|
9499
|
-
|
|
9500
|
-
|
|
9501
|
-
|
|
9502
|
-
|
|
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";
|
|
9503
9816
|
}
|
|
9504
|
-
throw error;
|
|
9505
9817
|
}
|
|
9506
|
-
|
|
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) => {
|
|
9507
9829
|
try {
|
|
9508
|
-
|
|
9509
|
-
} catch
|
|
9510
|
-
|
|
9511
|
-
|
|
9512
|
-
}
|
|
9513
|
-
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);
|
|
9514
9834
|
}
|
|
9515
|
-
}
|
|
9516
|
-
const
|
|
9517
|
-
|
|
9518
|
-
|
|
9519
|
-
|
|
9520
|
-
|
|
9521
|
-
|
|
9522
|
-
|
|
9523
|
-
|
|
9524
|
-
|
|
9525
|
-
|
|
9526
|
-
|
|
9527
|
-
|
|
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);
|
|
9528
9929
|
}
|
|
9529
|
-
throw error;
|
|
9530
9930
|
}
|
|
9531
|
-
|
|
9532
|
-
|
|
9533
|
-
|
|
9534
|
-
|
|
9535
|
-
|
|
9536
|
-
|
|
9537
|
-
|
|
9538
|
-
|
|
9539
|
-
|
|
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
|
+
};
|
|
9540
9978
|
}
|
|
9541
|
-
|
|
9979
|
+
return { action: "proceed" };
|
|
9542
9980
|
}
|
|
9543
9981
|
}
|
|
9982
|
+
});
|
|
9983
|
+
if (options.system) {
|
|
9984
|
+
builder.withSystem(options.system);
|
|
9544
9985
|
}
|
|
9545
|
-
|
|
9546
|
-
|
|
9547
|
-
function resolveGadgets(section, inheritedGadgets, sectionName, configPath) {
|
|
9548
|
-
const hasGadgets = "gadgets" in section;
|
|
9549
|
-
const hasGadgetLegacy = "gadget" in section;
|
|
9550
|
-
const hasGadgetAdd = "gadget-add" in section;
|
|
9551
|
-
const hasGadgetRemove = "gadget-remove" in section;
|
|
9552
|
-
if (hasGadgetLegacy && !hasGadgets) {
|
|
9553
|
-
console.warn(
|
|
9554
|
-
`[config] Warning: [${sectionName}].gadget is deprecated, use 'gadgets' (plural) instead`
|
|
9555
|
-
);
|
|
9986
|
+
if (options.maxIterations !== void 0) {
|
|
9987
|
+
builder.withMaxIterations(options.maxIterations);
|
|
9556
9988
|
}
|
|
9557
|
-
if (
|
|
9558
|
-
|
|
9559
|
-
`[${sectionName}] Cannot use 'gadgets' with 'gadget-add'/'gadget-remove'. Use either full replacement (gadgets) OR modification (gadget-add/gadget-remove).`,
|
|
9560
|
-
configPath
|
|
9561
|
-
);
|
|
9989
|
+
if (options.temperature !== void 0) {
|
|
9990
|
+
builder.withTemperature(options.temperature);
|
|
9562
9991
|
}
|
|
9563
|
-
|
|
9564
|
-
|
|
9992
|
+
const humanInputHandler = createHumanInputHandler(env, progress, keyboard);
|
|
9993
|
+
if (humanInputHandler) {
|
|
9994
|
+
builder.onHumanInput(humanInputHandler);
|
|
9565
9995
|
}
|
|
9566
|
-
|
|
9567
|
-
|
|
9996
|
+
builder.withSignal(abortController.signal);
|
|
9997
|
+
const gadgets = registry.getAll();
|
|
9998
|
+
if (gadgets.length > 0) {
|
|
9999
|
+
builder.withGadgets(...gadgets);
|
|
9568
10000
|
}
|
|
9569
|
-
|
|
9570
|
-
|
|
9571
|
-
const toRemove = new Set(section["gadget-remove"]);
|
|
9572
|
-
result = result.filter((g) => !toRemove.has(g));
|
|
10001
|
+
if (options.gadgetStartPrefix) {
|
|
10002
|
+
builder.withGadgetStartPrefix(options.gadgetStartPrefix);
|
|
9573
10003
|
}
|
|
9574
|
-
if (
|
|
9575
|
-
|
|
9576
|
-
result.push(...toAdd);
|
|
10004
|
+
if (options.gadgetEndPrefix) {
|
|
10005
|
+
builder.withGadgetEndPrefix(options.gadgetEndPrefix);
|
|
9577
10006
|
}
|
|
9578
|
-
|
|
9579
|
-
|
|
9580
|
-
|
|
9581
|
-
|
|
9582
|
-
|
|
9583
|
-
|
|
9584
|
-
|
|
9585
|
-
|
|
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 = "";
|
|
9586
10032
|
}
|
|
9587
|
-
|
|
9588
|
-
|
|
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
|
+
}
|
|
9589
10054
|
}
|
|
9590
|
-
|
|
9591
|
-
if (
|
|
9592
|
-
throw
|
|
10055
|
+
} catch (error) {
|
|
10056
|
+
if (!isAbortError(error)) {
|
|
10057
|
+
throw error;
|
|
9593
10058
|
}
|
|
9594
|
-
|
|
9595
|
-
|
|
9596
|
-
|
|
9597
|
-
|
|
9598
|
-
|
|
9599
|
-
|
|
9600
|
-
|
|
9601
|
-
|
|
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
|
+
`);
|
|
9602
10079
|
}
|
|
9603
|
-
|
|
9604
|
-
|
|
9605
|
-
|
|
9606
|
-
|
|
9607
|
-
|
|
9608
|
-
|
|
9609
|
-
|
|
9610
|
-
|
|
9611
|
-
|
|
9612
|
-
|
|
9613
|
-
|
|
9614
|
-
|
|
9615
|
-
|
|
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;
|
|
9616
10150
|
}
|
|
9617
|
-
delete merged["gadget"];
|
|
9618
|
-
delete merged["gadget-add"];
|
|
9619
|
-
delete merged["gadget-remove"];
|
|
9620
|
-
resolving.delete(name);
|
|
9621
|
-
resolved[name] = merged;
|
|
9622
|
-
return merged;
|
|
9623
10151
|
}
|
|
9624
|
-
|
|
9625
|
-
|
|
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);
|
|
9626
10158
|
}
|
|
9627
|
-
|
|
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
|
+
);
|
|
9628
10173
|
}
|
|
9629
10174
|
|
|
9630
10175
|
// src/cli/gadget-command.ts
|
|
@@ -10314,7 +10859,11 @@ function createCommandEnvironment(baseEnv, config) {
|
|
|
10314
10859
|
logFile: config["log-file"] ?? baseEnv.loggerConfig?.logFile,
|
|
10315
10860
|
logReset: config["log-reset"] ?? baseEnv.loggerConfig?.logReset
|
|
10316
10861
|
};
|
|
10317
|
-
return
|
|
10862
|
+
return {
|
|
10863
|
+
...baseEnv,
|
|
10864
|
+
loggerConfig,
|
|
10865
|
+
createLogger: createLoggerFactory(loggerConfig)
|
|
10866
|
+
};
|
|
10318
10867
|
}
|
|
10319
10868
|
function registerCustomCommand(program, name, config, env) {
|
|
10320
10869
|
const type = config.type ?? "agent";
|
|
@@ -10390,7 +10939,12 @@ async function runCLI(overrides = {}) {
|
|
|
10390
10939
|
logReset: globalOpts.logReset ?? config.global?.["log-reset"]
|
|
10391
10940
|
};
|
|
10392
10941
|
const defaultEnv = createDefaultEnvironment(loggerConfig);
|
|
10393
|
-
const env = {
|
|
10942
|
+
const env = {
|
|
10943
|
+
...defaultEnv,
|
|
10944
|
+
...envOverrides,
|
|
10945
|
+
// Pass Docker config from [docker] section
|
|
10946
|
+
dockerConfig: config.docker
|
|
10947
|
+
};
|
|
10394
10948
|
const program = createProgram(env, config);
|
|
10395
10949
|
await program.parseAsync(env.argv);
|
|
10396
10950
|
}
|