mini-coder 0.5.11 → 0.5.13
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/BENCHMARK.md +107 -0
- package/PROGRESS.md +4 -0
- package/README.md +66 -21
- package/assets/mc-claude-smart.png +0 -0
- package/assets/mc-gpt-smart.png +0 -0
- package/benchmark-baseline.sh +15 -0
- package/benchmark-loop.sh +19 -0
- package/bun.lock +265 -90
- package/package.json +8 -7
- package/skills-lock.json +15 -0
- package/src/agent.ts +20 -0
- package/src/cli.ts +2 -1
- package/src/headless.ts +152 -38
- package/src/index.ts +107 -143
- package/src/input.ts +13 -1
- package/src/mcp.ts +609 -0
- package/src/prompt.ts +10 -12
- package/src/session-message.ts +393 -0
- package/src/session.ts +102 -396
- package/src/settings.ts +218 -22
- package/src/shared.ts +39 -0
- package/src/skills.ts +12 -3
- package/src/submit.ts +20 -25
- package/src/text.ts +71 -0
- package/src/theme.ts +186 -3
- package/src/tool-common.ts +93 -0
- package/src/tool-grep.ts +606 -0
- package/src/tool-read.ts +313 -0
- package/src/tool-shell.ts +1001 -0
- package/src/tools.ts +190 -995
- package/src/ui/agent.ts +206 -110
- package/src/ui/commands.test.ts +489 -17
- package/src/ui/commands.ts +231 -55
- package/src/ui/conversation.test.ts +585 -0
- package/src/ui/conversation.ts +878 -455
- package/src/ui/help.ts +27 -11
- package/src/ui/input.test.ts +1 -43
- package/src/ui/runtime.ts +69 -0
- package/src/ui.ts +422 -185
- package/src/plugins.ts +0 -183
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mini-coder",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.13",
|
|
4
4
|
"description": "A small, fast CLI coding agent",
|
|
5
5
|
"module": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -18,17 +18,18 @@
|
|
|
18
18
|
"test": "bun test"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@cel-tui/components": "^0.
|
|
22
|
-
"@cel-tui/core": "^0.
|
|
23
|
-
"@cel-tui/types": "^0.
|
|
24
|
-
"@mariozechner/pi-ai": "^0.
|
|
21
|
+
"@cel-tui/components": "^0.8.1",
|
|
22
|
+
"@cel-tui/core": "^0.8.1",
|
|
23
|
+
"@cel-tui/types": "^0.8.1",
|
|
24
|
+
"@mariozechner/pi-ai": "^0.67.68",
|
|
25
|
+
"@modelcontextprotocol/sdk": "^1.29.0"
|
|
25
26
|
},
|
|
26
27
|
"devDependencies": {
|
|
27
28
|
"@biomejs/biome": "^2.4.12",
|
|
28
29
|
"@types/bun": "^1.3.12",
|
|
29
|
-
"lefthook": "^2.1.
|
|
30
|
+
"lefthook": "^2.1.6",
|
|
30
31
|
"prettier": "^3.8.3",
|
|
31
|
-
"typescript": "^6.0.
|
|
32
|
+
"typescript": "^6.0.3"
|
|
32
33
|
},
|
|
33
34
|
"license": "MIT"
|
|
34
35
|
}
|
package/skills-lock.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"skills": {
|
|
4
|
+
"bun-development": {
|
|
5
|
+
"source": "sickn33/antigravity-awesome-skills",
|
|
6
|
+
"sourceType": "github",
|
|
7
|
+
"computedHash": "87e59a1e7f7fe512333aad64022955501a4e36be6e65487196dfc311e5b786b7"
|
|
8
|
+
},
|
|
9
|
+
"typescript-advanced-types": {
|
|
10
|
+
"source": "wshobson/agents",
|
|
11
|
+
"sourceType": "github",
|
|
12
|
+
"computedHash": "3a3be8c925f96ac4e1280db28a01677e7ca5197f3792de2bbf35ab3bb324b6dd"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
package/src/agent.ts
CHANGED
|
@@ -418,6 +418,19 @@ function buildIncompleteAssistantMessage(
|
|
|
418
418
|
};
|
|
419
419
|
}
|
|
420
420
|
|
|
421
|
+
async function resolveStreamResultSoon(
|
|
422
|
+
streamResult: Promise<AssistantMessage>,
|
|
423
|
+
): Promise<AssistantMessage | undefined> {
|
|
424
|
+
const pending = Symbol("pending");
|
|
425
|
+
const result = await Promise.race([
|
|
426
|
+
streamResult,
|
|
427
|
+
new Promise<typeof pending>((resolve) => {
|
|
428
|
+
setTimeout(() => resolve(pending), 0);
|
|
429
|
+
}),
|
|
430
|
+
]);
|
|
431
|
+
return result === pending ? undefined : result;
|
|
432
|
+
}
|
|
433
|
+
|
|
421
434
|
async function streamAssistantMessage(
|
|
422
435
|
opts: Pick<
|
|
423
436
|
RunAgentOpts,
|
|
@@ -455,7 +468,13 @@ async function streamAssistantMessage(
|
|
|
455
468
|
}
|
|
456
469
|
|
|
457
470
|
// `end(result)` resolves the final result without emitting a terminal event.
|
|
471
|
+
// Some wrappers settle that promise on the next task, so give it one more
|
|
472
|
+
// turn before treating the stream as incomplete.
|
|
458
473
|
await Promise.resolve();
|
|
474
|
+
if (!assistantMessage && !settledStreamResult) {
|
|
475
|
+
settledStreamResult = await resolveStreamResultSoon(streamResult);
|
|
476
|
+
}
|
|
477
|
+
|
|
459
478
|
const finalAssistantMessage =
|
|
460
479
|
assistantMessage ??
|
|
461
480
|
settledStreamResult ??
|
|
@@ -596,6 +615,7 @@ function appendToolResultMessage(
|
|
|
596
615
|
toolCallId: toolCall.id,
|
|
597
616
|
toolName: toolCall.name,
|
|
598
617
|
content: result.content,
|
|
618
|
+
...(result.details !== undefined ? { details: result.details } : {}),
|
|
599
619
|
isError: result.isError,
|
|
600
620
|
timestamp: Date.now(),
|
|
601
621
|
};
|
package/src/cli.ts
CHANGED
|
@@ -32,7 +32,8 @@ export interface TtyState {
|
|
|
32
32
|
* Parse supported CLI arguments.
|
|
33
33
|
*
|
|
34
34
|
* Supports `-p, --prompt <text>` for headless one-shot mode and `--json`
|
|
35
|
-
* to stream NDJSON events instead of the default final-text
|
|
35
|
+
* to stream NDJSON events instead of the default final-text mode
|
|
36
|
+
* (stdout final answer plus stderr activity snippets).
|
|
36
37
|
* Unknown flags and positional arguments fail eagerly.
|
|
37
38
|
*
|
|
38
39
|
* @param argv - Process arguments excluding the Bun executable and script path.
|
package/src/headless.ts
CHANGED
|
@@ -12,6 +12,11 @@ import {
|
|
|
12
12
|
type SubmitTurnHooks,
|
|
13
13
|
submitResolvedInput,
|
|
14
14
|
} from "./submit.ts";
|
|
15
|
+
import {
|
|
16
|
+
collapseWhitespaceToNull,
|
|
17
|
+
joinTextBlocks,
|
|
18
|
+
truncateText,
|
|
19
|
+
} from "./text.ts";
|
|
15
20
|
|
|
16
21
|
// ---------------------------------------------------------------------------
|
|
17
22
|
// Types
|
|
@@ -22,32 +27,80 @@ type HeadlessStopReason = "stop" | "length" | "error" | "aborted";
|
|
|
22
27
|
/** Options for a headless NDJSON run. */
|
|
23
28
|
export interface HeadlessRunOptions {
|
|
24
29
|
/** Optional line writer for completed NDJSON event output. */
|
|
25
|
-
writeLine?: (line: string) => void
|
|
30
|
+
writeLine?: (line: string) => void | Promise<void>;
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
/** Options for a headless final-text run. */
|
|
29
34
|
export interface HeadlessTextRunOptions {
|
|
35
|
+
/** Optional writer for lightweight assistant-activity snippets. */
|
|
36
|
+
writeActivity?: (text: string) => void | Promise<void>;
|
|
30
37
|
/** Optional writer for the final assistant text output. */
|
|
31
|
-
writeText?: (text: string) => void
|
|
38
|
+
writeText?: (text: string) => void | Promise<void>;
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
interface HeadlessOutputController {
|
|
35
|
-
/**
|
|
42
|
+
/** Queue text for one output stream with broken-pipe handling. */
|
|
36
43
|
write(text: string): void;
|
|
37
|
-
/** Attach
|
|
44
|
+
/** Attach error handlers for the active run. */
|
|
38
45
|
attach(): void;
|
|
39
|
-
/** Remove
|
|
46
|
+
/** Remove error handlers after the run. */
|
|
40
47
|
detach(): void;
|
|
41
|
-
/**
|
|
42
|
-
finalize(stopReason: HeadlessStopReason): HeadlessStopReason
|
|
48
|
+
/** Wait for queued writes and resolve the final stop reason. */
|
|
49
|
+
finalize(stopReason: HeadlessStopReason): Promise<HeadlessStopReason>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface HeadlessProcessStream {
|
|
53
|
+
/** Register an output-stream error handler. */
|
|
54
|
+
on(event: "error", listener: (error: unknown) => void): void;
|
|
55
|
+
/** Remove an output-stream error handler. */
|
|
56
|
+
off(event: "error", listener: (error: unknown) => void): void;
|
|
57
|
+
/** Write a text chunk to the stream. */
|
|
58
|
+
write(text: string, callback?: () => void): boolean;
|
|
43
59
|
}
|
|
44
60
|
|
|
61
|
+
const HEADLESS_ACTIVITY_MAX_CHARS = 160;
|
|
62
|
+
|
|
45
63
|
// ---------------------------------------------------------------------------
|
|
46
64
|
// Helpers
|
|
47
65
|
// ---------------------------------------------------------------------------
|
|
48
66
|
|
|
49
|
-
function defaultWrite(
|
|
50
|
-
|
|
67
|
+
function defaultWrite(
|
|
68
|
+
stream: HeadlessProcessStream,
|
|
69
|
+
text: string,
|
|
70
|
+
): Promise<void> {
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
let settled = false;
|
|
73
|
+
|
|
74
|
+
const cleanup = (): void => {
|
|
75
|
+
stream.off("error", handleError);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const settle = (callback: () => void): void => {
|
|
79
|
+
if (settled) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
settled = true;
|
|
83
|
+
cleanup();
|
|
84
|
+
callback();
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const handleError = (error: unknown): void => {
|
|
88
|
+
settle(() => {
|
|
89
|
+
reject(error);
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
stream.on("error", handleError);
|
|
94
|
+
try {
|
|
95
|
+
stream.write(text, () => {
|
|
96
|
+
settle(resolve);
|
|
97
|
+
});
|
|
98
|
+
} catch (error) {
|
|
99
|
+
settle(() => {
|
|
100
|
+
reject(error);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
});
|
|
51
104
|
}
|
|
52
105
|
|
|
53
106
|
function isBrokenPipeError(error: unknown): boolean {
|
|
@@ -110,6 +163,17 @@ function extractAssistantText(message: AssistantMessage | null): string {
|
|
|
110
163
|
.join("");
|
|
111
164
|
}
|
|
112
165
|
|
|
166
|
+
function extractAssistantActivitySnippet(
|
|
167
|
+
message: AssistantMessage,
|
|
168
|
+
): string | null {
|
|
169
|
+
if (!message.content.some((block) => block.type === "toolCall")) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const text = collapseWhitespaceToNull(joinTextBlocks(message.content));
|
|
174
|
+
return text ? truncateText(text, HEADLESS_ACTIVITY_MAX_CHARS) : null;
|
|
175
|
+
}
|
|
176
|
+
|
|
113
177
|
function shouldWriteHeadlessJsonEvent(event: AgentEvent): boolean {
|
|
114
178
|
switch (event.type) {
|
|
115
179
|
case "user_message":
|
|
@@ -126,11 +190,17 @@ function shouldWriteHeadlessJsonEvent(event: AgentEvent): boolean {
|
|
|
126
190
|
|
|
127
191
|
function createHeadlessOutputController(
|
|
128
192
|
state: AppState,
|
|
129
|
-
|
|
193
|
+
stream: HeadlessProcessStream,
|
|
194
|
+
writeImpl: (text: string) => void | Promise<void>,
|
|
195
|
+
options?: {
|
|
196
|
+
attachSigint?: boolean;
|
|
197
|
+
},
|
|
130
198
|
): HeadlessOutputController {
|
|
131
199
|
let brokenPipe = false;
|
|
132
200
|
let outputError: unknown = null;
|
|
201
|
+
let pendingWrite = Promise.resolve();
|
|
133
202
|
const sigintHandler = createSigintHandler(state);
|
|
203
|
+
const attachSigint = options?.attachSigint ?? true;
|
|
134
204
|
|
|
135
205
|
const stopForBrokenPipe = (): void => {
|
|
136
206
|
if (brokenPipe) {
|
|
@@ -140,40 +210,52 @@ function createHeadlessOutputController(
|
|
|
140
210
|
state.abortController?.abort();
|
|
141
211
|
};
|
|
142
212
|
|
|
143
|
-
const
|
|
213
|
+
const failOutput = (error: unknown): void => {
|
|
144
214
|
if (isBrokenPipeError(error)) {
|
|
145
215
|
stopForBrokenPipe();
|
|
146
216
|
return;
|
|
147
217
|
}
|
|
148
218
|
|
|
149
|
-
outputError = error;
|
|
219
|
+
outputError = outputError ?? error;
|
|
150
220
|
state.abortController?.abort();
|
|
151
221
|
};
|
|
152
222
|
|
|
223
|
+
const streamErrorHandler = (error: unknown): void => {
|
|
224
|
+
failOutput(error);
|
|
225
|
+
};
|
|
226
|
+
|
|
153
227
|
return {
|
|
154
228
|
write(text) {
|
|
155
|
-
if (brokenPipe) {
|
|
229
|
+
if (brokenPipe || outputError) {
|
|
156
230
|
return;
|
|
157
231
|
}
|
|
158
232
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (!isBrokenPipeError(error)) {
|
|
163
|
-
throw error;
|
|
233
|
+
pendingWrite = pendingWrite.then(async () => {
|
|
234
|
+
if (brokenPipe || outputError) {
|
|
235
|
+
return;
|
|
164
236
|
}
|
|
165
|
-
|
|
166
|
-
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
await writeImpl(text);
|
|
240
|
+
} catch (error) {
|
|
241
|
+
failOutput(error);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
167
244
|
},
|
|
168
245
|
attach() {
|
|
169
|
-
|
|
170
|
-
|
|
246
|
+
stream.on("error", streamErrorHandler);
|
|
247
|
+
if (attachSigint) {
|
|
248
|
+
process.on("SIGINT", sigintHandler);
|
|
249
|
+
}
|
|
171
250
|
},
|
|
172
251
|
detach() {
|
|
173
|
-
|
|
174
|
-
|
|
252
|
+
stream.off("error", streamErrorHandler);
|
|
253
|
+
if (attachSigint) {
|
|
254
|
+
process.off("SIGINT", sigintHandler);
|
|
255
|
+
}
|
|
175
256
|
},
|
|
176
|
-
finalize(stopReason) {
|
|
257
|
+
async finalize(stopReason) {
|
|
258
|
+
await pendingWrite;
|
|
177
259
|
if (outputError) {
|
|
178
260
|
throw outputError;
|
|
179
261
|
}
|
|
@@ -203,7 +285,8 @@ export async function runHeadlessPrompt(
|
|
|
203
285
|
const content = resolveHeadlessContent(state, rawInput);
|
|
204
286
|
const output = createHeadlessOutputController(
|
|
205
287
|
state,
|
|
206
|
-
|
|
288
|
+
process.stdout,
|
|
289
|
+
options?.writeLine ?? ((line) => defaultWrite(process.stdout, `${line}\n`)),
|
|
207
290
|
);
|
|
208
291
|
const hooks: SubmitTurnHooks = {
|
|
209
292
|
onEvent: (event) => {
|
|
@@ -222,18 +305,19 @@ export async function runHeadlessPrompt(
|
|
|
222
305
|
state,
|
|
223
306
|
hooks,
|
|
224
307
|
);
|
|
225
|
-
return output.finalize(stopReason);
|
|
308
|
+
return await output.finalize(stopReason);
|
|
226
309
|
} finally {
|
|
227
310
|
output.detach();
|
|
228
311
|
}
|
|
229
312
|
}
|
|
230
313
|
|
|
231
314
|
/**
|
|
232
|
-
* Run a single headless prompt to completion and write
|
|
315
|
+
* Run a single headless prompt to completion and write the final assistant text.
|
|
233
316
|
*
|
|
234
317
|
* The raw input is parsed with the same rules as interactive input. Slash
|
|
235
|
-
* commands are rejected in headless mode.
|
|
236
|
-
*
|
|
318
|
+
* commands are rejected in headless mode. The final assistant text is written
|
|
319
|
+
* to stdout, while lightweight assistant commentary snippets from tool-use
|
|
320
|
+
* turns are written to stderr.
|
|
237
321
|
*
|
|
238
322
|
* @param state - Mutable application state for the run.
|
|
239
323
|
* @param rawInput - Exact raw prompt text supplied by the user.
|
|
@@ -246,20 +330,43 @@ export async function runHeadlessPromptText(
|
|
|
246
330
|
options?: HeadlessTextRunOptions,
|
|
247
331
|
): Promise<HeadlessStopReason> {
|
|
248
332
|
const content = resolveHeadlessContent(state, rawInput);
|
|
249
|
-
const
|
|
333
|
+
const finalOutput = createHeadlessOutputController(
|
|
334
|
+
state,
|
|
335
|
+
process.stdout,
|
|
336
|
+
options?.writeText ?? ((text) => defaultWrite(process.stdout, text)),
|
|
337
|
+
);
|
|
338
|
+
const activityOutput = createHeadlessOutputController(
|
|
250
339
|
state,
|
|
251
|
-
|
|
340
|
+
process.stderr,
|
|
341
|
+
options?.writeActivity ?? ((text) => defaultWrite(process.stderr, text)),
|
|
342
|
+
{ attachSigint: false },
|
|
252
343
|
);
|
|
253
344
|
let finalAssistantMessage: AssistantMessage | null = null;
|
|
254
345
|
const hooks: SubmitTurnHooks = {
|
|
255
346
|
onEvent: (event) => {
|
|
256
|
-
|
|
257
|
-
|
|
347
|
+
switch (event.type) {
|
|
348
|
+
case "assistant_message": {
|
|
349
|
+
const activitySnippet = extractAssistantActivitySnippet(
|
|
350
|
+
event.message,
|
|
351
|
+
);
|
|
352
|
+
if (activitySnippet) {
|
|
353
|
+
activityOutput.write(`${activitySnippet}\n`);
|
|
354
|
+
}
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
case "done":
|
|
358
|
+
case "error":
|
|
359
|
+
case "aborted":
|
|
360
|
+
finalAssistantMessage = event.message;
|
|
361
|
+
return;
|
|
362
|
+
default:
|
|
363
|
+
return;
|
|
258
364
|
}
|
|
259
365
|
},
|
|
260
366
|
};
|
|
261
367
|
|
|
262
|
-
|
|
368
|
+
finalOutput.attach();
|
|
369
|
+
activityOutput.attach();
|
|
263
370
|
try {
|
|
264
371
|
const stopReason = await submitResolvedInput(
|
|
265
372
|
rawInput,
|
|
@@ -269,10 +376,17 @@ export async function runHeadlessPromptText(
|
|
|
269
376
|
);
|
|
270
377
|
const finalText = extractAssistantText(finalAssistantMessage);
|
|
271
378
|
if (finalText.length > 0) {
|
|
272
|
-
|
|
379
|
+
finalOutput.write(finalText);
|
|
273
380
|
}
|
|
274
|
-
|
|
381
|
+
const [finalStopReason, activityStopReason] = await Promise.all([
|
|
382
|
+
finalOutput.finalize(stopReason),
|
|
383
|
+
activityOutput.finalize(stopReason),
|
|
384
|
+
]);
|
|
385
|
+
return finalStopReason === "stop" || activityStopReason === "stop"
|
|
386
|
+
? "stop"
|
|
387
|
+
: stopReason;
|
|
275
388
|
} finally {
|
|
276
|
-
|
|
389
|
+
activityOutput.detach();
|
|
390
|
+
finalOutput.detach();
|
|
277
391
|
}
|
|
278
392
|
}
|