min-agent 0.5.1 → 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 (169) hide show
  1. package/README.md +18 -18
  2. package/dist/cli.js +538 -12
  3. package/docs/API.md +6 -6
  4. package/package.json +6 -6
  5. package/skills/self-config/SKILL.md +4 -2
  6. package/skills/self-config/reference.md +4 -1
  7. package/dist/agent.js +0 -1388
  8. package/dist/assistant-stream.js +0 -131
  9. package/dist/cli/commands/chat.js +0 -13
  10. package/dist/cli/commands/ctx.js +0 -7
  11. package/dist/cli/commands/exec.js +0 -35
  12. package/dist/cli/commands/history.js +0 -58
  13. package/dist/cli/commands/index.js +0 -249
  14. package/dist/cli/commands/init.js +0 -18
  15. package/dist/cli/commands/mcp.js +0 -173
  16. package/dist/cli/commands/memory.js +0 -87
  17. package/dist/cli/commands/models.js +0 -21
  18. package/dist/cli/commands/permission.js +0 -12
  19. package/dist/cli/commands/rules.js +0 -33
  20. package/dist/cli/commands/sandbox.js +0 -13
  21. package/dist/cli/commands/serve.js +0 -9
  22. package/dist/cli/commands/setup.js +0 -56
  23. package/dist/cli/commands/shared.js +0 -25
  24. package/dist/cli/commands/skills.js +0 -119
  25. package/dist/cli/commands/think.js +0 -12
  26. package/dist/cli/commands/update.js +0 -7
  27. package/dist/cli/commands/write-config.js +0 -52
  28. package/dist/cli/errors.js +0 -36
  29. package/dist/cli/exec-prompt.js +0 -26
  30. package/dist/cli/option-helpers.js +0 -65
  31. package/dist/cli/program.js +0 -223
  32. package/dist/cli/setup/detect.js +0 -17
  33. package/dist/cli/setup/flags.js +0 -12
  34. package/dist/cli/setup/flow.js +0 -108
  35. package/dist/cli/setup/provider-form.js +0 -102
  36. package/dist/cli/setup/ui.js +0 -534
  37. package/dist/clipboard.js +0 -142
  38. package/dist/code-mode.js +0 -184
  39. package/dist/compaction.js +0 -644
  40. package/dist/config.js +0 -363
  41. package/dist/confirm.js +0 -172
  42. package/dist/context-window.js +0 -256
  43. package/dist/ctx-cli.js +0 -30
  44. package/dist/ctx.js +0 -80
  45. package/dist/doom-loop.js +0 -278
  46. package/dist/fetch-timeout.js +0 -152
  47. package/dist/http-approvals.js +0 -60
  48. package/dist/http.js +0 -119
  49. package/dist/instructions.js +0 -196
  50. package/dist/logger.js +0 -95
  51. package/dist/markdown.js +0 -259
  52. package/dist/mcp.js +0 -1036
  53. package/dist/memory-cli.js +0 -33
  54. package/dist/memory.js +0 -295
  55. package/dist/model-catalog.js +0 -285
  56. package/dist/ollama-model.js +0 -234
  57. package/dist/ollama-openai-bridge.js +0 -383
  58. package/dist/output.js +0 -76
  59. package/dist/paste-handler.js +0 -41
  60. package/dist/permission-cli.js +0 -40
  61. package/dist/plugins.js +0 -131
  62. package/dist/pricing.js +0 -119
  63. package/dist/provider.js +0 -63
  64. package/dist/question-format.js +0 -60
  65. package/dist/reasoning-stream.js +0 -158
  66. package/dist/sandbox-cli.js +0 -79
  67. package/dist/sandbox.js +0 -403
  68. package/dist/save-throttle.js +0 -45
  69. package/dist/scope.js +0 -23
  70. package/dist/serve/common.js +0 -425
  71. package/dist/serve/routes-chat.js +0 -367
  72. package/dist/serve/routes-mcp.js +0 -212
  73. package/dist/serve/routes-memory.js +0 -95
  74. package/dist/serve/routes-meta.js +0 -268
  75. package/dist/serve/routes-sessions.js +0 -61
  76. package/dist/serve/routes-skills.js +0 -70
  77. package/dist/serve.js +0 -138
  78. package/dist/sessions.js +0 -265
  79. package/dist/skills.js +0 -599
  80. package/dist/structured-output.js +0 -29
  81. package/dist/synthetic.js +0 -7
  82. package/dist/think-cli.js +0 -36
  83. package/dist/thinking-wire.js +0 -239
  84. package/dist/thinking.js +0 -166
  85. package/dist/title-gen.js +0 -55
  86. package/dist/token-display.js +0 -39
  87. package/dist/tool-display.js +0 -178
  88. package/dist/tool-output.js +0 -126
  89. package/dist/tools/apply_patch.js +0 -265
  90. package/dist/tools/atomic-file.js +0 -35
  91. package/dist/tools/backend.js +0 -61
  92. package/dist/tools/bash.js +0 -231
  93. package/dist/tools/code_search.js +0 -98
  94. package/dist/tools/edit.js +0 -68
  95. package/dist/tools/explore.js +0 -232
  96. package/dist/tools/glob.js +0 -40
  97. package/dist/tools/grep.js +0 -174
  98. package/dist/tools/index.js +0 -34
  99. package/dist/tools/question.js +0 -54
  100. package/dist/tools/read.js +0 -109
  101. package/dist/tools/search-searxng.js +0 -223
  102. package/dist/tools/search-serper.js +0 -189
  103. package/dist/tools/task.js +0 -165
  104. package/dist/tools/todo.js +0 -213
  105. package/dist/tools/web_fetch.js +0 -195
  106. package/dist/tools/web_search.js +0 -228
  107. package/dist/tools/write.js +0 -55
  108. package/dist/tui/App.js +0 -138
  109. package/dist/tui/ConfirmBar.js +0 -66
  110. package/dist/tui/CtxPicker.js +0 -68
  111. package/dist/tui/InputBar.js +0 -618
  112. package/dist/tui/MessageList.js +0 -718
  113. package/dist/tui/ModelPicker.js +0 -113
  114. package/dist/tui/QuestionBar.js +0 -136
  115. package/dist/tui/SessionPicker.js +0 -79
  116. package/dist/tui/Spinner.js +0 -6
  117. package/dist/tui/StatusBar.js +0 -22
  118. package/dist/tui/ThinkPicker.js +0 -75
  119. package/dist/tui/agent-runner.js +0 -223
  120. package/dist/tui/bracketed-paste.js +0 -37
  121. package/dist/tui/caret-pos.js +0 -179
  122. package/dist/tui/caret.js +0 -69
  123. package/dist/tui/click-count.js +0 -13
  124. package/dist/tui/diff-view.js +0 -61
  125. package/dist/tui/drag-state.js +0 -49
  126. package/dist/tui/hydrate.js +0 -129
  127. package/dist/tui/index.js +0 -264
  128. package/dist/tui/input-history.js +0 -125
  129. package/dist/tui/layout.js +0 -105
  130. package/dist/tui/mouse.js +0 -46
  131. package/dist/tui/overlay-input.js +0 -12
  132. package/dist/tui/paste-draft.js +0 -173
  133. package/dist/tui/prompt-queue.js +0 -24
  134. package/dist/tui/selection.js +0 -232
  135. package/dist/tui/session-switch.js +0 -28
  136. package/dist/tui/slash-commands.js +0 -129
  137. package/dist/tui/slash-handler.js +0 -615
  138. package/dist/tui/text-width.js +0 -113
  139. package/dist/tui/theme.js +0 -12
  140. package/dist/tui/token-info.js +0 -7
  141. package/dist/tui/tool-children.js +0 -19
  142. package/dist/tui/types.js +0 -1
  143. package/dist/tui/undo-stack.js +0 -14
  144. package/dist/tui/use-sgr-mouse.js +0 -29
  145. package/dist/tui-chat.js +0 -478
  146. package/dist/updater.js +0 -116
  147. package/dist/xml-search.js +0 -194
  148. package/docs/superpowers/plans/2026-08-16-batch1-tui-improvements.md +0 -1510
  149. package/docs/superpowers/plans/2026-08-16-batch2-cli-tools-api.md +0 -2105
  150. package/docs/superpowers/plans/2026-08-16-batch3-config-engineering.md +0 -1595
  151. package/docs/superpowers/plans/2026-08-16-input-caret.md +0 -782
  152. package/docs/superpowers/plans/2026-08-20-tui-completeness.md +0 -873
  153. package/docs/superpowers/plans/2026-08-20-unified-tui-default.md +0 -631
  154. package/docs/superpowers/plans/2026-08-23-cli-setup.md +0 -501
  155. package/docs/superpowers/plans/2026-08-23-input-paste-attachments.md +0 -475
  156. package/docs/superpowers/plans/2026-08-23-thinking-wire-profile.md +0 -450
  157. package/docs/superpowers/specs/2026-08-16-batch1-tui-improvements-design.md +0 -183
  158. package/docs/superpowers/specs/2026-08-16-batch2-cli-tools-api-design.md +0 -220
  159. package/docs/superpowers/specs/2026-08-16-batch3-config-engineering-design.md +0 -196
  160. package/docs/superpowers/specs/2026-08-16-input-caret-design.md +0 -63
  161. package/docs/superpowers/specs/2026-08-17-mouse-selection-design.md +0 -116
  162. package/docs/superpowers/specs/2026-08-20-config-http-alignment-design.md +0 -47
  163. package/docs/superpowers/specs/2026-08-20-mcp-plugins-alignment-design.md +0 -37
  164. package/docs/superpowers/specs/2026-08-20-sandbox-permissions-design.md +0 -68
  165. package/docs/superpowers/specs/2026-08-20-tui-completeness-design.md +0 -273
  166. package/docs/superpowers/specs/2026-08-20-unified-tui-default-design.md +0 -165
  167. package/docs/superpowers/specs/2026-08-23-cli-setup-design.md +0 -282
  168. package/docs/superpowers/specs/2026-08-23-input-paste-attachments-design.md +0 -174
  169. 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
- }