min-agent 0.3.0 → 0.4.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/README.md +111 -28
- package/dist/agent.js +1119 -256
- package/dist/cli/commands/chat.js +10 -0
- package/dist/cli/commands/exec.js +32 -0
- package/dist/cli/commands/history.js +58 -0
- package/dist/cli/commands/index.js +224 -0
- package/dist/cli/commands/init.js +18 -0
- package/dist/cli/commands/mcp.js +173 -0
- package/dist/cli/commands/memory.js +69 -0
- package/dist/cli/commands/models.js +21 -0
- package/dist/cli/commands/permission.js +12 -0
- package/dist/cli/commands/rules.js +33 -0
- package/dist/cli/commands/sandbox.js +13 -0
- package/dist/cli/commands/serve.js +9 -0
- package/dist/cli/commands/setup.js +4 -0
- package/dist/cli/commands/shared.js +16 -0
- package/dist/cli/commands/skills.js +119 -0
- package/dist/cli/commands/update.js +7 -0
- package/dist/cli/commands/write-config.js +30 -0
- package/dist/cli/errors.js +36 -0
- package/dist/cli/exec-prompt.js +26 -0
- package/dist/cli/option-helpers.js +53 -0
- package/dist/cli/program.js +180 -0
- package/dist/cli.js +5 -888
- package/dist/code-mode.js +32 -14
- package/dist/compaction.js +347 -160
- package/dist/config.js +119 -10
- package/dist/confirm.js +56 -9
- package/dist/context-window.js +107 -39
- package/dist/doom-loop.js +264 -29
- package/dist/fetch-timeout.js +152 -0
- package/dist/http-approvals.js +60 -0
- package/dist/instructions.js +21 -0
- package/dist/logger.js +33 -4
- package/dist/markdown.js +37 -11
- package/dist/mcp.js +328 -30
- package/dist/memory.js +97 -56
- package/dist/output.js +7 -5
- package/dist/permission-cli.js +43 -0
- package/dist/plugins.js +46 -8
- package/dist/pricing.js +4 -4
- package/dist/provider.js +23 -6
- package/dist/question-format.js +60 -0
- package/dist/sandbox-cli.js +82 -0
- package/dist/sandbox.js +403 -0
- package/dist/save-throttle.js +45 -0
- package/dist/serve/common.js +404 -0
- package/dist/serve/routes-chat.js +347 -0
- package/dist/serve/routes-mcp.js +212 -0
- package/dist/serve/routes-memory.js +66 -0
- package/dist/serve/routes-meta.js +205 -0
- package/dist/serve/routes-sessions.js +61 -0
- package/dist/serve/routes-skills.js +70 -0
- package/dist/serve.js +33 -883
- package/dist/sessions.js +53 -9
- package/dist/skills.js +82 -18
- package/dist/title-gen.js +8 -2
- package/dist/token-display.js +36 -0
- package/dist/tool-display.js +5 -0
- package/dist/tool-output.js +1 -3
- package/dist/tools/apply_patch.js +85 -11
- package/dist/tools/atomic-file.js +35 -0
- package/dist/tools/backend.js +2 -2
- package/dist/tools/bash.js +57 -19
- package/dist/tools/code_search.js +7 -1
- package/dist/tools/edit.js +11 -10
- package/dist/tools/explore.js +74 -14
- package/dist/tools/glob.js +4 -0
- package/dist/tools/grep.js +17 -10
- package/dist/tools/index.js +6 -21
- package/dist/tools/question.js +28 -9
- package/dist/tools/read.js +6 -4
- package/dist/tools/search-searxng.js +223 -0
- package/dist/tools/search-serper.js +189 -0
- package/dist/tools/task.js +84 -30
- package/dist/tools/todo.js +120 -19
- package/dist/tools/web_fetch.js +11 -3
- package/dist/tools/web_search.js +66 -556
- package/dist/tools/write.js +23 -6
- package/dist/tui/App.js +63 -14
- package/dist/tui/ConfirmBar.js +45 -13
- package/dist/tui/InputBar.js +150 -35
- package/dist/tui/MessageList.js +266 -125
- package/dist/tui/ModelPicker.js +8 -3
- package/dist/tui/QuestionBar.js +51 -19
- package/dist/tui/SessionPicker.js +79 -0
- package/dist/tui/StatusBar.js +8 -14
- package/dist/tui/agent-runner.js +142 -22
- package/dist/tui/caret-pos.js +48 -5
- package/dist/tui/caret.js +1 -1
- package/dist/tui/click-count.js +13 -0
- package/dist/tui/drag-state.js +8 -3
- package/dist/tui/hydrate.js +129 -0
- package/dist/tui/index.js +42 -13
- package/dist/tui/input-history.js +92 -11
- package/dist/tui/layout.js +75 -4
- package/dist/tui/prompt-queue.js +24 -0
- package/dist/tui/selection.js +113 -21
- package/dist/tui/session-switch.js +28 -0
- package/dist/tui/slash-commands.js +22 -6
- package/dist/tui/slash-handler.js +233 -58
- package/dist/tui/text-width.js +38 -16
- package/dist/tui/token-info.js +7 -0
- package/dist/tui/tool-children.js +19 -0
- package/dist/tui/undo-stack.js +1 -1
- package/dist/tui/use-sgr-mouse.js +3 -1
- package/dist/tui-chat.js +276 -40
- package/dist/updater.js +88 -29
- package/dist/xml-search.js +194 -0
- package/docs/API.md +257 -25
- package/docs/superpowers/plans/2026-08-20-tui-completeness.md +873 -0
- package/docs/superpowers/plans/2026-08-20-unified-tui-default.md +631 -0
- package/docs/superpowers/specs/2026-08-20-config-http-alignment-design.md +47 -0
- package/docs/superpowers/specs/2026-08-20-mcp-plugins-alignment-design.md +37 -0
- package/docs/superpowers/specs/2026-08-20-sandbox-permissions-design.md +68 -0
- package/docs/superpowers/specs/2026-08-20-tui-completeness-design.md +273 -0
- package/docs/superpowers/specs/2026-08-20-unified-tui-default-design.md +165 -0
- package/package.json +6 -1
- package/skills/self-config/SKILL.md +90 -0
- package/skills/self-config/reference.md +149 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { isConfigured } from "../../config.js";
|
|
2
|
+
import { CliError } from "../errors.js";
|
|
3
|
+
export async function runServeCommand(opts) {
|
|
4
|
+
if (!isConfigured()) {
|
|
5
|
+
throw new CliError("Not configured.", { hint: "Run: min-agent setup" });
|
|
6
|
+
}
|
|
7
|
+
const { runServe } = await import("../../serve.js");
|
|
8
|
+
await runServe({ port: opts.port, host: opts.host });
|
|
9
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { isConfigured } from "../../config.js";
|
|
2
|
+
import { CliError } from "../errors.js";
|
|
3
|
+
export async function startTuiSession(input) {
|
|
4
|
+
if (!isConfigured()) {
|
|
5
|
+
throw new CliError("Not configured.", { hint: "Run: min-agent setup" });
|
|
6
|
+
}
|
|
7
|
+
const { runTui } = await import("../../tui-chat.js");
|
|
8
|
+
const prompt = input.positionals.join(" ").trim();
|
|
9
|
+
await runTui({
|
|
10
|
+
modelId: input.model,
|
|
11
|
+
providerName: input.provider,
|
|
12
|
+
resumeSessionId: input.resume,
|
|
13
|
+
images: input.images,
|
|
14
|
+
initialPrompt: prompt || undefined,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { discoverSkills } from "../../skills.js";
|
|
2
|
+
import { existsSync, writeFileSync, mkdirSync } from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import os from "os";
|
|
5
|
+
import { CliError } from "../errors.js";
|
|
6
|
+
/** One-line, length-capped text for list output. */
|
|
7
|
+
function truncate(text, max) {
|
|
8
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
9
|
+
return flat.length <= max ? flat : `${flat.slice(0, max - 1)}…`;
|
|
10
|
+
}
|
|
11
|
+
export async function runSkillsList(json) {
|
|
12
|
+
discoverSkills({ silent: true });
|
|
13
|
+
const { getAllSkills, getStaleDisabledSkills } = await import("../../skills.js");
|
|
14
|
+
const all = getAllSkills().sort((a, b) => a.name.localeCompare(b.name));
|
|
15
|
+
const stale = getStaleDisabledSkills();
|
|
16
|
+
if (json) {
|
|
17
|
+
console.log(JSON.stringify({
|
|
18
|
+
count: all.length,
|
|
19
|
+
enabled_count: all.filter((s) => s.enabled).length,
|
|
20
|
+
stale_disabled: stale,
|
|
21
|
+
skills: all.map((s) => ({
|
|
22
|
+
name: s.name,
|
|
23
|
+
description: s.description,
|
|
24
|
+
location: s.location,
|
|
25
|
+
enabled: s.enabled,
|
|
26
|
+
...(s.disabledScope ? { disabled_scope: s.disabledScope } : {}),
|
|
27
|
+
...(s.version ? { version: s.version } : {}),
|
|
28
|
+
...(s.allowedTools.length ? { allowed_tools: s.allowedTools } : {}),
|
|
29
|
+
...(s.requiredBins.length ? { requires_bins: s.requiredBins } : {}),
|
|
30
|
+
...(s.builtin ? { builtin: true } : {}),
|
|
31
|
+
...(s.alwaysLoad ? { always_load: true } : {}),
|
|
32
|
+
})),
|
|
33
|
+
}, null, 2));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (all.length === 0 && stale.length === 0) {
|
|
37
|
+
console.log("No skills found. Add SKILL.md files in .min-agent/skills/<name>/");
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
console.log(`Skills (${all.filter((s) => s.enabled).length} enabled, ${all.filter((s) => !s.enabled).length} disabled):`);
|
|
41
|
+
for (const skill of all) {
|
|
42
|
+
const status = skill.enabled
|
|
43
|
+
? "\x1b[32menabled \x1b[0m"
|
|
44
|
+
: `\x1b[90mdisabled(${skill.disabledScope === "project" ? "proj" : "glob"})\x1b[0m`;
|
|
45
|
+
const origin = skill.builtin ? " \x1b[90mbuiltin\x1b[0m" : "";
|
|
46
|
+
console.log(` ${skill.name} ${status}${origin} \x1b[90m${truncate(skill.description, 60)}\x1b[0m`);
|
|
47
|
+
console.log(` \x1b[90m${skill.location}\x1b[0m`);
|
|
48
|
+
}
|
|
49
|
+
if (stale.length > 0) {
|
|
50
|
+
console.log(`\n\x1b[33m⚠ disabledSkills/enabledSkills entries matching no skill: ${stale.join(", ")}\x1b[0m`);
|
|
51
|
+
console.log(`\x1b[33m Remove them with: min-agent skills enable ${stale.join(" ")} (add --project if the entry is project-scoped)\x1b[0m`);
|
|
52
|
+
}
|
|
53
|
+
console.log("\nUse: min-agent skills info <name> for details");
|
|
54
|
+
}
|
|
55
|
+
export async function runSkillsInfo(name, json) {
|
|
56
|
+
discoverSkills({ silent: true });
|
|
57
|
+
const { getSkill, getAllSkills } = await import("../../skills.js");
|
|
58
|
+
const skill = getSkill(name);
|
|
59
|
+
if (!skill) {
|
|
60
|
+
const names = getAllSkills().map((s) => s.name);
|
|
61
|
+
throw new CliError(`Skill "${name}" not found.${names.length ? ` Available: ${names.join(", ")}` : " No skills discovered."}`);
|
|
62
|
+
}
|
|
63
|
+
if (json) {
|
|
64
|
+
console.log(JSON.stringify(skill, null, 2));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
console.log(`Name: ${skill.name}`);
|
|
68
|
+
console.log(`Status: ${skill.enabled ? "enabled" : `disabled (${skill.disabledScope} scope)`}`);
|
|
69
|
+
if (skill.builtin)
|
|
70
|
+
console.log("Source: builtin");
|
|
71
|
+
if (skill.version)
|
|
72
|
+
console.log(`Version: ${skill.version}`);
|
|
73
|
+
console.log(`Description: ${skill.description}`);
|
|
74
|
+
console.log(`Location: ${skill.location}`);
|
|
75
|
+
if (skill.allowedTools.length > 0)
|
|
76
|
+
console.log(`Tools: ${skill.allowedTools.join(", ")}`);
|
|
77
|
+
if (skill.requiredBins.length > 0)
|
|
78
|
+
console.log(`Requires: ${skill.requiredBins.join(", ")}`);
|
|
79
|
+
if (skill.content.trim()) {
|
|
80
|
+
console.log("\n--- Content ---\n");
|
|
81
|
+
console.log(skill.content.trim());
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export async function runSkillsToggle(names, enabled, scope) {
|
|
85
|
+
const verb = enabled ? "enable" : "disable";
|
|
86
|
+
if (names.length === 0)
|
|
87
|
+
throw new CliError(`Usage: min-agent skills ${verb} <name...> [--project]`);
|
|
88
|
+
discoverSkills({ silent: true });
|
|
89
|
+
const { getAllSkills, setSkillEnabled, getStaleDisabledSkills } = await import("../../skills.js");
|
|
90
|
+
const known = new Set(getAllSkills().map((s) => s.name));
|
|
91
|
+
const stale = new Set(getStaleDisabledSkills());
|
|
92
|
+
// `enable` must accept stale names so they can be cleaned up from config;
|
|
93
|
+
// `disable` on an unknown name is always an error.
|
|
94
|
+
const unknown = names.filter((n) => !known.has(n) && !(enabled && stale.has(n)));
|
|
95
|
+
if (unknown.length > 0) {
|
|
96
|
+
throw new CliError(`Skill${unknown.length > 1 ? "s" : ""} not found: ${unknown.join(", ")}`, {
|
|
97
|
+
hint: known.size ? `Available: ${[...known].join(", ")}` : "No skills discovered.",
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
for (const result of setSkillEnabled(names, enabled, scope)) {
|
|
101
|
+
console.log(`✓ Skill "${result.name}" ${verb}d (${scope} scope)`);
|
|
102
|
+
if (result.blockedBy) {
|
|
103
|
+
console.log(`\x1b[33m ⚠ still ${result.enabled ? "enabled" : "disabled"} — overridden by ${result.blockedBy} scope\x1b[0m`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
export async function runSkillsNew(name, global) {
|
|
108
|
+
if (!/^[\w-]+$/.test(name)) {
|
|
109
|
+
throw new CliError("Invalid skill name", { hint: "use letters, digits, _ or -" });
|
|
110
|
+
}
|
|
111
|
+
const root = global ? path.join(os.homedir(), ".agents", "skills") : path.join(process.cwd(), ".min-agent", "skills");
|
|
112
|
+
const dir = path.join(root, name);
|
|
113
|
+
if (existsSync(path.join(dir, "SKILL.md"))) {
|
|
114
|
+
throw new CliError(`Skill "${name}" already exists at ${dir}`);
|
|
115
|
+
}
|
|
116
|
+
mkdirSync(dir, { recursive: true });
|
|
117
|
+
writeFileSync(path.join(dir, "SKILL.md"), `---\nname: ${name}\ndescription: What this skill does\n# version: 1.0.0\n# allowed-tools: [read, grep]\n# metadata:\n# requires:\n# bins: ["some-cli"]\n---\n\n# ${name}\n\nWrite the skill instructions here.\n`, "utf-8");
|
|
118
|
+
console.log(`✓ Created skill "${name}" at ${path.join(dir, "SKILL.md")}`);
|
|
119
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { runSandboxCli } from "../../sandbox-cli.js";
|
|
2
|
+
import { runPermissionCli } from "../../permission-cli.js";
|
|
3
|
+
export async function runWriteConfig(input) {
|
|
4
|
+
const lines = [];
|
|
5
|
+
let ok = true;
|
|
6
|
+
if (input.sandbox !== undefined || input.network !== undefined) {
|
|
7
|
+
const result = runSandboxCli({
|
|
8
|
+
positionals: [],
|
|
9
|
+
flagMode: input.sandbox,
|
|
10
|
+
flagNetwork: input.network,
|
|
11
|
+
scope: input.scope,
|
|
12
|
+
});
|
|
13
|
+
lines.push(...result.lines);
|
|
14
|
+
if (!result.ok)
|
|
15
|
+
ok = false;
|
|
16
|
+
}
|
|
17
|
+
if (input.permission !== undefined) {
|
|
18
|
+
const result = runPermissionCli({
|
|
19
|
+
positionals: [],
|
|
20
|
+
flagMode: input.permission,
|
|
21
|
+
scope: input.scope,
|
|
22
|
+
});
|
|
23
|
+
lines.push(...result.lines);
|
|
24
|
+
if (!result.ok)
|
|
25
|
+
ok = false;
|
|
26
|
+
}
|
|
27
|
+
console.log(lines.join("\n"));
|
|
28
|
+
if (!ok)
|
|
29
|
+
process.exitCode = 1;
|
|
30
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified CLI error contract.
|
|
3
|
+
*
|
|
4
|
+
* Exit code convention:
|
|
5
|
+
* 1 - user error (bad arguments, missing input, not found, validation failure)
|
|
6
|
+
* 2 - runtime error (network failure, subprocess failure, unexpected exception)
|
|
7
|
+
*/
|
|
8
|
+
export class CliError extends Error {
|
|
9
|
+
exitCode;
|
|
10
|
+
hint;
|
|
11
|
+
constructor(message, options) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = "CliError";
|
|
14
|
+
this.exitCode = options?.exitCode ?? 1;
|
|
15
|
+
this.hint = options?.hint;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function userError(message, hint) {
|
|
19
|
+
return new CliError(message, { exitCode: 1, hint });
|
|
20
|
+
}
|
|
21
|
+
export function runtimeError(message, hint) {
|
|
22
|
+
return new CliError(message, { exitCode: 2, hint });
|
|
23
|
+
}
|
|
24
|
+
/** Render a CliError (or any error) as the standard `✖ message` block on stderr. */
|
|
25
|
+
export function formatCliError(err) {
|
|
26
|
+
if (err instanceof CliError) {
|
|
27
|
+
return err.hint ? `✖ ${err.message}\n → ${err.hint}` : `✖ ${err.message}`;
|
|
28
|
+
}
|
|
29
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
30
|
+
return `✖ ${message}`;
|
|
31
|
+
}
|
|
32
|
+
export function exitCodeFor(err) {
|
|
33
|
+
if (err instanceof CliError)
|
|
34
|
+
return err.exitCode;
|
|
35
|
+
return 2;
|
|
36
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the final prompt text for `min-agent exec`, combining positional
|
|
3
|
+
* arguments with piped stdin according to these rules:
|
|
4
|
+
* - `exec -` reads the entire prompt from stdin.
|
|
5
|
+
* - `exec` (no args) requires piped stdin (non-TTY); errors otherwise.
|
|
6
|
+
* - `exec <msg>` + pipe appends piped stdin after the message.
|
|
7
|
+
* - `exec <msg>` uses the message as-is.
|
|
8
|
+
*/
|
|
9
|
+
export function resolveExecPrompt(positionals, stdinText, stdinIsTty) {
|
|
10
|
+
const joined = positionals.join(" ");
|
|
11
|
+
const piped = stdinText.length > 0;
|
|
12
|
+
if (joined === "-") {
|
|
13
|
+
const prompt = stdinText;
|
|
14
|
+
if (!prompt.trim())
|
|
15
|
+
return { ok: false, error: "Usage: min-agent exec <message>" };
|
|
16
|
+
return { ok: true, prompt };
|
|
17
|
+
}
|
|
18
|
+
if (!joined) {
|
|
19
|
+
if (stdinIsTty || !piped)
|
|
20
|
+
return { ok: false, error: "Usage: min-agent exec <message>" };
|
|
21
|
+
return { ok: true, prompt: stdinText };
|
|
22
|
+
}
|
|
23
|
+
if (piped && !stdinIsTty)
|
|
24
|
+
return { ok: true, prompt: `${joined}\n\n${stdinText}` };
|
|
25
|
+
return { ok: true, prompt: joined };
|
|
26
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Option } from "commander";
|
|
2
|
+
import { parseNetworkPolicy, parseSandboxMode } from "../sandbox.js";
|
|
3
|
+
import { parsePermissionMode } from "../config.js";
|
|
4
|
+
import { userError } from "./errors.js";
|
|
5
|
+
/** Reusable --project / --global mutually exclusive scope selector. */
|
|
6
|
+
export function scopeOptions() {
|
|
7
|
+
return [
|
|
8
|
+
new Option("--project", "Apply to this project only (.min-agent/config.json)").conflicts("global"),
|
|
9
|
+
new Option("--global", "Apply to every project (~/.min-agent/config.json)").conflicts("project"),
|
|
10
|
+
];
|
|
11
|
+
}
|
|
12
|
+
/** Resolve --project/--global into a Scope, given an optional fallback. */
|
|
13
|
+
export function resolveScope(opts, fallback = "global") {
|
|
14
|
+
if (opts.project)
|
|
15
|
+
return "project";
|
|
16
|
+
if (opts.global)
|
|
17
|
+
return "global";
|
|
18
|
+
return fallback;
|
|
19
|
+
}
|
|
20
|
+
export function parseSandboxModeArg(value) {
|
|
21
|
+
const parsed = parseSandboxMode(value);
|
|
22
|
+
if (!parsed)
|
|
23
|
+
throw userError(`Invalid --sandbox value: "${value}"`, "use one of: off, workspace, strict");
|
|
24
|
+
return parsed;
|
|
25
|
+
}
|
|
26
|
+
export function parseNetworkPolicyArg(value) {
|
|
27
|
+
const parsed = parseNetworkPolicy(value);
|
|
28
|
+
if (!parsed)
|
|
29
|
+
throw userError(`Invalid --network value: "${value}"`, "use one of: allow, deny");
|
|
30
|
+
return parsed;
|
|
31
|
+
}
|
|
32
|
+
export function parsePermissionModeArg(value) {
|
|
33
|
+
const parsed = parsePermissionMode(value);
|
|
34
|
+
if (!parsed)
|
|
35
|
+
throw userError(`Invalid --permission value: "${value}"`, "use one of: ask, accept-edits, allow-all");
|
|
36
|
+
return parsed;
|
|
37
|
+
}
|
|
38
|
+
export function parsePortArg(value) {
|
|
39
|
+
const n = Number.parseInt(value, 10);
|
|
40
|
+
if (!Number.isFinite(n) || n <= 0 || n > 65535) {
|
|
41
|
+
throw userError(`Invalid --port value: "${value}"`, "use an integer between 1 and 65535");
|
|
42
|
+
}
|
|
43
|
+
return n;
|
|
44
|
+
}
|
|
45
|
+
export function parsePositiveIntArg(flagName) {
|
|
46
|
+
return (value) => {
|
|
47
|
+
const n = Number.parseInt(value, 10);
|
|
48
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
49
|
+
throw userError(`Invalid ${flagName} value: "${value}"`, "use a positive integer");
|
|
50
|
+
}
|
|
51
|
+
return n;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { Command, Option } from "commander";
|
|
2
|
+
import { setAutoApprove } from "../confirm.js";
|
|
3
|
+
import { CliError, formatCliError, exitCodeFor } from "./errors.js";
|
|
4
|
+
import { parseNetworkPolicyArg, parsePermissionModeArg, parseSandboxModeArg } from "./option-helpers.js";
|
|
5
|
+
import { localVersion } from "../updater.js";
|
|
6
|
+
function collectImage(value, previous) {
|
|
7
|
+
return [...previous, value];
|
|
8
|
+
}
|
|
9
|
+
/** Options shared across the root command and `exec` (session-affecting flags). */
|
|
10
|
+
export function addSessionOptions(cmd) {
|
|
11
|
+
return cmd
|
|
12
|
+
.option("-m, --model <model>", "Override model for this request")
|
|
13
|
+
.option("--provider <name>", "Use a specific configured provider")
|
|
14
|
+
.option("--resume <id>", "Resume a previous session by id")
|
|
15
|
+
.option("-i, --image <path>", "Attach an image (repeatable)", collectImage, [])
|
|
16
|
+
.addOption(new Option("--sandbox <mode>", "Isolation mode (off, workspace, strict)").argParser(parseSandboxModeArg))
|
|
17
|
+
.addOption(new Option("--network <policy>", "Network policy (allow, deny)").argParser(parseNetworkPolicyArg))
|
|
18
|
+
.addOption(new Option("--permission <mode>", "Confirmation mode (ask, accept-edits, allow-all)").argParser(parsePermissionModeArg))
|
|
19
|
+
.option("--project", "Write --permission/--sandbox to this repo's config")
|
|
20
|
+
.option("--global", "Write --permission/--sandbox to the global config")
|
|
21
|
+
.option("-y, --yes", "This session only: same as --permission allow-all");
|
|
22
|
+
}
|
|
23
|
+
export function buildProgram() {
|
|
24
|
+
const program = new Command("min-agent");
|
|
25
|
+
program
|
|
26
|
+
.description("min-agent - Minimal AI coding agent")
|
|
27
|
+
.version(localVersion(), "-v, --version", "Show version number")
|
|
28
|
+
.helpOption("-h, --help", "Show help")
|
|
29
|
+
.allowUnknownOption()
|
|
30
|
+
.enablePositionalOptions()
|
|
31
|
+
.configureOutput({
|
|
32
|
+
writeErr: (str) => process.stderr.write(str),
|
|
33
|
+
outputError: (str) => process.stderr.write(str),
|
|
34
|
+
})
|
|
35
|
+
.exitOverride();
|
|
36
|
+
addSessionOptions(program);
|
|
37
|
+
program.argument("[prompt...]", "Prompt to send in an interactive session");
|
|
38
|
+
program.addHelpText("after", `
|
|
39
|
+
Session:
|
|
40
|
+
min-agent Start interactive session
|
|
41
|
+
min-agent <message> Start interactive session and send message
|
|
42
|
+
min-agent --resume <id> Resume a previous interactive session
|
|
43
|
+
min-agent chat | code Same as interactive session (compat)
|
|
44
|
+
min-agent exec <message> Run one message non-interactively
|
|
45
|
+
min-agent exec --resume <id> <msg> Continue a saved exec session
|
|
46
|
+
|
|
47
|
+
Setup & config:
|
|
48
|
+
min-agent setup Configure providers (interactively)
|
|
49
|
+
min-agent init Initialize .min-agent/ in current directory
|
|
50
|
+
min-agent models List available models
|
|
51
|
+
min-agent rules [edit] Show or edit instruction rules
|
|
52
|
+
min-agent memory <add|search|delete|list> Manage saved memories
|
|
53
|
+
min-agent sandbox [off|workspace|strict] Show or set isolation mode
|
|
54
|
+
min-agent sandbox network <allow|deny> Show or set network policy
|
|
55
|
+
min-agent permission [ask|accept-edits|allow-all] Show or set confirmation mode
|
|
56
|
+
|
|
57
|
+
Integrations:
|
|
58
|
+
min-agent mcp <list|add|remove|info|check|enable|disable> Manage MCP servers
|
|
59
|
+
min-agent skills <list|info|new|enable|disable> Manage skills
|
|
60
|
+
|
|
61
|
+
Operations:
|
|
62
|
+
min-agent serve [--host H] [--port P] Start the HTTP API (see docs/API.md)
|
|
63
|
+
min-agent history [delete|rename|export] Manage saved sessions
|
|
64
|
+
min-agent update Upgrade to the latest npm release
|
|
65
|
+
|
|
66
|
+
Confirmation modes (default: ask; independent of isolation):
|
|
67
|
+
ask Prompt before dangerous operations
|
|
68
|
+
accept-edits Auto-approve file writes; still prompt for dangerous commands
|
|
69
|
+
allow-all Auto-approve everything
|
|
70
|
+
|
|
71
|
+
Isolation modes (default: off; independent of confirmation):
|
|
72
|
+
off No sandboxing
|
|
73
|
+
workspace Restrict file writes to the project workspace
|
|
74
|
+
strict Restrict file writes and deny risky commands
|
|
75
|
+
Env: MIN_AGENT_SANDBOX, MIN_AGENT_SANDBOX_NETWORK
|
|
76
|
+
|
|
77
|
+
Rules (loaded as system instructions):
|
|
78
|
+
Global: ~/.min-agent/rules.md
|
|
79
|
+
Project: ./AGENTS.md or ./RULES.md or ./.min-agent/AGENTS.md
|
|
80
|
+
Config: "instructions" array in ~/.min-agent/config.json
|
|
81
|
+
|
|
82
|
+
Examples:
|
|
83
|
+
min-agent setup
|
|
84
|
+
min-agent
|
|
85
|
+
min-agent "hello"
|
|
86
|
+
min-agent exec "hello"
|
|
87
|
+
min-agent exec --resume <id> "continue"
|
|
88
|
+
min-agent --resume <id>
|
|
89
|
+
min-agent --permission ask
|
|
90
|
+
min-agent serve --port 8787
|
|
91
|
+
min-agent update
|
|
92
|
+
min-agent rules edit
|
|
93
|
+
min-agent mcp add filesystem npx -y @modelcontextprotocol/server-filesystem /tmp
|
|
94
|
+
min-agent mcp add remote --url https://example.com/mcp --token "$TOKEN"
|
|
95
|
+
`);
|
|
96
|
+
return program;
|
|
97
|
+
}
|
|
98
|
+
/** Apply --yes/-y for this process. Sandbox/permission overrides are applied per-invocation-kind. */
|
|
99
|
+
export function applySessionOverrides(opts) {
|
|
100
|
+
if (opts.yes)
|
|
101
|
+
setAutoApprove(true);
|
|
102
|
+
}
|
|
103
|
+
export function hasSessionIntent(opts, prompt) {
|
|
104
|
+
return (prompt.length > 0 || Boolean(opts.resume) || opts.image.length > 0 || Boolean(opts.model) || Boolean(opts.provider));
|
|
105
|
+
}
|
|
106
|
+
/** Extract `--unknown` style tokens left over in positionals (from allowUnknownOption) as errors. */
|
|
107
|
+
export function extractUnknownOptionErrors(positionals) {
|
|
108
|
+
const rest = [];
|
|
109
|
+
const errors = [];
|
|
110
|
+
for (const p of positionals) {
|
|
111
|
+
if (p.startsWith("--"))
|
|
112
|
+
errors.push(`Unknown option: ${p}`);
|
|
113
|
+
else
|
|
114
|
+
rest.push(p);
|
|
115
|
+
}
|
|
116
|
+
return { rest, errors };
|
|
117
|
+
}
|
|
118
|
+
async function runRootAction(promptTokens, opts) {
|
|
119
|
+
applySessionOverrides(opts);
|
|
120
|
+
const { setSandboxOverride } = await import("../sandbox.js");
|
|
121
|
+
const { setPermissionOverride } = await import("../confirm.js");
|
|
122
|
+
const { rest, errors } = extractUnknownOptionErrors(promptTokens);
|
|
123
|
+
const sessionIntent = hasSessionIntent(opts, rest);
|
|
124
|
+
const wantsConfigWrite = opts.sandbox !== undefined || opts.network !== undefined || opts.permission !== undefined;
|
|
125
|
+
if (wantsConfigWrite && !sessionIntent) {
|
|
126
|
+
const { runWriteConfig } = await import("./commands/write-config.js");
|
|
127
|
+
await runWriteConfig({
|
|
128
|
+
sandbox: opts.sandbox,
|
|
129
|
+
network: opts.network,
|
|
130
|
+
permission: opts.permission,
|
|
131
|
+
scope: opts.project ? "project" : opts.global ? "global" : undefined,
|
|
132
|
+
});
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (opts.sandbox !== undefined)
|
|
136
|
+
setSandboxOverride({ mode: opts.sandbox });
|
|
137
|
+
if (opts.network !== undefined)
|
|
138
|
+
setSandboxOverride({ network: opts.network });
|
|
139
|
+
if (opts.permission !== undefined)
|
|
140
|
+
setPermissionOverride(opts.permission);
|
|
141
|
+
if (errors.length > 0) {
|
|
142
|
+
console.error(errors[0]);
|
|
143
|
+
process.exitCode = 1;
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const { startTuiSession } = await import("./commands/shared.js");
|
|
147
|
+
await startTuiSession({
|
|
148
|
+
model: opts.model,
|
|
149
|
+
provider: opts.provider,
|
|
150
|
+
resume: opts.resume,
|
|
151
|
+
images: opts.image,
|
|
152
|
+
positionals: rest,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
export async function runProgram(argv, registerCommands) {
|
|
156
|
+
const program = buildProgram();
|
|
157
|
+
program.action((promptTokens, opts) => runRootAction(promptTokens, opts));
|
|
158
|
+
registerCommands(program);
|
|
159
|
+
try {
|
|
160
|
+
await program.parseAsync(argv, { from: "user" });
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
if (err instanceof CliError) {
|
|
164
|
+
console.error(formatCliError(err));
|
|
165
|
+
process.exitCode = exitCodeFor(err);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
// Commander throws a CommanderError for --help/--version and for its own
|
|
169
|
+
// usage errors; those already printed output and set process.exitCode.
|
|
170
|
+
if (isCommanderExit(err)) {
|
|
171
|
+
process.exitCode = err.exitCode;
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
console.error(formatCliError(err));
|
|
175
|
+
process.exitCode = 2;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function isCommanderExit(err) {
|
|
179
|
+
return typeof err === "object" && err !== null && "exitCode" in err && "code" in err;
|
|
180
|
+
}
|