min-agent 0.5.0 → 0.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.
Files changed (156) hide show
  1. package/README.md +32 -18
  2. package/dist/cli.js +538 -12
  3. package/docs/API.md +25 -8
  4. package/package.json +6 -6
  5. package/skills/self-config/SKILL.md +7 -3
  6. package/skills/self-config/reference.md +8 -4
  7. package/dist/agent.js +0 -1342
  8. package/dist/assistant-stream.js +0 -131
  9. package/dist/cli/commands/chat.js +0 -13
  10. package/dist/cli/commands/exec.js +0 -35
  11. package/dist/cli/commands/history.js +0 -58
  12. package/dist/cli/commands/index.js +0 -241
  13. package/dist/cli/commands/init.js +0 -18
  14. package/dist/cli/commands/mcp.js +0 -173
  15. package/dist/cli/commands/memory.js +0 -87
  16. package/dist/cli/commands/models.js +0 -21
  17. package/dist/cli/commands/permission.js +0 -12
  18. package/dist/cli/commands/rules.js +0 -33
  19. package/dist/cli/commands/sandbox.js +0 -13
  20. package/dist/cli/commands/serve.js +0 -9
  21. package/dist/cli/commands/setup.js +0 -4
  22. package/dist/cli/commands/shared.js +0 -16
  23. package/dist/cli/commands/skills.js +0 -119
  24. package/dist/cli/commands/think.js +0 -12
  25. package/dist/cli/commands/update.js +0 -7
  26. package/dist/cli/commands/write-config.js +0 -52
  27. package/dist/cli/errors.js +0 -36
  28. package/dist/cli/exec-prompt.js +0 -26
  29. package/dist/cli/option-helpers.js +0 -65
  30. package/dist/cli/program.js +0 -217
  31. package/dist/clipboard.js +0 -142
  32. package/dist/code-mode.js +0 -184
  33. package/dist/compaction.js +0 -644
  34. package/dist/config.js +0 -470
  35. package/dist/confirm.js +0 -172
  36. package/dist/context-window.js +0 -246
  37. package/dist/doom-loop.js +0 -278
  38. package/dist/fetch-timeout.js +0 -152
  39. package/dist/http-approvals.js +0 -60
  40. package/dist/http.js +0 -119
  41. package/dist/instructions.js +0 -196
  42. package/dist/logger.js +0 -95
  43. package/dist/markdown.js +0 -259
  44. package/dist/mcp.js +0 -1036
  45. package/dist/memory-cli.js +0 -33
  46. package/dist/memory.js +0 -295
  47. package/dist/model-catalog.js +0 -285
  48. package/dist/output.js +0 -76
  49. package/dist/paste-handler.js +0 -41
  50. package/dist/permission-cli.js +0 -40
  51. package/dist/plugins.js +0 -131
  52. package/dist/pricing.js +0 -119
  53. package/dist/provider.js +0 -63
  54. package/dist/question-format.js +0 -60
  55. package/dist/reasoning-stream.js +0 -158
  56. package/dist/sandbox-cli.js +0 -79
  57. package/dist/sandbox.js +0 -403
  58. package/dist/save-throttle.js +0 -45
  59. package/dist/scope.js +0 -23
  60. package/dist/serve/common.js +0 -425
  61. package/dist/serve/routes-chat.js +0 -367
  62. package/dist/serve/routes-mcp.js +0 -212
  63. package/dist/serve/routes-memory.js +0 -95
  64. package/dist/serve/routes-meta.js +0 -233
  65. package/dist/serve/routes-sessions.js +0 -61
  66. package/dist/serve/routes-skills.js +0 -70
  67. package/dist/serve.js +0 -138
  68. package/dist/sessions.js +0 -265
  69. package/dist/skills.js +0 -599
  70. package/dist/structured-output.js +0 -29
  71. package/dist/synthetic.js +0 -7
  72. package/dist/think-cli.js +0 -36
  73. package/dist/thinking-wire.js +0 -228
  74. package/dist/thinking.js +0 -142
  75. package/dist/title-gen.js +0 -55
  76. package/dist/token-display.js +0 -39
  77. package/dist/tool-display.js +0 -178
  78. package/dist/tool-output.js +0 -126
  79. package/dist/tools/apply_patch.js +0 -265
  80. package/dist/tools/atomic-file.js +0 -35
  81. package/dist/tools/backend.js +0 -61
  82. package/dist/tools/bash.js +0 -231
  83. package/dist/tools/code_search.js +0 -98
  84. package/dist/tools/edit.js +0 -68
  85. package/dist/tools/explore.js +0 -232
  86. package/dist/tools/glob.js +0 -40
  87. package/dist/tools/grep.js +0 -174
  88. package/dist/tools/index.js +0 -34
  89. package/dist/tools/question.js +0 -54
  90. package/dist/tools/read.js +0 -109
  91. package/dist/tools/search-searxng.js +0 -223
  92. package/dist/tools/search-serper.js +0 -189
  93. package/dist/tools/task.js +0 -165
  94. package/dist/tools/todo.js +0 -213
  95. package/dist/tools/web_fetch.js +0 -195
  96. package/dist/tools/web_search.js +0 -228
  97. package/dist/tools/write.js +0 -55
  98. package/dist/tui/App.js +0 -126
  99. package/dist/tui/ConfirmBar.js +0 -66
  100. package/dist/tui/InputBar.js +0 -616
  101. package/dist/tui/MessageList.js +0 -718
  102. package/dist/tui/ModelPicker.js +0 -113
  103. package/dist/tui/QuestionBar.js +0 -136
  104. package/dist/tui/SessionPicker.js +0 -79
  105. package/dist/tui/Spinner.js +0 -6
  106. package/dist/tui/StatusBar.js +0 -22
  107. package/dist/tui/ThinkPicker.js +0 -77
  108. package/dist/tui/agent-runner.js +0 -223
  109. package/dist/tui/bracketed-paste.js +0 -37
  110. package/dist/tui/caret-pos.js +0 -179
  111. package/dist/tui/caret.js +0 -69
  112. package/dist/tui/click-count.js +0 -13
  113. package/dist/tui/diff-view.js +0 -61
  114. package/dist/tui/drag-state.js +0 -49
  115. package/dist/tui/hydrate.js +0 -129
  116. package/dist/tui/index.js +0 -258
  117. package/dist/tui/input-history.js +0 -125
  118. package/dist/tui/layout.js +0 -105
  119. package/dist/tui/mouse.js +0 -46
  120. package/dist/tui/overlay-input.js +0 -12
  121. package/dist/tui/paste-draft.js +0 -173
  122. package/dist/tui/prompt-queue.js +0 -24
  123. package/dist/tui/selection.js +0 -232
  124. package/dist/tui/session-switch.js +0 -28
  125. package/dist/tui/slash-commands.js +0 -123
  126. package/dist/tui/slash-handler.js +0 -589
  127. package/dist/tui/text-width.js +0 -113
  128. package/dist/tui/theme.js +0 -12
  129. package/dist/tui/token-info.js +0 -7
  130. package/dist/tui/tool-children.js +0 -19
  131. package/dist/tui/types.js +0 -1
  132. package/dist/tui/undo-stack.js +0 -14
  133. package/dist/tui/use-sgr-mouse.js +0 -29
  134. package/dist/tui-chat.js +0 -456
  135. package/dist/updater.js +0 -116
  136. package/dist/xml-search.js +0 -194
  137. package/docs/superpowers/plans/2026-08-16-batch1-tui-improvements.md +0 -1510
  138. package/docs/superpowers/plans/2026-08-16-batch2-cli-tools-api.md +0 -2105
  139. package/docs/superpowers/plans/2026-08-16-batch3-config-engineering.md +0 -1595
  140. package/docs/superpowers/plans/2026-08-16-input-caret.md +0 -782
  141. package/docs/superpowers/plans/2026-08-20-tui-completeness.md +0 -873
  142. package/docs/superpowers/plans/2026-08-20-unified-tui-default.md +0 -631
  143. package/docs/superpowers/plans/2026-08-23-input-paste-attachments.md +0 -475
  144. package/docs/superpowers/plans/2026-08-23-thinking-wire-profile.md +0 -450
  145. package/docs/superpowers/specs/2026-08-16-batch1-tui-improvements-design.md +0 -183
  146. package/docs/superpowers/specs/2026-08-16-batch2-cli-tools-api-design.md +0 -220
  147. package/docs/superpowers/specs/2026-08-16-batch3-config-engineering-design.md +0 -196
  148. package/docs/superpowers/specs/2026-08-16-input-caret-design.md +0 -63
  149. package/docs/superpowers/specs/2026-08-17-mouse-selection-design.md +0 -116
  150. package/docs/superpowers/specs/2026-08-20-config-http-alignment-design.md +0 -47
  151. package/docs/superpowers/specs/2026-08-20-mcp-plugins-alignment-design.md +0 -37
  152. package/docs/superpowers/specs/2026-08-20-sandbox-permissions-design.md +0 -68
  153. package/docs/superpowers/specs/2026-08-20-tui-completeness-design.md +0 -273
  154. package/docs/superpowers/specs/2026-08-20-unified-tui-default-design.md +0 -165
  155. package/docs/superpowers/specs/2026-08-23-input-paste-attachments-design.md +0 -174
  156. package/docs/superpowers/specs/2026-08-23-thinking-wire-profile-design.md +0 -140
