mini-coder 0.5.12 → 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.12",
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
@@ -27,31 +32,47 @@ export interface HeadlessRunOptions {
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
38
  writeText?: (text: string) => void | Promise<void>;
32
39
  }
33
40
 
34
41
  interface HeadlessOutputController {
35
- /** Queue text for 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
48
  /** Wait for queued writes and resolve the final stop reason. */
42
49
  finalize(stopReason: HeadlessStopReason): Promise<HeadlessStopReason>;
43
50
  }
44
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;
59
+ }
60
+
61
+ const HEADLESS_ACTIVITY_MAX_CHARS = 160;
62
+
45
63
  // ---------------------------------------------------------------------------
46
64
  // Helpers
47
65
  // ---------------------------------------------------------------------------
48
66
 
49
- function defaultWrite(text: string): Promise<void> {
67
+ function defaultWrite(
68
+ stream: HeadlessProcessStream,
69
+ text: string,
70
+ ): Promise<void> {
50
71
  return new Promise((resolve, reject) => {
51
72
  let settled = false;
52
73
 
53
74
  const cleanup = (): void => {
54
- process.stdout.off("error", handleError);
75
+ stream.off("error", handleError);
55
76
  };
56
77
 
57
78
  const settle = (callback: () => void): void => {
@@ -69,9 +90,9 @@ function defaultWrite(text: string): Promise<void> {
69
90
  });
70
91
  };
71
92
 
72
- process.stdout.on("error", handleError);
93
+ stream.on("error", handleError);
73
94
  try {
74
- process.stdout.write(text, () => {
95
+ stream.write(text, () => {
75
96
  settle(resolve);
76
97
  });
77
98
  } catch (error) {
@@ -142,6 +163,17 @@ function extractAssistantText(message: AssistantMessage | null): string {
142
163
  .join("");
143
164
  }
144
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
+
145
177
  function shouldWriteHeadlessJsonEvent(event: AgentEvent): boolean {
146
178
  switch (event.type) {
147
179
  case "user_message":
@@ -158,12 +190,17 @@ function shouldWriteHeadlessJsonEvent(event: AgentEvent): boolean {
158
190
 
159
191
  function createHeadlessOutputController(
160
192
  state: AppState,
193
+ stream: HeadlessProcessStream,
161
194
  writeImpl: (text: string) => void | Promise<void>,
195
+ options?: {
196
+ attachSigint?: boolean;
197
+ },
162
198
  ): HeadlessOutputController {
163
199
  let brokenPipe = false;
164
200
  let outputError: unknown = null;
165
201
  let pendingWrite = Promise.resolve();
166
202
  const sigintHandler = createSigintHandler(state);
203
+ const attachSigint = options?.attachSigint ?? true;
167
204
 
168
205
  const stopForBrokenPipe = (): void => {
169
206
  if (brokenPipe) {
@@ -183,7 +220,7 @@ function createHeadlessOutputController(
183
220
  state.abortController?.abort();
184
221
  };
185
222
 
186
- const stdoutErrorHandler = (error: unknown): void => {
223
+ const streamErrorHandler = (error: unknown): void => {
187
224
  failOutput(error);
188
225
  };
189
226
 
@@ -206,12 +243,16 @@ function createHeadlessOutputController(
206
243
  });
207
244
  },
208
245
  attach() {
209
- process.stdout.on("error", stdoutErrorHandler);
210
- process.on("SIGINT", sigintHandler);
246
+ stream.on("error", streamErrorHandler);
247
+ if (attachSigint) {
248
+ process.on("SIGINT", sigintHandler);
249
+ }
211
250
  },
212
251
  detach() {
213
- process.stdout.off("error", stdoutErrorHandler);
214
- process.off("SIGINT", sigintHandler);
252
+ stream.off("error", streamErrorHandler);
253
+ if (attachSigint) {
254
+ process.off("SIGINT", sigintHandler);
255
+ }
215
256
  },
216
257
  async finalize(stopReason) {
217
258
  await pendingWrite;
@@ -244,7 +285,8 @@ export async function runHeadlessPrompt(
244
285
  const content = resolveHeadlessContent(state, rawInput);
245
286
  const output = createHeadlessOutputController(
246
287
  state,
247
- options?.writeLine ?? ((line) => defaultWrite(`${line}\n`)),
288
+ process.stdout,
289
+ options?.writeLine ?? ((line) => defaultWrite(process.stdout, `${line}\n`)),
248
290
  );
249
291
  const hooks: SubmitTurnHooks = {
250
292
  onEvent: (event) => {
@@ -270,11 +312,12 @@ export async function runHeadlessPrompt(
270
312
  }
271
313
 
272
314
  /**
273
- * 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.
274
316
  *
275
317
  * The raw input is parsed with the same rules as interactive input. Slash
276
- * commands are rejected in headless mode. Only the final persisted assistant
277
- * 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.
278
321
  *
279
322
  * @param state - Mutable application state for the run.
280
323
  * @param rawInput - Exact raw prompt text supplied by the user.
@@ -287,20 +330,43 @@ export async function runHeadlessPromptText(
287
330
  options?: HeadlessTextRunOptions,
288
331
  ): Promise<HeadlessStopReason> {
289
332
  const content = resolveHeadlessContent(state, rawInput);
290
- 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(
291
339
  state,
292
- options?.writeText ?? defaultWrite,
340
+ process.stderr,
341
+ options?.writeActivity ?? ((text) => defaultWrite(process.stderr, text)),
342
+ { attachSigint: false },
293
343
  );
294
344
  let finalAssistantMessage: AssistantMessage | null = null;
295
345
  const hooks: SubmitTurnHooks = {
296
346
  onEvent: (event) => {
297
- if (event.type === "assistant_message") {
298
- 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;
299
364
  }
300
365
  },
301
366
  };
302
367
 
303
- output.attach();
368
+ finalOutput.attach();
369
+ activityOutput.attach();
304
370
  try {
305
371
  const stopReason = await submitResolvedInput(
306
372
  rawInput,
@@ -310,10 +376,17 @@ export async function runHeadlessPromptText(
310
376
  );
311
377
  const finalText = extractAssistantText(finalAssistantMessage);
312
378
  if (finalText.length > 0) {
313
- output.write(finalText);
379
+ finalOutput.write(finalText);
314
380
  }
315
- return await 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;
316
388
  } finally {
317
- output.detach();
389
+ activityOutput.detach();
390
+ finalOutput.detach();
318
391
  }
319
392
  }