mini-coder 0.0.20 β†’ 0.0.22

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.
@@ -0,0 +1,31 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "WebSearch",
5
+ "WebFetch(domain:github.com)",
6
+ "WebFetch(domain:dev.to)",
7
+ "WebFetch(domain:mariozechner.at)",
8
+ "WebFetch(domain:lobste.rs)",
9
+ "WebFetch(domain:reading.sh)",
10
+ "WebFetch(domain:daveswift.com)",
11
+ "Bash(grep:*)",
12
+ "Bash(ls:*)",
13
+ "Bash(bun run:*)",
14
+ "Bash(git stash:*)",
15
+ "Bash(gh pr:*)",
16
+ "Bash(git status:*)",
17
+ "Bash(git add:*)",
18
+ "Bash(git commit:*)",
19
+ "Bash(sqlite3:*)",
20
+ "Bash(git branch:*)",
21
+ "Bash(git checkout:*)",
22
+ "Bash(npm info:*)",
23
+ "Bash(npm install:*)",
24
+ "Bash(find:*)",
25
+ "Bash(git log:*)",
26
+ "Bash(npm ls:*)",
27
+ "Bash(gh api:*)",
28
+ "Bash(bun:*)"
29
+ ]
30
+ }
31
+ }
package/README.md CHANGED
@@ -26,20 +26,18 @@ I was built with a simple philosophy: **dev flow first**. No slow startup. No cl
26
26
 
27
27
  My toolkit is lean on purpose β€” every tool earns its spot, no passengers:
28
28
 
29
- | Tool | What it does |
30
- | --------------- | ----------------------------------------------------------- |
31
- | πŸ” `glob` | Find files by pattern across your project |
32
- | 🧲 `grep` | Search file contents with regex |
33
- | πŸ“– `read` | Read files (with line-range support) |
34
- | πŸ“ `create` | Create a new file or fully overwrite an existing file |
35
- | ✏️ `replace` | Replace or delete lines using hashline anchors |
36
- | βž• `insert` | Insert lines before/after an anchor without replacing |
37
- | 🐚 `shell` | Run shell commands and see their output |
38
- | πŸ€– `subagent` | Spawn a focused mini-me for parallel subtasks |
39
- | 🌐 `webSearch` | Search the web when `EXA_API_KEY` is set |
40
- | πŸ“„ `webContent` | Fetch full page content from URLs when `EXA_API_KEY` is set |
41
-
42
- Need more firepower? I connect to **MCP servers** over HTTP or stdio β€” bolt on external tools whenever the job calls for it.
29
+ | Tool | What it does |
30
+ | --------------- | -------------------------------------------------------------------------- |
31
+ | 🐚 `shell` | Run shell commands, inspect the repo, and use `mc-edit` for targeted edits |
32
+ | πŸ€– `subagent` | Spawn a focused mini-me for parallel subtasks |
33
+ | 🧰 `listSkills` | List discovered skills without loading full skill bodies |
34
+ | πŸ“˜ `readSkill` | Load one `SKILL.md` on demand |
35
+ | 🌐 `webSearch` | Search the web when `EXA_API_KEY` is set |
36
+ | πŸ“„ `webContent` | Fetch full page content from URLs when `EXA_API_KEY` is set |
37
+
38
+ mini-coder is intentionally **shell-first**: inspect with shell, edit with `mc-edit`, verify with shell.
39
+
40
+ Need more firepower? I also connect to **MCP servers** over HTTP or stdio β€” attached MCP tools show up dynamically whenever the job calls for them.
43
41
 
44
42
  ---
45
43
 
@@ -49,13 +47,12 @@ Need more firepower? I connect to **MCP servers** over HTTP or stdio β€” bolt on
49
47
  - **Built-in web search** β€” set `EXA_API_KEY` and I expose `webSearch` + `webContent` tools.
50
48
  - **Session memory** β€” conversations are saved in a local SQLite database. Resume where you left off with `-c` or pick a specific session with `-r <id>`.
51
49
  - **Shell integration** β€” prefix with `!` to run shell commands inline. Use `@` to reference files in your prompt (with Tab completion).
