mini-coder 0.0.7 → 0.0.9

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/hanging-bug.md ADDED
@@ -0,0 +1,78 @@
1
+ # Hanging Bug Investigation
2
+
3
+ ## Symptoms
4
+
5
+ After a shell tool call completes successfully, **SOMETIMES** the app hangs indefinitely instead of
6
+ returning to the prompt. Two observed states:
7
+
8
+ ```
9
+ $ $ rm src/session/db.ts && bun run test
10
+ ⠇ shell
11
+ ```
12
+
13
+ Stayed spinning with shell label for well over the timeout time. I had to kill the app.
14
+
15
+ Another example with a different shell command:
16
+ ```
17
+ $ $ git diff
18
+ ✔ 0
19
+ │ diff --git a/src/cli/tool-render.ts b/src/cli/tool-render.ts
20
+ │ index f8d33af..e224932 100644
21
+ │ --- a/src/cli/tool-render.ts
22
+ │ +++ b/src/cli/tool-render.ts
23
+ │ @@ -148,21 +148,25 @@ export function renderToolResultInline(
24
+ │ }
25
+
26
+ │ if (toolName === "glob") {
27
+ │ - const r = result as { files: string[]; truncated: boolean };
28
+ │ - const n = r.files.length;
29
+ │ - writeln(
30
+ │ - `${indent}${G.info} ${c.dim(n === 0 ? "no matches" : `${n} file${n === 1 ? "" : "s"}${r.truncated ? " (capped)" : ""}`)}`,
31
+ │ - );
32
+ │ - return;
33
+ │ + const r = result as { files?: string[]; truncated?: boolean };
34
+ │ + if (Array.isArray(r.files)) {
35
+ │ + const n = r.files.length;
36
+ │ + writeln(
37
+ │ + `${indent}${G.info} ${c.dim(n === 0 ? "no matches" : `${n} file${n === 1 ? "" : "s"}${r.truncated ? " (capped)" : ""}`)}`,
38
+ │ + );
39
+ │ … +150 lines
40
+ ⠦ thinking
41
+ ```
42
+ User also had to kill the app.
43
+
44
+ Only seen with Gemini/Google models.
45
+ Only happens after shell tool calls.
46
+ Other tools calls worked just fine during the same session with the same model and mini-coder version.
47
+ There are no shell tool related hooks configured anywhere.
48
+
49
+ The expectation was the turn to continue as normal since the tool calls succeded.
50
+
51
+ Find the root cause, show proof.
52
+
53
+ ## Findings:
54
+
55
+ Here is the concrete proof I just ran, no edits:
56
+ - I executed renderTurn with an event stream that does:
57
+ 1) tool-call-start(shell)
58
+ 2) tool-result(shell success)
59
+ 3) then never yields again
60
+ - Result: the promise did not resolve within 700ms (timed_out_700ms).
61
+ Then I ran the same sequence but added turn-complete:
62
+ - Result: it resolved immediately (resolved).
63
+ So this is now proven in your codepath:
64
+ - renderTurn waits forever when the upstream async stream goes silent after a shell tool result.
65
+ - This exactly matches the visible hang states.
66
+
67
+ ### Root Cause 1: Hangs spinning on `"shell"`
68
+ **Proof in code:** `src/tools/shell.ts`
69
+ * When a command times out, `proc.kill("SIGTERM")` only kills the parent process (e.g., `bash`). Any child processes (e.g., `bun`) become orphaned but stay alive, holding the write end of the `stdout`/`stderr` pipes open.
70
+ * Because the pipe never closes, `await reader.read()` inside `collectStream()` hangs indefinitely.
71
+ * Because `collectStream()` never resolves, the tool execution never finishes, `tool-result` is never yielded, and the stream goes completely silent while the spinner stays stuck on "shell".
72
+
73
+ ### Root Cause 2: Hangs spinning on `"thinking"`
74
+ **Proof in code:** `src/llm-api/turn.ts`
75
+ * After `git diff` completes, the tool resolves and `renderTurn` switches the spinner to `"thinking"`.
76
+ * The AI SDK automatically makes a new HTTP request to the Gemini API containing the tool result to generate the next step.
77
+ * Gemini's API occasionally hangs indefinitely or silently drops connections when receiving certain payloads (like large tool outputs or ANSI color codes, which `git diff` outputs).
78
+ * Because there is no timeout configured on the `streamText` call in `runTurn` (unless the user manually aborts), the underlying fetch request waits forever. The `result.fullStream` never yields the next chunk, but also never closes or errors.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mini-coder",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "A small, fast CLI coding agent",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
@@ -0,0 +1,169 @@
1
+ # Code Health Remediation Plan
2
+
3
+ ## Goal
4
+ Address maintainability and reliability issues identified in `code-health.md` with low-risk, incremental refactors that keep behavior stable.
5
+
6
+ ## Constraints
7
+ - Keep `mini-coder-idea.md` and `README.md` unchanged.
8
+ - Prefer small PR-sized changes with passing tests after each step.
9
+ - Preserve current CLI behavior while improving structure.
10
+
11
+ ## Workstreams
12
+
13
+ ### 1) Decompose `src/agent/agent.ts` (High)
14
+ **Outcome:** `runAgent` remains orchestration entrypoint; responsibilities split into focused modules.
15
+
16
+ **Steps:**
17
+ 1. Add `src/agent/reporter.ts` interface (narrow surface for output/status/tool events).
18
+ 2. Extract session lifecycle + turn loop into `src/agent/session-runner.ts`.
19
+ 3. Extract subagent execution into `src/agent/subagent-runner.ts`.
20
+ 4. Extract snapshot/undo helpers into `src/agent/undo-snapshot.ts`.
21
+ 5. Extract user input processing into `src/agent/input-loop.ts`.
22
+ 6. Keep `agent.ts` as composition/wiring file only.
23
+
24
+ **Checks:**
25
+ - Add/adjust unit tests around orchestration boundaries.
26
+ - Ensure no behavior regressions in interrupts, resume, and tool-call flows.
27
+
28
+ ---
29
+
30
+ ### 2) Decompose `src/cli/output.ts` (High)
31
+ **Outcome:** Rendering responsibilities isolated and testable.
32
+
33
+ **Target modules:**
34
+ - `src/cli/spinner.ts`
35
+ - `src/cli/tool-render.ts`
36
+ - `src/cli/stream-render.ts`
37
+ - `src/cli/status-bar.ts`
38
+ - `src/cli/error-render.ts`
39
+ - `src/cli/output.ts` as facade
40
+
41
+ **Steps:**
42
+ 1. Extract pure formatting helpers first (no IO).
43
+ 2. Extract spinner lifecycle module.
44
+ 3. Extract stream queue/tick/flush behavior.
45
+ 4. Keep compatibility exports in `output.ts` to avoid broad callsite churn.
46
+
47
+ **Checks:**
48
+ - Add focused tests for formatting + stream behavior.
49
+ - Verify terminal rendering remains stable manually.
50
+
51
+ ---
52
+
53
+ ### 3) Introduce `TerminalIO` abstraction (Medium)
54
+ **Outcome:** Centralized process/TTY interactions and signal lifecycle.
55
+
56
+ **Steps:**
57
+ 1. Create `src/cli/terminal-io.ts` with methods for stdout/stderr writes, raw mode, signal subscriptions.
58
+ 2. Replace direct `process.*` use in output/input stack with injected `TerminalIO`.
59
+ 3. Centralize signal registration/unregistration in one lifecycle owner.
60
+
61
+ **Checks:**
62
+ - Add unit tests for signal registration cleanup semantics.
63
+ - Confirm no stuck raw-mode edge cases.
64
+
65
+ ---
66
+
67
+ ### 4) Split DB layer by domain (Medium)
68
+ **Outcome:** Reduced blast radius and clearer data ownership.
69
+
70
+ **Target modules:**
71
+ - `src/session/db/connection.ts`
72
+ - `src/session/db/session-repo.ts`
73
+ - `src/session/db/message-repo.ts`
74
+ - `src/session/db/settings-repo.ts`
75
+ - `src/session/db/mcp-repo.ts`
76
+ - `src/session/db/snapshot-repo.ts`
77
+ - `src/session/db/index.ts` (facade exports)
78
+
79
+ **Steps:**
80
+ 1. Move code without behavior changes.
81
+ 2. Keep SQL and schema unchanged initially.
82
+ 3. Replace direct `JSON.parse` in message loading with guarded parser:
83
+ - skip malformed rows
84
+ - emit diagnostic via logger/reporter
85
+
86
+ **Checks:**
87
+ - Add tests for malformed payload handling.
88
+ - Validate existing DB tests still pass.
89
+
90
+ ---
91
+
92
+ ### 5) Shared markdown config loader (Medium)
93
+ **Outcome:** Remove duplication across agents/skills/custom-commands.
94
+
95
+ **Steps:**
96
+ 1. Create `src/cli/load-markdown-configs.ts` with parameterized layout strategy.
97
+ 2. Migrate:
98
+ - `src/cli/agents.ts`
99
+ - `src/cli/skills.ts`
100
+ - `src/cli/custom-commands.ts`
101
+ 3. Keep precedence rules identical (built-in/user/project).
102
+ 4. Preserve existing frontmatter semantics.
103
+
104
+ **Checks:**
105
+ - Reuse/expand existing loader tests to cover parity.
106
+
107
+ ---
108
+
109
+ ### 6) Runtime/UI decoupling via reporter boundary (Medium)
110
+ **Outcome:** Core runtime no longer depends directly on terminal rendering.
111
+
112
+ **Steps:**
113
+ 1. Define domain events or reporter interface in `src/agent/reporter.ts`.
114
+ 2. Implement CLI reporter adapter in `src/cli/output-reporter.ts`.
115
+ 3. Replace direct output calls in agent runtime with reporter calls.
116
+
117
+ **Checks:**
118
+ - Add tests using test reporter to assert emitted events.
119
+
120
+ ---
121
+
122
+ ### 7) Error observability and silent catches (Medium)
123
+ **Outcome:** Non-fatal failures become diagnosable without crashing.
124
+
125
+ **Steps:**
126
+ 1. Find empty/broad catches in agent/output/loaders.
127
+ 2. Add debug-level diagnostics with contextual metadata.
128
+ 3. Keep user-facing behavior unchanged unless critical.
129
+
130
+ **Checks:**
131
+ - Validate noisy paths are still quiet at normal verbosity.
132
+
133
+ ---
134
+
135
+ ### 8) Startup FS sync usage (Low/Deferred)
136
+ **Outcome:** Optional responsiveness improvement if startup cost grows.
137
+
138
+ **Steps:**
139
+ 1. Measure startup and config-loading time first.
140
+ 2. If needed, move high-volume file scanning to async or cache results with invalidation.
141
+
142
+ ---
143
+
144
+ ### 9) Test hygiene cleanup (Low)
145
+ **Outcome:** Cleaner CI output.
146
+
147
+ **Steps:**
148
+ 1. Remove `console.log` skip notices in `src/tools/shell.test.ts`.
149
+ 2. Use test-framework-native skip annotations/helpers.
150
+
151
+ ---
152
+
153
+ ## Execution Order (recommended)
154
+ 1. Reporter interface (foundation for later decoupling).
155
+ 2. `agent.ts` decomposition.
156
+ 3. `output.ts` decomposition.
157
+ 4. Shared config loader extraction.
158
+ 5. DB module split + safe JSON parsing.
159
+ 6. TerminalIO + centralized signals.
160
+ 7. Silent catch diagnostics.
161
+ 8. Test hygiene and any deferred FS optimization.
162
+
163
+ ## Definition of Done
164
+ - `bun run typecheck && bun run format && bun run lint && bun test` passes.
165
+ - No behavior regressions in interactive CLI flows.
166
+ - `agent.ts` and `output.ts` materially reduced in size/responsibility.
167
+ - Config loader duplication removed.
168
+ - Message loading resilient to malformed JSON rows.
169
+ - New abstractions documented in code comments where non-obvious.
package/better-errors.md DELETED
@@ -1,96 +0,0 @@
1
- # Better Errors — Implementation Plan
2
-
3
- ## Overview
4
-
5
- Add structured error logging to file and friendly error parsing for display. Three concerns: (1) log full error details to `~/.config/mini-coder/errors.log`, (2) map known AI SDK errors to terse user-facing messages, (3) wire both into existing error surfaces.
6
-
7
- ---
8
-
9
- ## Files to Create
10
-
11
- | File | Purpose |
12
- |---|---|
13
- | `src/cli/error-log.ts` | `initErrorLog()`, `logError()` |
14
- | `src/cli/error-parse.ts` | `parseAppError()` |
15
-
16
- ## Files to Modify
17
-
18
- | File | Change |
19
- |---|---|
20
- | `src/index.ts` | Call `initErrorLog()` at startup; use `logError` + `parseAppError` in top-level catch and `main().catch()` |
21
- | `src/cli/output.ts` | Update `renderError(err: unknown)` to call log + parse; update `turn-error` branch in `renderTurn()` |
22
- | `src/agent/agent.ts` | Pass `unknown` to `renderError()` — no logic change needed if signature widens correctly |
23
-
24
- ---
25
-
26
- ## Implementation Steps
27
-
28
- 1. **Create `src/cli/error-log.ts`**
29
- - Module-level `let writer: ReturnType<ReturnType<typeof Bun.file>['writer']> | null = null`
30
- - `initErrorLog()`: if `writer` is not null, return early (idempotency). Otherwise resolve path `~/.config/mini-coder/errors.log`, open with `Bun.file(path).writer()` (truncates on open), assign to `writer`. Register `process.on('uncaughtException', (err) => { logError(err, 'uncaught'); process.exit(1) })`.
31
- - `logError(err: unknown, context?: string)`: if `writer` is null, return. Build log entry string (see Log Format), call `writer.write(entry)`. Keep sync-ish by not awaiting — `write()` on a Bun file writer is buffered; call `writer.flush()` after each write so data lands before a crash.
32
- - Extract error fields via type-narrowing helpers (not exported): `isObject(err)`, read `.name`, `.message`, `.stack`, `.statusCode`, `.url`, `.isRetryable` defensively.
33
-
34
- 2. **Create `src/cli/error-parse.ts`**
35
- - Import AI SDK error classes: `APICallError`, `RetryError`, `NoContentGeneratedError`, `LoadAPIKeyError`, `NoSuchModelError` from `ai`.
36
- - Export `parseAppError(err: unknown): { headline: string; hint?: string }`.
37
- - Implement as a chain of `instanceof` checks (see Error Parse Table). For `RetryError`, recurse on `.lastError` and prepend `"Retries exhausted: "` to `headline`.
38
- - Fallback: extract first non-empty line of `(err as any)?.message ?? String(err)`, no hint.
39
- - Network check: before the fallback, check if `(err as any)?.code === 'ECONNREFUSED'` or message includes `'ECONNREFUSED'`.
40
-
41
- 3. **Update `src/cli/output.ts`**
42
- - Change `renderError` signature from `(msg: string)` to `(err: unknown)`. Inside: call `logError(err, 'render')`, call `parseAppError(err)`, print `✖ red(headline)`, if `hint` print a dim indented hint line (e.g. ` dim(hint)`).
43
- - In `renderTurn()` `turn-error` branch (non-abort path): replace raw `event.error.message` display with `logError(event.error, 'turn')` then `parseAppError(event.error)` → print `✖ red(headline)`, optional dim hint. Keep the abort quiet-note branch unchanged.
44
- - All callers of `renderError` that currently pass a string (e.g. in `agent.ts`) — check each call site; if passing a plain string, wrap in `new Error(string)` or let `parseAppError` fallback handle a string gracefully (add string branch at top of `parseAppError`: `if (typeof err === 'string') return { headline: err }`).
45
-
46
- 4. **Update `src/index.ts`**
47
- - After `registerTerminalCleanup()` (or equivalent startup call), add `initErrorLog()`.
48
- - Top-level `catch` around `runAgent()`: replace any raw print with `logError(err, 'agent')` + `parseAppError(err)` → print `✖ red(headline)` + dim hint, then `process.exit(1)`.
49
- - `main().catch()`: same pattern — `logError(err, 'main')` + parse + print + `process.exit(1)`. Remove bare `console.error(err)`.
50
-
51
- 5. **Tests (`src/cli/error-parse.test.ts`)**
52
- - Test `parseAppError` for each mapped error type.
53
- - Construct real instances where possible (e.g. `new APICallError({ ... })`); check `headline` and `hint` values.
54
- - Test `RetryError` unwrapping.
55
- - Test fallback for plain `Error` and plain string.
56
- - No mocks, no file I/O, no server calls.
57
-
58
- ---
59
-
60
- ## Error Parse Table
61
-
62
- | Condition | `headline` | `hint` |
63
- |---|---|---|
64
- | `APICallError` with `statusCode === 429` | `"Rate limit hit"` | `"Wait a moment and retry, or switch model with /model"` |
65
- | `APICallError` with `statusCode === 401 \|\| 403` | `"Auth failed"` | `"Check the relevant provider API key env var"` |
66
- | `APICallError` other | `"API error \${statusCode}"` | `url` if present |
67
- | `RetryError` | `"Retries exhausted: \${inner.headline}"` | inner `hint` |
68
- | `NoContentGeneratedError` | `"Model returned empty response"` | `"Try rephrasing or switching model with /model"` |
69
- | `LoadAPIKeyError` | `"API key not found"` | `"Set the relevant provider env var"` |
70
- | `NoSuchModelError` | `"Model not found"` | `"Use /model to pick a valid model"` |
71
- | `code === 'ECONNREFUSED'` or message contains `'ECONNREFUSED'` | `"Connection failed"` | `"Check network or local server"` |
72
- | `string` input | string value | — |
73
- | fallback | first line of `err.message` | — |
74
-
75
- > `AbortError` is never passed to `parseAppError` — abort is handled at the call sites before reaching these functions.
76
-
77
- ---
78
-
79
- ## Log Format
80
-
81
- ```
82
- [2026-02-25T22:38:53.123Z] context=turn
83
- name: APICallError
84
- message: 429 Too Many Requests
85
- statusCode: 429
86
- url: https://api.anthropic.com/v1/messages
87
- isRetryable: true
88
- stack: APICallError: 429 Too Many Requests
89
- at ...
90
- ---
91
- ```
92
-
93
- - Each entry ends with `---\n`.
94
- - Extra fields (`statusCode`, `url`, `isRetryable`) are only emitted if present on the error object.
95
- - Stack is indented two spaces per line.
96
- - File is truncated on each app start (writer opened without append flag).