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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mini-coder",
3
- "version": "0.5.11",
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.7.2",
22
- "@cel-tui/core": "^0.7.2",
23
- "@cel-tui/types": "^0.7.2",
24
- "@mariozechner/pi-ai": "^0.66.1"
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.5",
30
+ "lefthook": "^2.1.6",
30
31
  "prettier": "^3.8.3",
31
- "typescript": "^6.0.2"
32
+ "typescript": "^6.0.3"
32
33
  },
33
34
  "license": "MIT"
34
35
  }
@@ -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 output.
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
- /** Write text to stdout with broken-pipe handling. */
42
+ /** Queue text for one output stream with broken-pipe handling. */
36
43
  write(text: string): void;
37
- /** Attach SIGINT/stdout error handlers for the active run. */
44
+ /** Attach error handlers for the active run. */
38
45
  attach(): void;
39
- /** Remove SIGINT/stdout error handlers after the run. */
46
+ /** Remove error handlers after the run. */
40
47
  detach(): void;
41
- /** Resolve the final stop reason, converting broken pipes into quiet shutdowns. */
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(text: string): void {
50
- process.stdout.write(text);
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
- writeImpl: (text: string) => void,
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 stdoutErrorHandler = (error: unknown): void => {
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
- try {
160
- writeImpl(text);
161
- } catch (error) {
162
- if (!isBrokenPipeError(error)) {
163
- throw error;
233
+ pendingWrite = pendingWrite.then(async () => {
234
+ if (brokenPipe || outputError) {
235
+ return;
164
236
  }
165
- stopForBrokenPipe();
166
- }
237
+
238
+ try {
239
+ await writeImpl(text);
240
+ } catch (error) {
241
+ failOutput(error);
242
+ }
243
+ });
167
244
  },
168
245
  attach() {
169
- process.stdout.on("error", stdoutErrorHandler);
170
- process.on("SIGINT", sigintHandler);
246
+ stream.on("error", streamErrorHandler);
247
+ if (attachSigint) {
248
+ process.on("SIGINT", sigintHandler);
249
+ }
171
250
  },
172
251
  detach() {
173
- process.stdout.off("error", stdoutErrorHandler);
174
- process.off("SIGINT", sigintHandler);
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
- options?.writeLine ?? ((line) => defaultWrite(`${line}\n`)),
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 only the final assistant text.
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. Only the final persisted assistant
236
- * message's text content is written to stdout.
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 output = createHeadlessOutputController(
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
- options?.writeText ?? defaultWrite,
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
- if (event.type === "assistant_message") {
257
- finalAssistantMessage = event.message;
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
- output.attach();
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
- output.write(finalText);
379
+ finalOutput.write(finalText);
273
380
  }
274
- return output.finalize(stopReason);
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
- output.detach();
389
+ activityOutput.detach();
390
+ finalOutput.detach();
277
391
  }
278
392
  }