52
- - **Slash commands** β€” `/model` or `/models` to list/switch models, `/model effort <low|medium|high|xhigh|off>` for reasoning effort, `/reasoning [on|off]` to toggle reasoning display, `/context` to inspect or tune pruning/tool-result caps, `/plan` for read-only thinking mode, `/ralph` for autonomous looping, `/review` for a code review (global custom command, auto-created at `~/.agents/commands/review.md`), `/agent [name]` to set or clear an active primary agent, `/undo` to roll back a turn, `/new` for a clean session, `/mcp list|add|remove` to manage MCP servers, and `/exit` (`/quit`, `/q`) to leave. See all with `/help`.
50
+ - **Slash commands** β€” `/model` or `/models` to list/switch models, `/model effort <low|medium|high|xhigh|off>` for reasoning effort, `/reasoning [on|off]` to toggle reasoning display, `/context` to inspect or tune pruning/tool-result caps, `/cache` to configure prompt caching, `/review` for a code review (global custom command, auto-created at `~/.agents/commands/review.md`), `/agent [name]` to set or clear an active primary agent, `/undo` to remove the last conversation turn (it does not revert filesystem changes), `/new` for a clean session, `/mcp list|add|remove` to manage MCP servers, and `/exit` (`/quit`, `/q`) to leave. See all with `/help`.
53
51
 
54
52
  - **Custom commands** β€” drop a `.md` file in `.agents/commands/` and it becomes a `/command`. Claude-compatible `.claude/commands/` works too. Supports argument placeholders (`$ARGUMENTS`, `$1`…`$9`) and shell interpolation (`` !`cmd` ``). Global commands live in `~/.agents/commands/` and `~/.claude/commands/`. Custom commands take precedence over built-ins. β†’ [docs/custom-commands.md](docs/custom-commands.md)
55
- - **Custom agents** β€” drop a `.md` file in `.agents/agents/` or `.claude/agents/` (or `~/.agents/agents/` / `~/.claude/agents/` globally) and activate it with `/agent [name]`. Agent definitions are also exposed to subagent delegation unless `mode: primary`. `@agent-name` is supported for completion and is a useful prompt convention. β†’ [docs/custom-agents.md](docs/custom-agents.md)
53
+ - **Custom agents** β€” drop a `.md` file in `.agents/agents/` or `.claude/agents/` (or `~/.agents/agents/` / `~/.claude/agents/` globally) and activate it with `/agent [name]`. Agent definitions are also exposed to subagent delegation unless `mode: primary`. β†’ [docs/custom-agents.md](docs/custom-agents.md)
56
54
  - **Skills** β€” place a `SKILL.md` in `.agents/skills/<name>/` and inject it into any prompt with `@skill-name`. Claude-compatible `.claude/skills/<name>/SKILL.md` works too. Skills are _never_ auto-loaded β€” always explicit. β†’ [docs/skills.md](docs/skills.md)
57
- - **Post-tool hooks** β€” drop an executable at `.agents/hooks/post-<tool>` (or `~/.agents/hooks/post-<tool>` globally) and I'll run it after matching built-in tool calls. β†’ [docs/tool-hooks.md](docs/tool-hooks.md)
58
- - **Beautiful, minimal output** β€” diffs for edits, formatted trees for file searches, a live status bar with model, git branch, and token counts.
55
+ - **Beautiful, minimal output** β€” compact tool output, formatted trees for file searches, a live status bar with model, git branch, and token counts.
59
56
  - **16 ANSI colors only** β€” my output inherits _your_ terminal theme. Dark mode, light mode, Solarized, Gruvbox β€” I fit right in.
60
57
 
61
58
  ---
@@ -64,7 +61,7 @@ Need more firepower? I connect to **MCP servers** over HTTP or stdio β€” bolt on
64
61
 
65
62
  - **I eat my own dog food.** I was built _by_ a mini-coder agent. It's agents all the way down. 🐒
66
63
  - **I'm tiny but mighty.** The whole runtime is [Bun.js](https://bun.com) β€” fast startup, native TypeScript, and a built-in SQLite driver.