package/dist/skills.js DELETED
@@ -1,599 +0,0 @@
1
- import { tool, jsonSchema } from "ai";
2
- import { readFileSync, existsSync, readdirSync, realpathSync } from "fs";
3
- import os from "os";
4
- import path from "path";
5
- import { fileURLToPath } from "url";
6
- import { globSync } from "glob";
7
- import { loadConfig, saveConfig, loadProjectConfig, saveProjectConfig } from "./config.js";
8
- import { truncateToolOutput } from "./tool-output.js";
9
- /** Packaged self-management skill; keep this name in sync with `skills/self-config/SKILL.md`. */
10
- export const SELF_CONFIG_SKILL = "self-config";
11
- function getPackageRoot() {
12
- return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
13
- }
14
- export function getBuiltinSkillsDir() {
15
- return path.join(getPackageRoot(), "skills");
16
- }
17
- function builtinSkillsAllowed() {
18
- const value = process.env.MIN_AGENT_NO_BUILTIN_SKILLS?.trim().toLowerCase();
19
- return value !== "1" && value !== "true";
20
- }
21
- function isUnderDir(root, filePath) {
22
- const rel = path.relative(root, filePath);
23
- return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
24
- }
25
- /** Coerce a config skill list to an array of names, tolerating hand-edited configs. */
26
- function skillNames(value) {
27
- return Array.isArray(value) ? value.filter((n) => typeof n === "string") : [];
28
- }
29
- /**
30
- * Effective enable state: project `.min-agent/config.json` wins over the global
31
- * `~/.min-agent/config.json`, so disabling a repo-local skill in one project
32
- * does not silently disable a same-named skill everywhere.
33
- */
34
- function resolveSkillState() {
35
- const project = loadProjectConfig();
36
- const forced = new Set(skillNames(project.enabledSkills));
37
- const disabled = new Set();
38
- const scopeOf = new Map();
39
- for (const name of skillNames(loadConfig().disabledSkills)) {
40
- if (forced.has(name))
41
- continue;
42
- disabled.add(name);
43
- scopeOf.set(name, "global");
44
- }
45
- for (const name of skillNames(project.disabledSkills)) {
46
- disabled.add(name);
47
- scopeOf.set(name, "project");
48
- }
49
- return { disabled, scopeOf };
50
- }
51
- function applyState(skill, state) {
52
- const disabled = state.disabled.has(skill.name);
53
- return {
54
- ...skill,
55
- enabled: !disabled,
56
- ...(disabled ? { disabledScope: state.scopeOf.get(skill.name) } : {}),
57
- };
58
- }
59
- /** Max SKILL.md depth below a skills root: `<skill>/SKILL.md` plus one nesting level. */
60
- const SKILL_SCAN_MAX_DEPTH = 3;
61
- /** Guard against pathological skill trees blowing up the system prompt. */
62
- const MAX_SKILL_FILES = 20;
63
- const SKILL_CONTENT_MAX_BYTES = 32 * 1024;
64
- /**
65
- * Skill scan order: later entries win on duplicate `name` in frontmatter.
66
- * Global user skills first, then project-local dirs so repo skills override ~/.agents.
67
- * Computed lazily so process.cwd() changes are respected.
68
- * `MIN_AGENT_SKILLS_DIRS` (path-delimiter separated) replaces the default user list.
69
- * Packaged skills are scanned separately and win on name.
70
- */
71
- function getSkillDirs() {
72
- const override = process.env.MIN_AGENT_SKILLS_DIRS?.trim();
73
- if (override) {
74
- return override
75
- .split(path.delimiter)
76
- .map((dir) => dir.trim())
77
- .filter(Boolean)
78
- .map((dir) => path.resolve(dir));
79
- }
80
- return [
81
- path.join(os.homedir(), ".agents", "skills"),
82
- path.join(process.cwd(), ".min-agent", "skills"),
83
- path.join(process.cwd(), ".agents", "skills"),
84
- path.join(process.cwd(), ".opencode", "skills"),
85
- path.join(process.cwd(), ".claude", "skills"),
86
- ];
87
- }
88
- let loadedSkills = {};
89
- const stderrWarn = (line) => process.stderr.write(`${line}\n`);
90
- /** Same underlying file? Symlinked skill libraries surface under several roots. */
91
- function sameFile(a, b) {
92
- if (a === b)
93
- return true;
94
- try {
95
- return realpathSync(a) === realpathSync(b);
96
- }
97
- catch {
98
- return false;
99
- }
100
- }
101
- /**
102
- * Scan a skills root for SKILL.md files.
103
- * Symlinked skill dirs are followed — sharing one skill library across
104
- * `.claude/skills`, `.agents/skills`, … via symlink is a common setup.
105
- * Within one root the shallowest SKILL.md wins for a given name, so the
106
- * `<skill>/<skill>/SKILL.md` layout some installers produce doesn't shadow
107
- * the canonical `<skill>/SKILL.md`.
108
- */
109
- export function scanSkillDir(dir, warn = stderrWarn) {
110
- if (!existsSync(dir))
111
- return [];
112
- const matches = globSync("**/SKILL.md", {
113
- cwd: dir,
114
- absolute: true,
115
- follow: true,
116
- maxDepth: SKILL_SCAN_MAX_DEPTH,
117
- ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**"],
118
- }).sort((a, b) => a.split(path.sep).length - b.split(path.sep).length || a.localeCompare(b));
119
- const byName = new Map();
120
- for (const match of matches) {
121
- const skill = parseSkillFile(match, warn);
122
- if (!skill || byName.has(skill.name))
123
- continue;
124
- byName.set(skill.name, skill);
125
- }
126
- return [...byName.values()];
127
- }
128
- function mergeSkill(next, skill, warn) {
129
- const previous = next[skill.name];
130
- if (previous && sameFile(previous.location, skill.location)) {
131
- if (skill.builtin && !previous.builtin)
132
- next[skill.name] = { ...previous, builtin: true };
133
- return;
134
- }
135
- if (previous) {
136
- const versionNote = previous.version && skill.version && previous.version !== skill.version
137
- ? ` (version ${previous.version} → ${skill.version})`
138
- : "";
139
- warn?.(`\x1b[33m ⚠ Duplicate skill "${skill.name}": ${skill.location} overrides ${previous.location}${versionNote}\x1b[0m`);
140
- }
141
- next[skill.name] = skill;
142
- }
143
- export function discoverSkills(opts) {
144
- const warn = opts?.silent ? null : stderrWarn;
145
- const next = {};
146
- for (const dir of getSkillDirs()) {
147
- for (const skill of scanSkillDir(dir, warn))
148
- mergeSkill(next, skill, warn);
149
- }
150
- if (builtinSkillsAllowed()) {
151
- const builtinDir = getBuiltinSkillsDir();
152
- for (const skill of scanSkillDir(builtinDir, warn)) {
153
- mergeSkill(next, { ...skill, builtin: isUnderDir(builtinDir, skill.location) }, warn);
154
- }
155
- }
156
- const state = resolveSkillState();
157
- loadedSkills = Object.fromEntries(Object.entries(next).map(([name, skill]) => [name, applyState(skill, state)]));
158
- const count = Object.keys(loadedSkills).length;
159
- if (count > 0 && !opts?.silent) {
160
- const enabledCount = Object.values(loadedSkills).filter((s) => s.enabled).length;
161
- warn?.(`\x1b[90m Skills: ${enabledCount} enabled${count > enabledCount ? `, ${count - enabledCount} disabled` : ""}\x1b[0m`);
162
- }
163
- }
164
- function unquote(value) {
165
- const trimmed = value.trim();
166
- const quoted = trimmed.match(/^"([\s\S]*)"$/) ?? trimmed.match(/^'([\s\S]*)'$/);
167
- return quoted ? quoted[1] : trimmed;
168
- }
169
- /**
170
- * Read one top-level frontmatter key.
171
- * Supports `key: value`, quoted values, plain multi-line scalars, and block
172
- * scalars (`|`, `>`, `|-`, `>-`) — all of which real-world SKILL.md files use
173
- * for long descriptions.
174
- */
175
- export function readFrontmatterValue(frontmatter, key) {
176
- const lines = frontmatter.split("\n");
177
- const index = lines.findIndex((line) => new RegExp(`^${key}:(\\s|$)`).test(line));
178
- if (index === -1)
179
- return null;
180
- const inline = lines[index].slice(key.length + 1).trim();
181
- const isBlockScalar = /^[|>][+-]?\d*$/.test(inline);
182
- const body = [];
183
- for (const line of lines.slice(index + 1)) {
184
- if (line.trim() !== "" && !/^\s/.test(line))
185
- break;
186
- body.push(line.replace(/^\s+/, ""));
187
- }
188
- if (inline && !isBlockScalar) {
189
- // Plain scalar: indented follow-up lines are continuations, joined by spaces.
190
- const continuation = body.filter((l) => l !== "").join(" ");
191
- return unquote(continuation ? `${inline} ${continuation}` : inline);
192
- }
193
- const joined = isBlockScalar && inline.startsWith(">") ? body.join(" ") : body.join("\n");
194
- const value = joined.trim();
195
- return value === "" ? null : value;
196
- }
197
- /** Read a YAML list, either `key: [a, b]` or a `- item` block. */
198
- function readFrontmatterList(frontmatter, key) {
199
- const lines = frontmatter.split("\n");
200
- const index = lines.findIndex((line) => new RegExp(`^\\s*${key}:(\\s|$)`).test(line));
201
- if (index === -1)
202
- return [];
203
- const header = lines[index];
204
- const indent = header.match(/^\s*/)[0].length;
205
- const inline = header.slice(header.indexOf(":") + 1).trim();
206
- const flow = inline.match(/^\[([\s\S]*)\]$/);
207
- if (flow) {
208
- return flow[1]
209
- .split(",")
210
- .map((item) => unquote(item))
211
- .filter(Boolean);
212
- }
213
- if (inline)
214
- return [unquote(inline)].filter(Boolean);
215
- const items = [];
216
- for (const line of lines.slice(index + 1)) {
217
- if (line.trim() === "")
218
- continue;
219
- const currentIndent = line.match(/^\s*/)[0].length;
220
- if (currentIndent <= indent)
221
- break;
222
- const item = line.trim();
223
- if (!item.startsWith("- "))
224
- break;
225
- items.push(unquote(item.slice(2)));
226
- }
227
- return items;
228
- }
229
- /**
230
- * Read `metadata.requires.bins` from a frontmatter block. Resolves the nested
231
- * path explicitly so a stray top-level `bins:` key cannot be misread.
232
- */
233
- function readRequiredBins(frontmatter) {
234
- const lines = frontmatter.split("\n");
235
- const metaIdx = lines.findIndex((line) => /^metadata:(\s|$)/.test(line));
236
- if (metaIdx === -1)
237
- return [];
238
- const metaIndent = lines[metaIdx].match(/^\s*/)[0].length;
239
- const requiresIdx = lines.slice(metaIdx + 1).findIndex((line) => {
240
- if (line.trim() === "" || /^\s*$/.test(line))
241
- return false;
242
- const indent = line.match(/^\s*/)[0].length;
243
- return indent > metaIndent && /^requires:(\s|$)/.test(line.trim());
244
- });
245
- if (requiresIdx === -1)
246
- return [];
247
- const requiresLine = lines[metaIdx + 1 + requiresIdx];
248
- const requiresIndent = requiresLine.match(/^\s*/)[0].length;
249
- const binsIdx = lines.slice(metaIdx + 1 + requiresIdx + 1).findIndex((line) => {
250
- if (line.trim() === "" || /^\s*$/.test(line))
251
- return false;
252
- const indent = line.match(/^\s*/)[0].length;
253
- return indent > requiresIndent && /^bins:(\s|$)/.test(line.trim());
254
- });
255
- if (binsIdx === -1)
256
- return [];
257
- const block = lines.slice(metaIdx + 1 + requiresIdx + 1 + binsIdx).join("\n");
258
- return readFrontmatterList(block, "bins");
259
- }
260
- function parseSkillFile(filePath, warn) {
261
- let raw;
262
- try {
263
- raw = readFileSync(filePath, "utf-8")
264
- .replace(/^\uFEFF/, "")
265
- .replace(/\r\n/g, "\n");
266
- }
267
- catch (err) {
268
- warn?.(`\x1b[33m ⚠ Skill skipped (unreadable): ${filePath} — ${err.message}\x1b[0m`);
269
- return null;
270
- }
271
- const fmMatch = raw.match(/^---[ \t]*\n([\s\S]*?)\n---[ \t]*(?:\n([\s\S]*))?$/);
272
- if (!fmMatch) {
273
- warn?.(`\x1b[33m ⚠ Skill skipped (missing --- frontmatter block): ${filePath}\x1b[0m`);
274
- return null;
275
- }
276
- const frontmatter = fmMatch[1];
277
- const name = readFrontmatterValue(frontmatter, "name");
278
- const description = readFrontmatterValue(frontmatter, "description");
279
- const missing = [!name && "name", !description && "description"].filter(Boolean);
280
- if (!name || !description) {
281
- warn?.(`\x1b[33m ⚠ Skill skipped (frontmatter missing ${missing.join(", ")}): ${filePath}\x1b[0m`);
282
- return null;
283
- }
284
- return {
285
- name,
286
- description,
287
- location: filePath,
288
- content: (fmMatch[2] ?? "").trim(),
289
- enabled: true,
290
- allowedTools: readFrontmatterList(frontmatter, "allowed-tools"),
291
- requiredBins: readRequiredBins(frontmatter),
292
- version: readFrontmatterValue(frontmatter, "version") ?? undefined,
293
- builtin: false,
294
- alwaysLoad: readFrontmatterBool(frontmatter, "always-load"),
295
- };
296
- }
297
- function readFrontmatterBool(frontmatter, key) {
298
- const raw = readFrontmatterValue(frontmatter, key);
299
- if (raw == null)
300
- return false;
301
- const value = raw.trim().toLowerCase();
302
- return value === "true" || value === "yes" || value === "1";
303
- }
304
- /** Every discovered skill, disabled ones included (`enabled` reflects config). */
305
- export function getAllSkills() {
306
- const state = resolveSkillState();
307
- return Object.values(loadedSkills).map((s) => applyState(s, state));
308
- }
309
- export function getSkills() {
310
- return getAllSkills().filter((s) => s.enabled);
311
- }
312
- export function getSkill(name) {
313
- const skill = loadedSkills[name];
314
- return skill ? applyState(skill, resolveSkillState()) : undefined;
315
- }
316
- /** Disabled or force-enabled names in either scope that match no discovered skill. */
317
- export function getStaleDisabledSkills() {
318
- const project = loadProjectConfig();
319
- const names = new Set([
320
- ...skillNames(loadConfig().disabledSkills),
321
- ...skillNames(project.disabledSkills),
322
- ...skillNames(project.enabledSkills),
323
- ]);
324
- return [...names].filter((name) => !loadedSkills[name]);
325
- }
326
- /**
327
- * Toggle skills in one scope.
328
- * Project scope wins over global: enabling in project scope records an explicit
329
- * override so a globally disabled skill stays available in this repo.
330
- * Returns per-skill effective state plus the scope that still disables it, if any.
331
- */
332
- export function setSkillEnabled(names, enabled, scope) {
333
- if (scope === "global") {
334
- const config = loadConfig();
335
- const disabled = new Set(skillNames(config.disabledSkills));
336
- for (const name of names) {
337
- if (enabled)
338
- disabled.delete(name);
339
- else
340
- disabled.add(name);
341
- }
342
- config.disabledSkills = [...disabled];
343
- saveConfig(config);
344
- }
345
- else {
346
- const project = loadProjectConfig();
347
- const disabled = new Set(skillNames(project.disabledSkills));
348
- const forced = new Set(skillNames(project.enabledSkills));
349
- const globallyDisabled = new Set(skillNames(loadConfig().disabledSkills));
350
- for (const name of names) {
351
- if (enabled) {
352
- disabled.delete(name);
353
- if (globallyDisabled.has(name))
354
- forced.add(name);
355
- else
356
- forced.delete(name);
357
- }
358
- else {
359
- disabled.add(name);
360
- forced.delete(name);
361
- }
362
- }
363
- saveProjectConfig({ ...project, disabledSkills: [...disabled], enabledSkills: [...forced] });
364
- }
365
- const state = resolveSkillState();
366
- return names.map((name) => {
367
- const effective = !state.disabled.has(name);
368
- return {
369
- name,
370
- enabled: effective,
371
- ...(effective === enabled ? {} : { blockedBy: state.scopeOf.get(name) }),
372
- };
373
- });
374
- }
375
- function listSkillFiles(dir) {
376
- const out = [];
377
- let total = 0;
378
- const walk = (current, depth) => {
379
- let dirents;
380
- try {
381
- dirents = readdirSync(current, { withFileTypes: true });
382
- }
383
- catch {
384
- return;
385
- }
386
- for (const d of dirents) {
387
- if (d.name === "SKILL.md" || d.name.startsWith("."))
388
- continue;
389
- total++;
390
- if (out.length < MAX_SKILL_FILES) {
391
- out.push(d.isDirectory() ? `${path.join(current, d.name)}/` : path.join(current, d.name));
392
- }
393
- if (d.isDirectory() && depth > 0)
394
- walk(path.join(current, d.name), depth - 1);
395
- }
396
- };
397
- walk(dir, 2);
398
- return { entries: out.sort(), total };
399
- }
400
- function missingBins(bins) {
401
- if (bins.length === 0)
402
- return [];
403
- const dirs = (process.env.PATH ?? "").split(path.delimiter).filter(Boolean);
404
- const exts = process.platform === "win32" ? ["", ".exe", ".cmd", ".bat", ".ps1"] : [""];
405
- const has = (bin) => dirs.some((d) => exts.some((ext) => existsSync(path.join(d, `${bin}${ext}`))));
406
- return bins.filter((bin) => !has(bin));
407
- }
408
- export function buildSkillContent(skill) {
409
- const dir = path.dirname(skill.location);
410
- const { entries, total } = listSkillFiles(dir);
411
- const absent = missingBins(skill.requiredBins);
412
- const body = [
413
- `<skill_content name="${escapeXmlAttr(skill.name)}">`,
414
- `# Skill: ${skill.name}`,
415
- "",
416
- `Base directory for this skill: ${dir}`,
417
- "Relative paths in this skill (e.g., scripts/, reference/) are relative to this base directory.",
418
- "",
419
- skill.allowedTools.length
420
- ? `This skill declares allowed-tools: ${escapeXml(skill.allowedTools.join(", "))}. Prefer these tools while the skill is active.`
421
- : "",
422
- absent.length
423
- ? `⚠ Required binaries not found on PATH: ${absent.join(", ")}. Tell the user to install them before running this skill's commands.`
424
- : "",
425
- entries.length
426
- ? [
427
- "<skill_files>",
428
- ...entries.map((f) => ` <file>${escapeXml(f)}</file>`),
429
- total > entries.length
430
- ? ` <note>${total - entries.length} more entries not listed; use glob on the base directory</note>`
431
- : "",
432
- "</skill_files>",
433
- ]
434
- .filter(Boolean)
435
- .join("\n")
436
- : "",
437
- "",
438
- skill.content,
439
- `</skill_content>`,
440
- ]
441
- .filter(Boolean)
442
- .join("\n");
443
- return truncateToolOutput(body, { direction: "head", maxBytes: SKILL_CONTENT_MAX_BYTES }).content;
444
- }
445
- /**
446
- * Skill tool — content is loaded on-demand when the model calls this tool.
447
- * The tool description is kept minimal; the full skill list lives in the system prompt.
448
- */
449
- export function getSkillsTool(tracker = new Set()) {
450
- return tool({
451
- description: "Load a specialized skill when the task at hand matches one of the skills listed in the system prompt. " +
452
- "Use this tool to inject the skill's instructions and resources into the current conversation. " +
453
- "The skill name must match one of the skills listed in your system prompt under available_skills.",
454
- inputSchema: jsonSchema({
455
- type: "object",
456
- properties: {
457
- name: { type: "string", description: "The name of the skill from available_skills" },
458
- },
459
- required: ["name"],
460
- }),
461
- execute: async ({ name }) => {
462
- const state = resolveSkillState();
463
- const available = Object.keys(loadedSkills)
464
- .filter((n) => !state.disabled.has(n))
465
- .sort();
466
- if (state.disabled.has(name)) {
467
- return `Skill "${name}" is disabled (${state.scopeOf.get(name)} scope). Available skills: ${available.join(", ") || "none"}`;
468
- }
469
- const skill = loadedSkills[name];
470
- if (!skill) {
471
- return `Skill "${name}" not found. Available skills: ${available.length ? available.join(", ") : "none"}`;
472
- }
473
- if (tracker.has(name)) {
474
- return `Skill "${name}" is already loaded in this conversation — its instructions are still in effect above. Base directory: ${path.dirname(skill.location)}`;
475
- }
476
- tracker.add(name);
477
- return buildSkillContent(skill);
478
- },
479
- });
480
- }
481
- function escapeXml(value) {
482
- return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
483
- }
484
- function escapeXmlAttr(value) {
485
- return escapeXml(value).replace(/"/g, "&quot;");
486
- }
487
- /** Reverse of `escapeXmlAttr` — used to recover the raw skill name from a marker. */
488
- function unescapeXmlAttr(value) {
489
- return value
490
- .replace(/&quot;/g, '"')
491
- .replace(/&lt;/g, "<")
492
- .replace(/&gt;/g, ">")
493
- .replace(/&amp;/g, "&");
494
- }
495
- /**
496
- * System prompt section — verbose XML format for better model comprehension.
497
- * Only name/description/location are injected; content is loaded on-demand via the skill tool.
498
- */
499
- export function buildSkillsPrompt(skills) {
500
- if (skills.length === 0)
501
- return "";
502
- const hasAlwaysLoad = skills.some((s) => s.alwaysLoad);
503
- return [
504
- "Skills provide specialized instructions and workflows for specific tasks.",
505
- "Use the skill tool to load a skill when a task matches its description.",
506
- hasAlwaysLoad
507
- ? "Skills with always_load already have their full instructions in this prompt. Do not call the skill tool for those unless compaction dropped them."
508
- : "",
509
- "",
510
- "<available_skills>",
511
- ...skills.flatMap((s) => [
512
- " <skill>",
513
- ` <name>${escapeXml(s.name)}</name>`,
514
- ` <description>${escapeXml(s.description)}</description>`,
515
- ` <location>${escapeXml(s.location)}</location>`,
516
- s.alwaysLoad ? " <always_load>true</always_load>" : "",
517
- s.builtin ? " <builtin>true</builtin>" : "",
518
- " </skill>",
519
- ]),
520
- "</available_skills>",
521
- ]
522
- .filter((line) => line !== "")
523
- .join("\n");
524
- }
525
- const SKILL_CONTENT_MARKER = /<skill_content name="([^"]*)"/g;
526
- function textOf(part) {
527
- if (typeof part === "string")
528
- return part;
529
- if (!part || typeof part !== "object")
530
- return "";
531
- const rec = part;
532
- if (typeof rec.text === "string")
533
- return rec.text;
534
- const output = rec.output;
535
- if (typeof output === "string")
536
- return output;
537
- if (output && typeof output === "object" && typeof output.value === "string") {
538
- return output.value;
539
- }
540
- return "";
541
- }
542
- /** Skills whose content is already present in the conversation. */
543
- export function collectLoadedSkillNames(messages) {
544
- const names = new Set();
545
- const scan = (text) => {
546
- for (const m of text.matchAll(SKILL_CONTENT_MARKER))
547
- names.add(unescapeXmlAttr(m[1]));
548
- };
549
- for (const msg of messages) {
550
- if (typeof msg.content === "string")
551
- scan(msg.content);
552
- else if (Array.isArray(msg.content))
553
- for (const part of msg.content)
554
- scan(textOf(part));
555
- }
556
- return names;
557
- }
558
- /**
559
- * Note appended to a compaction summary: the skill instructions themselves are
560
- * gone with the summarized history, so the model is told what was active and
561
- * how to get it back.
562
- */
563
- export function buildSkillReloadNote(names) {
564
- if (names.length === 0)
565
- return "";
566
- const withDirs = names.map((name) => {
567
- const skill = loadedSkills[name];
568
- return skill ? `${name} (base dir: ${path.dirname(skill.location)})` : name;
569
- });
570
- return [
571
- "## Skills Previously Loaded",
572
- ...withDirs.map((entry) => `- ${entry}`),
573
- "Their full instructions were dropped during compaction. Re-call the skill tool if you still need them.",
574
- ].join("\n");
575
- }
576
- /**
577
- * Single entry point for wiring skills into an agent: registers the `skill`
578
- * tool and returns the matching system-prompt section. Keeping both sides here
579
- * prevents the tool from being registered without `available_skills` in the prompt.
580
- */
581
- export function attachSkills(tools, tracker) {
582
- const skills = getSkills();
583
- if (skills.length === 0)
584
- return "";
585
- const loadTracker = tracker ?? new Set();
586
- const auto = skills.filter((s) => s.alwaysLoad);
587
- for (const skill of auto)
588
- loadTracker.add(skill.name);
589
- tools.skill = getSkillsTool(loadTracker);
590
- const catalog = buildSkillsPrompt(skills);
591
- if (auto.length === 0)
592
- return catalog;
593
- return [
594
- catalog,
595
- "",
596
- "The following skills are already loaded for this conversation. Follow them without calling the skill tool again.",
597
- ...auto.map(buildSkillContent),
598
- ].join("\n");
599
- }
@@ -1,29 +0,0 @@
1
- import { tool, jsonSchema } from "ai";
2
- /**
3
- * Structured output support.
4
- *
5
- * When the user requests JSON output matching a schema, a StructuredOutput tool
6
- * is injected. The model MUST call this tool to return its final answer in the
7
- * requested format.
8
- *
9
- * Based on opencode's structured output implementation in prompt.ts.
10
- */
11
- const STRUCTURED_OUTPUT_DESCRIPTION = `Use this tool to return your final response in the requested structured format.
12
-
13
- IMPORTANT:
14
- - You MUST call this tool exactly once at the end of your response
15
- - The input must be valid JSON matching the required schema
16
- - Complete all necessary research and tool calls BEFORE calling this tool
17
- - This tool provides your final answer - no further actions are taken after calling it`;
18
- export const STRUCTURED_OUTPUT_SYSTEM = `IMPORTANT: The user has requested structured output. You MUST use the StructuredOutput tool to provide your final response. Do NOT respond with plain text - you MUST call the StructuredOutput tool with your answer formatted according to the schema.`;
19
- export function createStructuredOutputTool(request, onSuccess) {
20
- const { $schema: _, ...toolSchema } = request.schema;
21
- return tool({
22
- description: STRUCTURED_OUTPUT_DESCRIPTION,
23
- inputSchema: jsonSchema(toolSchema),
24
- execute: async (args) => {
25
- onSuccess(args);
26
- return JSON.stringify(args, null, 2);
27
- },
28
- });
29
- }
package/dist/synthetic.js DELETED
@@ -1,7 +0,0 @@
1
- const synthetic = new WeakSet();
2
- export function markSyntheticMessage(msg) {
3
- synthetic.add(msg);
4
- }
5
- export function isSyntheticMessage(msg) {
6
- return synthetic.has(msg);
7
- }
package/dist/think-cli.js DELETED
@@ -1,36 +0,0 @@
1
- import { takeScopeFlags, scopeLabel } from "./scope.js";
2
- import { parseThinkingEffort, resolveThinkingEffort, setThinkingEffort, setThinkingOverride, thinkingEffortLabel, } from "./thinking.js";
3
- export const THINK_CLI_USAGE = [
4
- "Usage: min-agent think [off|low|medium|high|max] [--project|--global]",
5
- " min-agent --think off|low|medium|high|max [--project|--global]",
6
- ].join("\n");
7
- function sourceLabel(source) {
8
- return source === "cli" ? "this run" : source === "project" ? "project" : source === "global" ? "global" : "default";
9
- }
10
- export function runThinkCli(input) {
11
- const { scope: posScope, rest } = takeScopeFlags(input.positionals);
12
- const scope = posScope ?? input.scope ?? "global";
13
- if (rest[0] === "--help" || rest[0] === "-h") {
14
- return { ok: true, lines: [THINK_CLI_USAGE] };
15
- }
16
- if (rest.length === 1) {
17
- const parsed = parseThinkingEffort(rest[0]);
18
- if (!parsed)
19
- return { ok: false, lines: [THINK_CLI_USAGE] };
20
- setThinkingEffort(parsed, scope);
21
- setThinkingOverride(parsed);
22
- return { ok: true, lines: [`✓ Thinking set to ${thinkingEffortLabel(parsed)} (${scopeLabel(scope)})`] };
23
- }
24
- if (rest.length > 1)
25
- return { ok: false, lines: [THINK_CLI_USAGE] };
26
- if (input.flagEffort) {
27
- setThinkingEffort(input.flagEffort, scope);
28
- setThinkingOverride(input.flagEffort);
29
- return { ok: true, lines: [`✓ Thinking set to ${thinkingEffortLabel(input.flagEffort)} (${scopeLabel(scope)})`] };
30
- }
31
- const { thinking, source } = resolveThinkingEffort();
32
- return {
33
- ok: true,
34
- lines: [`Current thinking: ${thinkingEffortLabel(thinking)} (${sourceLabel(source)})`, THINK_CLI_USAGE],
35
- };
36
- }