mini-coder 0.2.2 → 0.2.3
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 +3 -1
- package/dist/mc.js +81 -20
- package/docs/design-decisions.md +74 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
|
|
11
11
|
A terminal coding agent for developers who want a sharp tool, not a bloated IDE plugin. Shell-first, multi-provider, minimal tool surface. Just you, your terminal, and an AI that keeps up.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
<p align="center">
|
|
14
|
+
<img src="./assets/preview.gif" alt="Minicoder Preview"/>
|
|
15
|
+
</p>
|
|
14
16
|
|
|
15
17
|
---
|
|
16
18
|
|
package/dist/mc.js
CHANGED
|
@@ -13,7 +13,7 @@ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
|
13
13
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
14
14
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
15
15
|
// src/internal/version.ts
|
|
16
|
-
var PACKAGE_VERSION = "0.2.
|
|
16
|
+
var PACKAGE_VERSION = "0.2.3";
|
|
17
17
|
|
|
18
18
|
// src/mcp/client.ts
|
|
19
19
|
async function connectMcpServer(config) {
|
|
@@ -2400,6 +2400,7 @@ function renderStatusBar(opts) {
|
|
|
2400
2400
|
}
|
|
2401
2401
|
|
|
2402
2402
|
// src/cli/stream-render.ts
|
|
2403
|
+
import { basename as basename2 } from "path";
|
|
2403
2404
|
import * as c7 from "yoctocolors";
|
|
2404
2405
|
|
|
2405
2406
|
// src/llm-api/history/gemini.ts
|
|
@@ -3323,6 +3324,18 @@ async function renderTurn(events, spinner, opts) {
|
|
|
3323
3324
|
}
|
|
3324
3325
|
break;
|
|
3325
3326
|
}
|
|
3327
|
+
case "file-generated": {
|
|
3328
|
+
liveReasoning.finish();
|
|
3329
|
+
content.flushOpenContent();
|
|
3330
|
+
if (!quiet) {
|
|
3331
|
+
spinner.stop();
|
|
3332
|
+
if (renderedVisibleOutput)
|
|
3333
|
+
writeln();
|
|
3334
|
+
writeln(`${G.info} ${c7.dim("file")} ${c7.dim(event.mediaType)} ${c7.dim("\u2192")} ${basename2(event.filePath)}`);
|
|
3335
|
+
renderedVisibleOutput = true;
|
|
3336
|
+
}
|
|
3337
|
+
break;
|
|
3338
|
+
}
|
|
3326
3339
|
case "turn-complete": {
|
|
3327
3340
|
liveReasoning.finish();
|
|
3328
3341
|
content.flushOpenContent();
|
|
@@ -4399,6 +4412,30 @@ import { streamText } from "ai";
|
|
|
4399
4412
|
// src/llm-api/turn-execution.ts
|
|
4400
4413
|
import { dynamicTool, jsonSchema } from "ai";
|
|
4401
4414
|
|
|
4415
|
+
// src/llm-api/generated-files.ts
|
|
4416
|
+
import { join as join6 } from "path";
|
|
4417
|
+
var MEDIA_TYPE_TO_EXT = {
|
|
4418
|
+
"image/jpeg": "jpg",
|
|
4419
|
+
"image/svg+xml": "svg"
|
|
4420
|
+
};
|
|
4421
|
+
function extensionFromMediaType(mediaType) {
|
|
4422
|
+
if (MEDIA_TYPE_TO_EXT[mediaType])
|
|
4423
|
+
return MEDIA_TYPE_TO_EXT[mediaType];
|
|
4424
|
+
const slash = mediaType.indexOf("/");
|
|
4425
|
+
if (slash === -1 || slash === mediaType.length - 1)
|
|
4426
|
+
return "bin";
|
|
4427
|
+
return mediaType.slice(slash + 1);
|
|
4428
|
+
}
|
|
4429
|
+
var counter = 0;
|
|
4430
|
+
async function saveGeneratedFile(file, cwd) {
|
|
4431
|
+
counter += 1;
|
|
4432
|
+
const ext = extensionFromMediaType(file.mediaType);
|
|
4433
|
+
const name = `generated-${counter}.${ext}`;
|
|
4434
|
+
const filePath = join6(cwd, name);
|
|
4435
|
+
await Bun.write(filePath, file.uint8Array);
|
|
4436
|
+
return filePath;
|
|
4437
|
+
}
|
|
4438
|
+
|
|
4402
4439
|
// src/llm-api/turn-stream-events.ts
|
|
4403
4440
|
function shouldLogStreamChunk(c10) {
|
|
4404
4441
|
return c10.type !== "text-delta" && c10.type !== "reasoning" && c10.type !== "reasoning-delta";
|
|
@@ -4751,6 +4788,18 @@ async function* mapFullStreamToTurnEvents(stream, opts) {
|
|
|
4751
4788
|
yield { type: "context-pruned", ...rec };
|
|
4752
4789
|
}
|
|
4753
4790
|
}
|
|
4791
|
+
if (originalChunk.type === "file" && opts.cwd) {
|
|
4792
|
+
const fileData = originalChunk.file;
|
|
4793
|
+
if (fileData?.uint8Array) {
|
|
4794
|
+
const filePath = await saveGeneratedFile(fileData, opts.cwd);
|
|
4795
|
+
yield {
|
|
4796
|
+
type: "file-generated",
|
|
4797
|
+
filePath,
|
|
4798
|
+
mediaType: fileData.mediaType
|
|
4799
|
+
};
|
|
4800
|
+
continue;
|
|
4801
|
+
}
|
|
4802
|
+
}
|
|
4754
4803
|
const prepared = toolCallTracker.prepare(originalChunk);
|
|
4755
4804
|
const chunk = prepared.chunk;
|
|
4756
4805
|
const route = textPhaseTracker.route(chunk);
|
|
@@ -5171,6 +5220,12 @@ function buildTurnProviderOptions(input) {
|
|
|
5171
5220
|
const thinkingOpts = thinkingEffort ? getThinkingProviderOptions(modelString, thinkingEffort) : null;
|
|
5172
5221
|
const reasoningSummaryRequested = isRecord(thinkingOpts) && isRecord(thinkingOpts.openai) && typeof thinkingOpts.openai.reasoningSummary === "string";
|
|
5173
5222
|
const cacheFamily = getCacheFamily(modelString);
|
|
5223
|
+
const googleOpts = isGeminiModelFamily(modelString) ? {
|
|
5224
|
+
google: {
|
|
5225
|
+
responseModalities: ["TEXT", "IMAGE"],
|
|
5226
|
+
...isRecord(thinkingOpts?.google) ? thinkingOpts.google : {}
|
|
5227
|
+
}
|
|
5228
|
+
} : {};
|
|
5174
5229
|
const providerOptions = {
|
|
5175
5230
|
...thinkingOpts ?? {},
|
|
5176
5231
|
...isOpenAIGPT(modelString) ? {
|
|
@@ -5178,7 +5233,8 @@ function buildTurnProviderOptions(input) {
|
|
|
5178
5233
|
store: false,
|
|
5179
5234
|
...isRecord(thinkingOpts?.openai) ? thinkingOpts.openai : {}
|
|
5180
5235
|
}
|
|
5181
|
-
} : {}
|
|
5236
|
+
} : {},
|
|
5237
|
+
...googleOpts
|
|
5182
5238
|
};
|
|
5183
5239
|
return {
|
|
5184
5240
|
cacheFamily,
|
|
@@ -5256,7 +5312,8 @@ async function* runTurn(options) {
|
|
|
5256
5312
|
tools,
|
|
5257
5313
|
systemPrompt,
|
|
5258
5314
|
signal,
|
|
5259
|
-
thinkingEffort
|
|
5315
|
+
thinkingEffort,
|
|
5316
|
+
cwd
|
|
5260
5317
|
} = options;
|
|
5261
5318
|
const rawToolSet = buildToolSet(tools);
|
|
5262
5319
|
const toolSet = annotateToolCaching(rawToolSet, modelString);
|
|
@@ -5307,6 +5364,7 @@ async function* runTurn(options) {
|
|
|
5307
5364
|
result.response.catch(() => {});
|
|
5308
5365
|
for await (const event of mapFullStreamToTurnEvents(result.fullStream, {
|
|
5309
5366
|
stepPruneQueue,
|
|
5367
|
+
...cwd ? { cwd } : {},
|
|
5310
5368
|
onChunk: (streamChunk) => {
|
|
5311
5369
|
if (streamChunk.type === "tool-call" || streamChunk.type === "tool-result") {
|
|
5312
5370
|
logApiEvent("stream chunk", {
|
|
@@ -5722,7 +5780,8 @@ ${output}
|
|
|
5722
5780
|
tools: this.tools,
|
|
5723
5781
|
...systemPrompt ? { systemPrompt } : {},
|
|
5724
5782
|
signal: abortController.signal,
|
|
5725
|
-
...this.currentThinkingEffort ? { thinkingEffort: this.currentThinkingEffort } : {}
|
|
5783
|
+
...this.currentThinkingEffort ? { thinkingEffort: this.currentThinkingEffort } : {},
|
|
5784
|
+
cwd: this.cwd
|
|
5726
5785
|
});
|
|
5727
5786
|
const { inputTokens, outputTokens, contextTokens, newMessages } = await this.reporter.renderTurn(events, {
|
|
5728
5787
|
showReasoning: this.showReasoning,
|
|
@@ -5825,7 +5884,7 @@ import { z as z3 } from "zod";
|
|
|
5825
5884
|
|
|
5826
5885
|
// src/internal/file-edit/command.ts
|
|
5827
5886
|
import { existsSync as existsSync4 } from "fs";
|
|
5828
|
-
import { dirname as dirname3, extname, join as
|
|
5887
|
+
import { dirname as dirname3, extname, join as join7 } from "path";
|
|
5829
5888
|
import { fileURLToPath } from "url";
|
|
5830
5889
|
function quoteShellArg(value) {
|
|
5831
5890
|
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
@@ -5837,7 +5896,7 @@ function resolveSiblingFileEditScript(scriptPath) {
|
|
|
5837
5896
|
const mainDir = dirname3(scriptPath);
|
|
5838
5897
|
const mainBase = scriptPath.slice(mainDir.length + 1);
|
|
5839
5898
|
if (mainBase === `index${ext}` || mainBase === `mc${ext}`) {
|
|
5840
|
-
return
|
|
5899
|
+
return join7(mainDir, `mc-edit${ext}`);
|
|
5841
5900
|
}
|
|
5842
5901
|
return null;
|
|
5843
5902
|
}
|
|
@@ -5846,7 +5905,7 @@ function resolveModuleLocalFileEditScript(moduleUrl) {
|
|
|
5846
5905
|
const ext = extname(modulePath);
|
|
5847
5906
|
if (!ext)
|
|
5848
5907
|
return null;
|
|
5849
|
-
const helperPath =
|
|
5908
|
+
const helperPath = join7(dirname3(modulePath), "..", "..", `mc-edit${ext}`);
|
|
5850
5909
|
return existsSync4(helperPath) ? helperPath : null;
|
|
5851
5910
|
}
|
|
5852
5911
|
function resolveProcessScriptPath(mainModule, argv1) {
|
|
@@ -5875,21 +5934,22 @@ function buildFileEditShellPrelude(command = getFileEditCommand()) {
|
|
|
5875
5934
|
// src/tools/shell.ts
|
|
5876
5935
|
var ShellSchema = z3.object({
|
|
5877
5936
|
command: z3.string().describe("Shell command to execute"),
|
|
5878
|
-
timeout: z3.number().int().min(1000).
|
|
5879
|
-
env: z3.record(z3.string(), z3.string()).
|
|
5937
|
+
timeout: z3.number().int().min(1000).nullable().describe("Timeout in milliseconds. If omitted, the command runs until it exits."),
|
|
5938
|
+
env: z3.record(z3.string(), z3.string()).nullable().describe("Additional environment variables to set")
|
|
5880
5939
|
});
|
|
5881
5940
|
var MAX_OUTPUT_BYTES = 1e4;
|
|
5882
5941
|
async function runShellCommand(input) {
|
|
5883
5942
|
const cwd = input.cwd ?? process.cwd();
|
|
5884
|
-
const timeout = input.timeout;
|
|
5885
|
-
const
|
|
5943
|
+
const timeout = input.timeout ?? undefined;
|
|
5944
|
+
const inputEnv = input.env ?? undefined;
|
|
5945
|
+
const existingGitCount = Number(inputEnv?.GIT_CONFIG_COUNT ?? process.env.GIT_CONFIG_COUNT ?? "0") || 0;
|
|
5886
5946
|
const gitIdx = String(existingGitCount);
|
|
5887
5947
|
const env = Object.assign({}, process.env, {
|
|
5888
5948
|
FORCE_COLOR: "1",
|
|
5889
5949
|
GIT_CONFIG_COUNT: String(existingGitCount + 1),
|
|
5890
5950
|
[`GIT_CONFIG_KEY_${gitIdx}`]: "color.ui",
|
|
5891
5951
|
[`GIT_CONFIG_VALUE_${gitIdx}`]: "always"
|
|
5892
|
-
},
|
|
5952
|
+
}, inputEnv ?? {});
|
|
5893
5953
|
let timedOut = false;
|
|
5894
5954
|
const readers = [];
|
|
5895
5955
|
const wasRaw = process.stdin.isTTY ? process.stdin.isRaw : false;
|
|
@@ -6185,7 +6245,7 @@ ${c13.bold("Examples:")}`);
|
|
|
6185
6245
|
// src/cli/bootstrap.ts
|
|
6186
6246
|
import { existsSync as existsSync5, mkdirSync as mkdirSync2, writeFileSync } from "fs";
|
|
6187
6247
|
import { homedir as homedir6 } from "os";
|
|
6188
|
-
import { join as
|
|
6248
|
+
import { join as join8 } from "path";
|
|
6189
6249
|
import * as c14 from "yoctocolors";
|
|
6190
6250
|
var REVIEW_SKILL_CONTENT = `---
|
|
6191
6251
|
name: review
|
|
@@ -6216,8 +6276,8 @@ Review recent changes and provide actionable feedback.
|
|
|
6216
6276
|
- Keep feedback actionable: say what's wrong and suggest a fix.
|
|
6217
6277
|
`;
|
|
6218
6278
|
function bootstrapGlobalDefaults() {
|
|
6219
|
-
const skillDir =
|
|
6220
|
-
const skillPath =
|
|
6279
|
+
const skillDir = join8(homedir6(), ".agents", "skills", "review");
|
|
6280
|
+
const skillPath = join8(skillDir, "SKILL.md");
|
|
6221
6281
|
if (!existsSync5(skillPath)) {
|
|
6222
6282
|
mkdirSync2(skillDir, { recursive: true });
|
|
6223
6283
|
writeFileSync(skillPath, REVIEW_SKILL_CONTENT, "utf-8");
|
|
@@ -6226,7 +6286,7 @@ function bootstrapGlobalDefaults() {
|
|
|
6226
6286
|
}
|
|
6227
6287
|
|
|
6228
6288
|
// src/cli/file-refs.ts
|
|
6229
|
-
import { join as
|
|
6289
|
+
import { join as join9 } from "path";
|
|
6230
6290
|
async function resolveFileRefs(text, cwd) {
|
|
6231
6291
|
const atPattern = /@([\w./\-_]+)/g;
|
|
6232
6292
|
let result = text;
|
|
@@ -6236,7 +6296,7 @@ async function resolveFileRefs(text, cwd) {
|
|
|
6236
6296
|
const ref = match[1];
|
|
6237
6297
|
if (!ref)
|
|
6238
6298
|
continue;
|
|
6239
|
-
const filePath = ref.startsWith("/") ? ref :
|
|
6299
|
+
const filePath = ref.startsWith("/") ? ref : join9(cwd, ref);
|
|
6240
6300
|
if (isImageFilename(ref)) {
|
|
6241
6301
|
const attachment = await loadImageFile(filePath);
|
|
6242
6302
|
if (attachment) {
|
|
@@ -6264,7 +6324,7 @@ import * as c21 from "yoctocolors";
|
|
|
6264
6324
|
import { randomBytes } from "crypto";
|
|
6265
6325
|
import { unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
6266
6326
|
import { tmpdir } from "os";
|
|
6267
|
-
import { join as
|
|
6327
|
+
import { join as join10 } from "path";
|
|
6268
6328
|
import * as c20 from "yoctocolors";
|
|
6269
6329
|
|
|
6270
6330
|
// src/cli/commands-help.ts
|
|
@@ -6587,8 +6647,9 @@ async function handleModelSelect(ctx) {
|
|
|
6587
6647
|
const freeTag = model.free ? c18.green(" free") : "";
|
|
6588
6648
|
const contextTag = model.context ? c18.dim(` ${Math.round(model.context / 1000)}k`) : "";
|
|
6589
6649
|
const currentTag = isCurrent ? c18.cyan(" \u25C0") : "";
|
|
6650
|
+
const providerTag = c18.dim(` [${model.provider}]`);
|
|
6590
6651
|
return {
|
|
6591
|
-
label: `${model.displayName}${freeTag}${contextTag}${currentTag}`,
|
|
6652
|
+
label: `${model.displayName}${freeTag}${contextTag}${currentTag}${providerTag}`,
|
|
6592
6653
|
value: model.id,
|
|
6593
6654
|
filterText: `${model.id} ${model.displayName} ${model.provider}`
|
|
6594
6655
|
};
|
|
@@ -6755,7 +6816,7 @@ ${args}` : loaded2.content;
|
|
|
6755
6816
|
}
|
|
6756
6817
|
}
|
|
6757
6818
|
async function runForkedSkill(skillName, prompt, cwd) {
|
|
6758
|
-
const tmpFile =
|
|
6819
|
+
const tmpFile = join10(tmpdir(), `mc-fork-${randomBytes(8).toString("hex")}.md`);
|
|
6759
6820
|
writeFileSync2(tmpFile, prompt, "utf8");
|
|
6760
6821
|
try {
|
|
6761
6822
|
writeln(`${PREFIX.info} ${c20.dim("running subagent\u2026")}`);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Design Decisions
|
|
2
|
+
|
|
3
|
+
Documenting why mini-coder makes certain architectural choices — especially where we intentionally diverge from AI SDK defaults or common patterns.
|
|
4
|
+
|
|
5
|
+
## Why not ToolLoopAgent?
|
|
6
|
+
|
|
7
|
+
**Decision:** Use `streamText` directly instead of the AI SDK's `ToolLoopAgent`.
|
|
8
|
+
|
|
9
|
+
`ToolLoopAgent` is a convenience wrapper that manages the tool-call loop, context, and stopping conditions. Mini-coder needs explicit control over every aspect it abstracts away:
|
|
10
|
+
|
|
11
|
+
- **Streaming event rendering** — We yield granular `TurnEvent`s (text deltas, tool calls, tool results, reasoning, context-pruned notifications) as they arrive from `fullStream`. The reporter renders them append-only into the terminal in real time. `ToolLoopAgent` gives you the final result; we need the firehose.
|
|
12
|
+
- **ESC interrupt mid-turn** — An `AbortController` is wired through to `streamText`'s `signal`. On ESC, we abort, preserve partial messages, and append an interrupt stub so the LLM retains context. `ToolLoopAgent` doesn't expose this kind of mid-stream abort-and-preserve behavior.
|
|
13
|
+
- **Custom context pruning** — After every turn, `SessionRunner` runs `applyContextPruning` + `compactToolResultPayloads` on the in-memory history. This is rolling, per-turn pruning that must not break prompt caching. `ToolLoopAgent`'s built-in context management doesn't match these constraints.
|
|
14
|
+
- **Per-step DB persistence** — Each turn's messages are saved to SQLite with a turn index as they complete. The in-memory `coreHistory` diverges from the DB history (pruned vs. full). `ToolLoopAgent` has no hook for this.
|
|
15
|
+
- **Provider-specific caching annotations** — `annotateToolCaching` adds caching metadata to the tool set based on the model string, injected directly into the `streamText` call.
|
|
16
|
+
- **No step/tool-call limits** — Per the design: "No max steps or tool call limits — user can interrupt." `ToolLoopAgent` defaults to `stopWhen: stepCountIs(20)`.
|
|
17
|
+
|
|
18
|
+
**Summary:** `ToolLoopAgent` reduces boilerplate for simple request→response agents. Mini-coder is a shell-first coding agent where the loop _is_ the product. Using `ToolLoopAgent` would mean fighting the abstraction at every turn.
|
|
19
|
+
|
|
20
|
+
## Why no cross-session memory?
|
|
21
|
+
|
|
22
|
+
**Decision:** No agent-managed persistent memory across sessions. The repo and user-authored config files are the memory.
|
|
23
|
+
|
|
24
|
+
The AI SDK offers several memory approaches (Anthropic memory tool, Mem0, Letta, custom tools) that let agents save facts and recall them in future conversations. We intentionally don't use any of these.
|
|
25
|
+
|
|
26
|
+
### What we have instead
|
|
27
|
+
|
|
28
|
+
- **Within-session persistence** — Full message history saved to SQLite per-turn, sessions resumable via `/session`.
|
|
29
|
+
- **Context pruning** — `applyContextPruning` and `applyStepPruning` strip old reasoning/tool-calls to fit context windows without breaking prompt caching.
|
|
30
|
+
- **Static cross-session context** — `AGENTS.md`/`CLAUDE.md` files loaded into the system prompt. This is user-curated project knowledge, not agent-managed memory.
|
|
31
|
+
- **Skills** — Reusable instruction sets discoverable via `/` autocomplete.
|
|
32
|
+
|
|
33
|
+
### Why not agent-written memory?
|
|
34
|
+
|
|
35
|
+
We considered having the agent write to `~/.agents/AGENTS.md` for cross-session recall. Rejected because:
|
|
36
|
+
|
|
37
|
+
- **Intrusive** — `~/.agents/` is the user's space. Agent writes would mix generated noise with intentional configuration, creating surprises ("where did this line come from?").
|
|
38
|
+
- **Violates conventions** — `AGENTS.md`/`CLAUDE.md` are community standards meant to be human-authored instructions _to_ the agent, not an agent scratchpad. Using them as memory inverts the relationship.
|
|
39
|
+
- **Safety conflict** — Our own system prompt requires confirmation before irreversible actions. Silently modifying a user's global config violates that principle.
|
|
40
|
+
- **Complexity** — Memory adds storage, retrieval, relevance ranking, and non-determinism. The design philosophy is performance first, minimal setup.
|
|
41
|
+
|
|
42
|
+
### If we ever want this
|
|
43
|
+
|
|
44
|
+
A dedicated `~/.config/mini-coder/memories.md` that's clearly agent-owned and separate from user config would be the right path — not overloading existing community standards.
|
|
45
|
+
|
|
46
|
+
**Summary:** For a coding agent that operates on a repo, the repo _is_ the memory. Users who want cross-session context write it in `AGENTS.md` themselves — that's an intentional act, not an LLM side effect.
|
|
47
|
+
|
|
48
|
+
## Why no tool-call permissions?
|
|
49
|
+
|
|
50
|
+
**Decision:** No approval prompts, no blacklists, no whitelists. Every tool call executes immediately.
|
|
51
|
+
|
|
52
|
+
Our inspirations (Claude Code, OpenCode) require user approval for tool calls — shell commands, file writes, etc. We intentionally skip this.
|
|
53
|
+
|
|
54
|
+
### Permission systems provide a false sense of security
|
|
55
|
+
|
|
56
|
+
- **Shell bypasses everything.** An LLM with shell access can `curl`, `eval`, pipe through `bash`, encode payloads, or chain commands in ways no static blacklist can anticipate. Any permission scheme that allows shell but blocks specific patterns is playing whack-a-mole.
|
|
57
|
+
- **Blacklists and whitelists always have gaps.** Block `rm -rf /`? The model uses `find -delete`. Block `git push --force`? It uses `git push origin +main`. The surface area is unbounded.
|
|
58
|
+
- **Approval fatigue degrades security.** After the 20th "Allow shell command?" prompt, users auto-approve everything. The permission system trains the user to click "yes" reflexively — the opposite of its intent.
|
|
59
|
+
|
|
60
|
+
### Permissions are cumbersome
|
|
61
|
+
|
|
62
|
+
A coding agent runs dozens of shell commands per task. Requiring approval for each one destroys the flow that makes a CLI agent useful. The whole point of mini-coder is: small, fast, stays out of the way.
|
|
63
|
+
|
|
64
|
+
### Isolation is a separate concern
|
|
65
|
+
|
|
66
|
+
Sandboxing is a real need, but it belongs at the OS/container level — not inside the agent. Tools like [nono](https://nono.sh/) provide proper filesystem and network isolation that the LLM cannot circumvent. This is defense in depth done right: the agent runs unrestricted inside a sandbox that enforces actual boundaries.
|
|
67
|
+
|
|
68
|
+
### Our approach
|
|
69
|
+
|
|
70
|
+
- The system prompt includes safety rules (no secrets, confirm destructive actions, no unauthorized reverts).
|
|
71
|
+
- The user can interrupt at any time with ESC (preserve context) or Ctrl+C (hard exit).
|
|
72
|
+
- For real isolation, run mini-coder inside a sandboxed environment.
|
|
73
|
+
|
|
74
|
+
**Summary:** Permission dialogs give the appearance of safety without the substance. Real security comes from sandboxing the environment, not gatekeeping individual tool calls. Mini-coder codes — isolating it is a job for the right tool.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mini-coder",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "A small, fast CLI coding agent",
|
|
5
5
|
"module": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"diff": "^8.0.3",
|
|
32
32
|
"yoctocolors": "^2.1.2",
|
|
33
33
|
"yoctomarkdown": "^0.0.7",
|
|
34
|
-
"yoctoselect": "0.0.
|
|
34
|
+
"yoctoselect": "0.0.3",
|
|
35
35
|
"zod": "^4.3.6"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|