67
- - **I respect existing conventions.** Hook scripts live in `.agents/hooks/`, context in `AGENTS.md` or `CLAUDE.md`, commands in `.agents/commands/`, agents in `.agents/agents/`, skills in `.agents/skills/` β€” I follow the ecosystem instead of inventing new standards.
64
+ - **I respect existing conventions.** Context lives in `AGENTS.md` or `CLAUDE.md`, commands in `.agents/commands/`, agents in `.agents/agents/`, skills in `.agents/skills/` β€” I follow the ecosystem instead of inventing new standards.
68
65
  - **I spin while I think.** ⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏ (It's the little things.)
69
66
  - **I can clone myself.** The `subagent` tool lets me spin up parallel instances of myself to tackle independent subtasks simultaneously. Divide and conquer! (Up to 10 levels deep.)
70
67
 
@@ -82,7 +79,6 @@ I follow the [`.agents` convention](https://github.com/agentsmd/agents) β€” the
82
79
  | `.claude/agents/*.md` | Alternate `.claude` path for custom agents |
83
80
  | `.agents/skills/<name>/SKILL.md` | Reusable skill instructions (`@name`) |
84
81
  | `.claude/skills/<name>/SKILL.md` | Claude-compatible skills |
85
- | `.agents/hooks/post-<tool>` | Scripts run after supported built-in tool calls |
86
82
  | `.agents/AGENTS.md` | Preferred local project context |
87
83
  | `CLAUDE.md` | Local fallback context if `.agents/AGENTS.md` is absent |
88
84
  | `AGENTS.md` | Local fallback context if `.agents/AGENTS.md` and `CLAUDE.md` are absent |
@@ -119,7 +115,7 @@ export EXA_API_KEY=your-exa-key # enables webSearch/webContent
119
115
  mc
120
116
  ```
121
117
 
122
- Or drop me a prompt straight away and stay in the session:
118
+ Or drop me a prompt straight away for one-shot mode (runs once, then exits):
123
119
 
124
120
  ```bash
125
121
  mc "Refactor the auth module to use async/await"
@@ -142,7 +138,7 @@ mc -h # show help
142
138
 
143
139
  Everything I remember lives in `~/.config/mini-coder/` β€” here's what I'm holding onto:
144
140
 
145
- - `sessions.db` β€” your full session history, `/undo` snapshots, MCP server config, and model metadata, all in one tidy SQLite file
141
+ - `sessions.db` β€” your full session history, MCP server config, and model metadata, all in one tidy SQLite file
146
142
  - `api.log` β€” a request/response log for every provider call this run, if you want to peek under the hood
147
143
  - `errors.log` β€” anything that went sideways, caught and written down so you can actually debug it
148
144
 
@@ -156,7 +152,6 @@ The README hits the highlights β€” the docs have the full story:
156
152
  - [docs/custom-agents.md](docs/custom-agents.md)
157
153
  - [docs/skills.md](docs/skills.md)
158
154
  - [docs/configs.md](docs/configs.md)
159
- - [docs/tool-hooks.md](docs/tool-hooks.md)
160
155
 
161
156
  ## πŸ—‚οΈ Project Structure
162
157
 
@@ -166,8 +161,9 @@ src/
166
161
  agent/ # Main REPL loop + tool registry
167
162
  cli/ # Input, output, slash commands, markdown rendering
168
163
  llm-api/ # Provider factory + streaming turn logic
169
- tools/ # glob, grep, read, create, replace, insert, shell, subagent
170
- # + webSearch, webContent, hashline anchors, diffs, hooks, snapshots
164
+ tools/ # shell, subagent, skill tools
165
+ # + webSearch, webContent
166
+ internal/ # shared internals, including the mc-edit helper implementation
171
167
  mcp/ # MCP server connections
172
168
  session/ # SQLite-backed session & history management
173
169
  ```
@@ -0,0 +1,251 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+
4
+ // src/cli/structured-output.ts
5
+ import { createTwoFilesPatch } from "diff";
6
+ function normalizePatchLines(patchText) {
7
+ const patchLines = patchText.split(`
8
+ `);
9
+ while (patchLines.at(-1) === "") {
10
+ patchLines.pop();
11
+ }
12
+ const diffLines = patchLines.filter((line) => !line.startsWith("Index: ") && line !== "===================================================================");
13
+ return diffLines.map((line) => {
14
+ if (line.startsWith("--- ") || line.startsWith("+++ ")) {
15
+ return line.split("\t", 1)[0] ?? line;
16
+ }
17
+ return line;
18
+ });
19
+ }
20
+ function renderUnifiedDiff(filePath, before, after) {
21
+ if (before === after) {
22
+ return "(no changes)";
23
+ }
24
+ const patchText = createTwoFilesPatch(filePath, filePath, before, after, "", "", {
25
+ context: 3
26
+ });
27
+ return normalizePatchLines(patchText).join(`
28
+ `);
29
+ }
30
+ function renderMetadataBlock(result) {
31
+ const lines = [`ok: ${result.ok}`];
32
+ if ("path" in result && result.path) {
33
+ lines.push(`path: ${result.path}`);
34
+ }
35
+ if (result.ok) {
36
+ lines.push(`changed: ${result.changed}`);
37
+ } else {
38
+ lines.push(`code: ${result.code}`);
39
+ lines.push(`message: ${result.message}`);
40
+ }
41
+ return lines.join(`
42
+ `);
43
+ }
44
+ function writeFileEditResult(io, result) {
45
+ if (result.ok) {
46
+ const sections = [
47
+ renderUnifiedDiff(result.path, result.before, result.after),
48
+ renderMetadataBlock(result)
49
+ ];
50
+ io.stdout(`${sections.join(`
51
+
52
+ `)}
53
+ `);
54
+ return;
55
+ }
56
+ io.stderr(`${renderMetadataBlock(result)}
57
+ `);
58
+ }
59
+
60
+ // src/internal/file-edit/path.ts
61
+ import { homedir } from "os";
62
+ import { join, relative } from "path";
63
+ function stripMatchingQuotes(value) {
64
+ if (value.length < 2)
65
+ return value;
66
+ const first = value[0];
67
+ const last = value[value.length - 1];
68
+ if ((first === '"' || first === "'") && first === last) {
69
+ return value.slice(1, -1);
70
+ }
71
+ return value;
72
+ }
73
+ function normalizePathInput(pathInput) {
74
+ return stripMatchingQuotes(pathInput.trim());
75
+ }
76
+ function resolvePath(cwdInput, pathInput) {
77
+ const cwd = cwdInput ?? process.cwd();
78
+ const normalizedInput = normalizePathInput(pathInput);
79
+ const expanded = normalizedInput.startsWith("~/") ? join(homedir(), normalizedInput.slice(2)) : normalizedInput === "~" ? homedir() : normalizedInput;
80
+ const filePath = expanded.startsWith("/") ? expanded : join(cwd, expanded);
81
+ const relPath = relative(cwd, filePath);
82
+ return { cwd, filePath, relPath };
83
+ }
84
+
85
+ // src/internal/file-edit/exact-text.ts
86
+ class FileEditError extends Error {
87
+ code;
88
+ constructor(code, message) {
89
+ super(message);
90
+ this.code = code;
91
+ this.name = "FileEditError";
92
+ }
93
+ }
94
+ function findExactMatchOffsets(source, target) {
95
+ if (target.length === 0) {
96
+ throw new FileEditError("empty_old_text", "Expected text must be non-empty.");
97
+ }
98
+ const matches = [];
99
+ let searchStart = 0;
100
+ while (searchStart <= source.length - target.length) {
101
+ const matchIndex = source.indexOf(target, searchStart);
102
+ if (matchIndex === -1)
103
+ break;
104
+ matches.push(matchIndex);
105
+ searchStart = matchIndex + 1;
106
+ }
107
+ return matches;
108
+ }
109
+ function planExactTextEdit(source, oldText, newText) {
110
+ const matches = findExactMatchOffsets(source, oldText);
111
+ if (matches.length === 0) {
112
+ throw new FileEditError("target_not_found", "Expected text was not found in the file.");
113
+ }
114
+ if (matches.length > 1) {
115
+ throw new FileEditError("target_not_unique", "Expected text matched multiple locations in the file.");
116
+ }
117
+ const matchIndex = matches[0] ?? 0;
118
+ const updated = source.slice(0, matchIndex) + newText + source.slice(matchIndex + oldText.length);
119
+ return {
120
+ updated,
121
+ changed: updated !== source
122
+ };
123
+ }
124
+ async function applyExactTextEdit(input) {
125
+ const { filePath, relPath } = resolvePath(input.cwd, input.path);
126
+ const file = Bun.file(filePath);
127
+ if (!await file.exists()) {
128
+ throw new FileEditError("file_not_found", `File not found: "${relPath}".`);
129
+ }
130
+ const original = await file.text();
131
+ const plan = planExactTextEdit(original, input.oldText, input.newText);
132
+ if (plan.changed) {
133
+ await Bun.write(filePath, plan.updated);
134
+ }
135
+ return {
136
+ path: relPath,
137
+ changed: plan.changed,
138
+ before: original,
139
+ after: plan.updated
140
+ };
141
+ }
142
+
143
+ // src/internal/file-edit/cli.ts
144
+ var HELP = `Usage: mc-edit <path> (--old <text> | --old-file <path>) [--new <text> | --new-file <path>] [--cwd <path>]
145
+
146
+ Apply one safe exact-text edit to an existing file.
147
+ - The expected old text must match exactly once.
148
+ - Omit --new / --new-file to delete the matched text.
149
+ - Success output is human-oriented: plain unified diff first, metadata second.`;
150
+ async function readArgText(flag, filePath) {
151
+ const file = Bun.file(filePath);
152
+ if (!await file.exists()) {
153
+ throw new FileEditError("file_not_found", `${flag} file not found: "${filePath}".`);
154
+ }
155
+ return file.text();
156
+ }
157
+ async function parseFileEditCliArgs(argv) {
158
+ let cwd = process.cwd();
159
+ let path = null;
160
+ let oldText = null;
161
+ let oldFilePath = null;
162
+ let newText = null;
163
+ let newFilePath = null;
164
+ for (let i = 0;i < argv.length; i++) {
165
+ const arg = argv[i] ?? "";
166
+ switch (arg) {
167
+ case "--help":
168
+ case "-h":
169
+ return null;
170
+ case "--cwd":
171
+ cwd = argv[++i] ?? process.cwd();
172
+ break;
173
+ case "--old":
174
+ oldText = argv[++i] ?? "";
175
+ break;
176
+ case "--old-file":
177
+ oldFilePath = argv[++i] ?? null;
178
+ break;
179
+ case "--new":
180
+ newText = argv[++i] ?? "";
181
+ break;
182
+ case "--new-file":
183
+ newFilePath = argv[++i] ?? null;
184
+ break;
185
+ default:
186
+ if (arg.startsWith("-")) {
187
+ throw new Error(`Unknown flag: ${arg}`);
188
+ }
189
+ if (path !== null) {
190
+ throw new Error("Expected exactly one positional <path> argument.");
191
+ }
192
+ path = arg;
193
+ }
194
+ }
195
+ if (path === null) {
196
+ throw new Error("Missing required <path> argument.");
197
+ }
198
+ if (oldText === null === (oldFilePath === null)) {
199
+ throw new Error("Provide exactly one of --old or --old-file.");
200
+ }
201
+ if (newText !== null && newFilePath !== null) {
202
+ throw new Error("Provide at most one of --new or --new-file.");
203
+ }
204
+ return {
205
+ cwd,
206
+ path,
207
+ oldText: oldText ?? await readArgText("--old-file", oldFilePath ?? ""),
208
+ newText: newText ?? (newFilePath ? await readArgText("--new-file", newFilePath) : "")
209
+ };
210
+ }
211
+ function buildCliFailure(code, message, path) {
212
+ return {
213
+ ok: false,
214
+ code,
215
+ message,
216
+ ...path ? { path } : {}
217
+ };
218
+ }
219
+ function normalizeCliError(error, path) {
220
+ if (error instanceof FileEditError) {
221
+ return buildCliFailure(error.code, error.message, path);
222
+ }
223
+ if (error instanceof Error) {
224
+ return buildCliFailure("invalid_args", error.message, path);
225
+ }
226
+ return buildCliFailure("invalid_args", "Unknown error.", path);
227
+ }
228
+ async function runFileEditCli(argv, io = {
229
+ stdout: (text) => process.stdout.write(text),
230
+ stderr: (text) => process.stderr.write(text)
231
+ }) {
232
+ let parsed = null;
233
+ try {
234
+ parsed = await parseFileEditCliArgs(argv);
235
+ if (parsed === null) {
236
+ io.stderr(`${HELP}
237
+ `);
238
+ return 0;
239
+ }
240
+ const result = await applyExactTextEdit(parsed);
241
+ writeFileEditResult(io, { ok: true, ...result });
242
+ return 0;
243
+ } catch (error) {
244
+ writeFileEditResult(io, normalizeCliError(error, parsed?.path));
245
+ return 1;
246
+ }
247
+ }
248
+
249
+ // src/mc-edit.ts
250
+ var exitCode = await runFileEditCli(process.argv.slice(2));
251
+ process.exit(exitCode);