s-app-scoder-agent 0.4.0 → 0.6.0

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 CHANGED
@@ -50,14 +50,17 @@ npx app-scoder-agent "your task"
50
50
  ## Quick Start
51
51
 
52
52
  ```bash
53
- # Set your API key
53
+ # One-time setup wizard — writes .scoderrc.json (base URL, key, model, build cmd)
54
+ scoder init
55
+
56
+ # …or set your API key manually
54
57
  export SCODER_API_KEY=sk-... # any OpenAI-compatible key (OpenAI, Groq, OpenRouter, Ollama, …)
55
58
  export OPENAI_BASE_URL=https://api.openai.com/v1 # optional, for custom endpoints
56
59
 
57
60
  # Run a task
58
61
  scoder "add a ping function to src/network.ts"
59
62
 
60
- # Interactive REPL
63
+ # Interactive REPL (supports /help, /clear, /cost, /model)
61
64
  scoder
62
65
  ```
63
66
 
@@ -80,9 +83,22 @@ You (task) → LLM plans → Tools execute code → Build runs → Success? →
80
83
  | `edit_file` | Find-and-replace (unique match required) |
81
84
  | `list_files` | Recursive directory listing |
82
85
  | `run_command` | Execute any shell command (subject to permission policy) |
86
+ | `web_fetch` | Fetch a public HTTP(S) URL → readable text (HTML→text, JSON pretty-printed) |
83
87
 
84
88
  File writes (`write_file` / `edit_file`) are symlink-safe: paths are resolved via `realpath` and cannot escape the working directory, even through a malicious symlink. `read_file` refuses files larger than 2MB with guidance to use `grep`/`head`/`tail`.
85
89
 
90
+ ### Web access (`web_fetch`)
91
+
92
+ The agent can read the internet through the built-in `web_fetch` tool — it fetches exact URLs (it cannot search) and returns readable plain text: HTML is converted to text (scripts/styles stripped, entities decoded, title extracted), JSON is pretty-printed, binary content is refused. The system prompt instructs the agent *when to fetch* (external/API docs, URLs in error messages, current versions, changelogs) and when not to (answer available locally).
93
+
94
+ Safety defaults:
95
+
96
+ - **SSRF protection** — `localhost`, private and link-local addresses (10.x, 172.16/12, 192.168.x, 127.x, 169.254.x, IPv6 ULA/link-local) are blocked. Set `SCODER_WEB_ALLOW_PRIVATE=1` to allow local dev servers.
97
+ - Only `http`/`https`; up to 5 redirects — every hop is re-validated against the private-address block list.
98
+ - 30 s timeout, SIGINT-abortable; body capped at 512 KB; extracted text capped at 20k chars (configurable via `max_chars`).
99
+ - HTML is converted to plain text (no JS execution); images/archives/PDFs are refused.
100
+ - Read-only — the tool never writes locally, so it is allowed even in `readonly` permission mode.
101
+
86
102
  ## Configuration
87
103
 
88
104
  ### Environment variables
@@ -96,6 +112,9 @@ File writes (`write_file` / `edit_file`) are symlink-safe: paths are resolved vi
96
112
  | `SCODER_MAX_STEPS` | `40` | Max loop iterations before giving up |
97
113
  | `SCODER_PERMISSION` | `auto` | Permission mode: `auto` \| `ask` \| `readonly` |
98
114
  | `SCODER_STREAM` | `1` | `0` to disable streaming LLM output |
115
+ | `SCODER_STREAM` | `1` | `0` to disable streaming LLM output |
116
+ | `SCODER_SERVER_TOKEN` | — | `scoder serve` only: require `Authorization: Bearer <token>` |
117
+ | `SCODER_WEB_ALLOW_PRIVATE` | — | Set to `1` to let `web_fetch` reach localhost/private addresses (local dev) |
99
118
  | `SCODER_NO_APIKEY_WARNING` | — | Set to `1` to suppress the apiKey-in-rc warning |
100
119
 
101
120
  ### Per-project config file (.scoderrc.json)
@@ -225,7 +244,61 @@ Sessions older than 7 days are pruned automatically on the next run. Use `--no-s
225
244
 
226
245
  ## Streaming
227
246
 
228
- LLM output is streamed token-by-token by default (`SCODER_STREAM=0` to disable). If a provider doesn't support streaming, scoder automatically falls back to non-streaming requests.
247
+ scoder streams everything it can, live, as it happens:
248
+
249
+ - **LLM tokens** — assistant text renders character-by-character (OpenAI-compatible SSE and native Anthropic streaming). If a provider doesn't support streaming, scoder automatically falls back to non-streaming requests. Disable with `SCODER_STREAM=0`.
250
+ - **Tool output** — `run_command` and build output print live (dimmed block) while the command runs, instead of appearing only after it finishes. Live output is capped at 20k chars; the full output still goes to the LLM.
251
+ - **SIGINT-safe** — Ctrl+C kills the running command immediately; streamed rendering never blocks the event loop.
252
+ - Output honors `NO_COLOR` and degrades to plain text when piped (CI-friendly).
253
+
254
+ ## Full-Screen TUI (`scoder tui`)
255
+
256
+ **Mission Control for the build loop** — a terminal UI built with [Ink](https://github.com/vadimdemedes/ink) (React for terminals), driven by the same `AgentEvent` stream the HTTP API exposes:
257
+
258
+ ```bash
259
+ scoder tui # start an interactive full-screen session
260
+ scoder tui "fix the bug" # auto-submit an initial task
261
+ ```
262
+
263
+ Features:
264
+
265
+ - **Build Rail** (top bar) — brand, build lamp (`● build ok` / `● build fail` / `no build yet`), permission mode, and a step fuel gauge (`▰▰▰▱▱▱▱▱▱▱`) that fills as the agent burns through its step budget
266
+ - **Chat pane** — your messages, streaming assistant text, and every tool call with live stdout/stderr preview (`✔/✘` status, spinner while running), prefixed with a flight-recorder step number (`03▸`)
267
+ - **Diff cards** (`✎`) — every file write/edit renders as a unified diff (`+` green / `-` red / `@@` cyan hunk headers); the same diff also flows out of the HTTP SSE stream
268
+ - **Status bar** — model, token usage (input/output/cached), keybind hints
269
+ - **Slash commands** — `/help`, `/clear`, `/cost`, `/model`, `/exit` (identical to the REPL)
270
+ - **Multi-turn** — keep typing follow-ups after a task finishes; the conversation context carries over (same as the REPL)
271
+ - **Scrollback** — `PgUp`/`PgDn` page through history; `Esc` returns to the live tail (press again to abort)
272
+ - **Keybinds** — `Enter` send · `PgUp`/`PgDn` scroll · `Esc` reset scroll / abort · `Ctrl+C` quit
273
+ - Non-TTY environments (CI, pipes) automatically fall back to the classic REPL
274
+ - `permission: "ask"` is not supported in the TUI (no readline prompts on the alt screen) — it falls back to `auto`
275
+
276
+ ## HTTP Server Mode (`scoder serve`)
277
+
278
+ Run scoder as a local HTTP service — submit tasks programmatically and consume progress as Server-Sent Events:
279
+
280
+ ```bash
281
+ scoder serve # http://127.0.0.1:0 (random port, printed at startup)
282
+ scoder serve --port 4114 # fixed port
283
+ SCODER_SERVER_TOKEN=secret scoder serve --host 127.0.0.1
284
+ ```
285
+
286
+ | Endpoint | Method | Description |
287
+ |----------|--------|-------------|
288
+ | `/api/health` | GET | Liveness: `{ ok, running, runs }` |
289
+ | `/api/runs` | POST | Start a run: `{ "task": "...", "workdir?", "buildCmd?", "model?", "provider?", "maxSteps?", "permission?" }` → `202 { id, eventsUrl }` |
290
+ | `/api/runs` | GET | List runs with status/timing |
291
+ | `/api/runs/<id>` | GET | Run status + final result |
292
+ | `/api/runs/<id>/events` | GET | **SSE stream** — full event replay, then live, closes on finish |
293
+ | `/api/runs/<id>/abort` | POST | Abort a running run (equivalent to SIGINT) |
294
+
295
+ SSE event types: `run_start`, `step`, `llm_delta` (assistant text), `tool_call`, `tool_output` (live stdout/stderr chunks), `tool_result`, `llm_usage`, `build`, `finish`. The stream replays every event from the beginning, then stays live, and closes after `finish` — safe to (re)connect at any time.
296
+
297
+ Notes:
298
+
299
+ - Runs are always non-interactive; `permission: "ask"` is rejected — use `auto` or `readonly`.
300
+ - Binds `127.0.0.1` by default. Set `SCODER_SERVER_TOKEN` to require `Authorization: Bearer <token>` on every request.
301
+ - Max 4 concurrent runs (extra requests get `429`); per-run overrides: `workdir`, `buildCmd`, `model`, `provider`, `maxSteps`, `permission`.
229
302
 
230
303
  ## Logging
231
304
 
@@ -268,6 +341,9 @@ scoder detects the build command automatically:
268
341
  ```
269
342
  scoder [options] "<task>" Run a task autonomously until the build passes
270
343
  scoder Start interactive session
344
+ scoder init Setup wizard — writes .scoderrc.json (+ connection test)
345
+ scoder tui [task] Full-screen terminal UI (Mission Control)
346
+ scoder serve [--port] [--host] HTTP server mode (see above)
271
347
  scoder --help | --version Help and version
272
348
  ```
273
349
 
@@ -283,6 +359,9 @@ scoder --help | --version Help and version
283
359
  | `-y` | Non-interactive mode (denies commands needing confirmation) |
284
360
  | `--provider <name>` | `openai` \| `anthropic` (env SCODER_PROVIDER; auto-detected) |
285
361
  | `--no-save` | Disable session auto-save to `.scoder/sessions/` |
362
+ | `--log-file` | Write a structured JSONL event log to `.scoder/logs/` |
363
+ | `--port <n>` | (`serve`) TCP port, `0` = random free port |
364
+ | `--host <addr>` | (`serve`) Bind address (default `127.0.0.1`) |
286
365
 
287
366
  ## Examples
288
367
 
@@ -305,6 +384,15 @@ scoder -m claude-sonnet-4-5 "refactor the parser module"
305
384
 
306
385
  # CI: non-interactive, no session saving
307
386
  scoder --no-save -y "run the full migration"
387
+
388
+ # Run as an HTTP service and consume progress via SSE
389
+ scoder serve --port 4114 &
390
+ curl -s -X POST localhost:4114/api/runs -H 'Content-Type: application/json' \
391
+ -d '{"task": "add input validation to the CLI"}'
392
+ curl -N http://127.0.0.1:4114/api/runs/<id>/events # live progress
393
+
394
+ # Fetch external docs during a task (agent decides when, via web_fetch)
395
+ scoder "upgrade express to v5 — check the official migration guide first"
308
396
  ```
309
397
 
310
398
  ## Architecture
@@ -312,32 +400,40 @@ scoder --no-save -y "run the full migration"
312
400
  ```
313
401
  ┌─────────────────────────────────────────────┐
314
402
  │ CLI (src/index.ts) │
315
- │ arg parsing, REPL, SIGINT handling │
316
- └─────────────────┬───────────────────────────┘
317
- │
318
- ┌─────────────────▼───────────────────────────┐
319
- │ Agent (src/agent.ts) │
320
- │ Closed-loop orchestrator │
321
- │ history trimming, error injection │
322
- └───────┬───────────────────────┬─────────────┘
323
- │ │
324
- ┌───────▼───────┐ ┌─────────▼──────────┐
325
- │ LLM Client │ │ Tool Registry │
326
- │ (src/llm.ts) │ │ (src/tools.ts) │
327
- │ OpenAI compat │ │ read/write/run/list │
328
- └───────────────┘ └──────────────────────┘
329
- │
330
- ▼
331
- ┌─────────────────────────────────────────────┐
332
- │ Build Checker (src/build.ts) │
333
- │ run build → exit 0 = stop condition met │
334
- └─────────────────────────────────────────────┘
403
+ │ arg parsing, REPL, serve, tui, SIGINT │
404
+ └───────┬─────────────────────┬───────────────┘
405
+ │ │
406
+ ┌───────▼───────────┐ ┌──────▼─────────────────────┐
407
+ │ TUI (src/tui/) │ │ Agent (src/agent.ts) │
408
+ │ Ink/React full- │ │ Closed-loop orchestrator │
409
+ │ screen chat UI, │─▶│ history trimming, error │
410
+ │ renders the same │ │ injection │
411
+ │ AgentEvent stream│ │ emits AgentEvent │
412
+ └───────────────────┘ └──┬──────────┬──────────────┘
413
+ │ │
414
+ │ └────────────────────┐
415
+ ▼ ▼
416
+ ┌───────────────────────────────┐ ┌──────────────────────────┐
417
+ │ LLM Client (src/llm.ts) │ │ Server (src/server.ts) │
418
+ │ OpenAI-compatible + native │ │ HTTP API + SSE │
419
+ │ Anthropic provider │ │ runs in background │
420
+ └──────────────┬────────────────┘ └──────────────────────────┘
421
+ ▼
422
+ ┌────────────────────────────────────────┐
423
+ │ Tool Registry (src/tools.ts) │
424
+ │ + web_fetch (src/webfetch.ts) │
425
+ └──────────────┬─────────────────────────┘
426
+ ▼
427
+ ┌───────────────────────────────┐
428
+ │ Build Checker (src/build.ts) │
429
+ │ run build → exit 0 = stop │
430
+ └───────────────────────────────┘
335
431
  ```
336
432
 
337
433
  ## Integration Testing
338
434
 
339
435
  ```bash
340
- npm test # runs: lint → typecheck → build → 116 unit tests + e2e + MCP smoke
436
+ npm test # runs: build → unit tests (node --test) → e2e closed-loop → MCP smoke
341
437
  npm run lint # eslint flat config (typescript-eslint + eqeqeq, prefer-const...)
342
438
  npm run typecheck # tsc --noEmit
343
439
  ```
@@ -345,17 +441,20 @@ npm run typecheck # tsc --noEmit
345
441
  Test suites (run with `npm test` — auto-discovers `test/*.test.mjs`):
346
442
  - `test/permissions.test.mjs` — catastrophic patterns, mode classification, allowlist word-boundary, timeout-denies
347
443
  - `test/permission-prompt.test.mjs` — interactive prompt timeout behavior
348
- - `test/tools.test.mjs` — safePath traversal, symlink escape, file-size cap, write/edit/list
444
+ - `test/tools.test.mjs` — safePath traversal, symlink escape, file-size cap, write/edit/list, live `run_command` output streaming
445
+ - `test/webfetch.test.mjs` — HTML→text, JSON, redirects, SSRF guard, binary refusal, truncation
446
+ - `test/server.test.mjs` — HTTP API: run lifecycle via SSE, abort, auth, live `tool_output` events
447
+ - `test/tui.test.mjs` — full-screen TUI in a real PTY (via `test/pty-run.py`): closed loop render + slash commands (`/help`, `/cost`) + clean exit
448
+ - `test/tui-state.test.mjs` — event reducer unit tests: diff items, build lamp, abort marking, slash-command parsing & help formatting
349
449
  - `test/core.test.mjs` — unified diff, retry backoff, context trimming
350
450
  - `test/config.test.mjs` — env/rc/cli precedence, logFile mapping, permission lists
351
451
  - `test/session.test.mjs` — session store, smart prune, resolve
452
+ - `test/cost.test.mjs` — token usage & cost aggregation
352
453
  - `test/anthropic.test.mjs` — message translation + native SSE streaming
353
- - `test/llm.test.mjs` — OpenAI-compatible chat + SSE streaming + redaction
454
+ - `test/llm.test.mjs` — OpenAI-compatible chat + SSE streaming + redaction + wire-format normalization
354
455
  - `test/run-e2e.mjs` — closed-loop integration (mock LLM server)
355
456
  - `test/run-mcp-test.mjs` — MCP smoke (mock stdio server)
356
457
 
357
- Coverage (lines): **90.3%** overall — see `npm run coverage`.
358
-
359
458
  ## Exit Codes
360
459
 
361
460
  | Code | Meaning |
package/dist/agent.js CHANGED
@@ -28,7 +28,13 @@ RULES:
28
28
  6. Never invent requirements beyond the given task.
29
29
  7. IMPORTANT: if a run_command is denied by permission policy, you will receive
30
30
  an error message. Use this as a signal to find a safer alternative or to
31
- communicate the limitation to the user.`;
31
+ communicate the limitation to the user.
32
+ 8. You have web access ONLY through web_fetch (it fetches exact URLs, it cannot
33
+ search). Use it when the task depends on external or fast-changing knowledge:
34
+ library/API documentation, a URL found in an error message or code comment,
35
+ current package versions, changelogs, release notes, standards. Skip it when
36
+ the answer is in the working directory or is stable general knowledge. After
37
+ fetching, ground your edits in what the page actually says.`;
32
38
  export class Agent {
33
39
  config;
34
40
  client;
@@ -48,6 +54,10 @@ export class Agent {
48
54
  mcp = null;
49
55
  /** Aborted when abort() is called — cancels in-flight LLM requests (SIGINT). */
50
56
  runSignal = new AbortController();
57
+ /** Optional structured-event sink (used by the HTTP/SSE server frontend). */
58
+ onEvent = null;
59
+ /** Set while a tool call is executing, for tool_output event correlation. */
60
+ currentToolCall = null;
51
61
  constructor(config) {
52
62
  this.config = config;
53
63
  const commonOpts = {
@@ -72,6 +82,7 @@ export class Agent {
72
82
  workdir: config.workdir,
73
83
  permissions: this.permissions,
74
84
  permissionMode: config.permissionMode,
85
+ onToolOutput: this.handleToolOutput,
75
86
  };
76
87
  this.session = new SessionStore(config.workdir, config.noSave !== true);
77
88
  this.logger = JsonlLogger.forSession(config.workdir, this.session.id, config.logFile);
@@ -92,6 +103,27 @@ export class Agent {
92
103
  // terminate its child process immediately (long builds must be interruptible).
93
104
  this.ctx.signal = this.runSignal.signal;
94
105
  }
106
+ /** Emit one structured run event to the sink (if attached). */
107
+ emit(ev) {
108
+ this.onEvent?.(ev);
109
+ }
110
+ /**
111
+ * Live tool-output handler: renders chunks in the terminal (dimmed block) and
112
+ * forwards them as `tool_output` events. Opens the stream block lazily on the
113
+ * first chunk; `endToolStream()` closes it after the tool completes.
114
+ */
115
+ handleToolOutput = (name, stream, delta) => {
116
+ if (!ui.isToolStreamOpen()) {
117
+ ui.toolStreamStart();
118
+ }
119
+ ui.toolStreamChunk(delta);
120
+ this.emit({ type: "tool_output", name, stream, delta });
121
+ };
122
+ /** Close the live tool-output block (if open) before printing the next UI element. */
123
+ endToolStream() {
124
+ if (ui.isToolStreamOpen())
125
+ ui.toolStreamEnd();
126
+ }
95
127
  /** Push one tool-result message back into the conversation. */
96
128
  pushToolResult(tc, content) {
97
129
  this.messages.push({
@@ -108,6 +140,7 @@ export class Agent {
108
140
  */
109
141
  rejectToolCall(step, tc, output, logExtra) {
110
142
  ui.toolResult(tc.name, false, output);
143
+ this.emit({ type: "tool_result", step, name: tc.name, ok: false, output });
111
144
  this.logger.log({ type: "tool", step, tool: tc.name, ok: false, ...logExtra });
112
145
  this.pushToolResult(tc, output);
113
146
  }
@@ -115,6 +148,10 @@ export class Agent {
115
148
  * Record usage of one LLM call: render token counts, log an `llm_usage`
116
149
  * event, and accumulate totals into session state.
117
150
  */
151
+ /** Token usage totals accumulated in this session (for REPL `/cost`). */
152
+ getUsage() {
153
+ return { ...this.usageTotals };
154
+ }
118
155
  recordLlmUsage(usage) {
119
156
  if (!usage)
120
157
  return;
@@ -126,6 +163,10 @@ export class Agent {
126
163
  this.usageTotals.outputTokens += output;
127
164
  this.usageTotals.cachedTokens += cached;
128
165
  ui.info(`tokens: in=${usage.prompt_tokens ?? "?"} out=${usage.completion_tokens ?? "?"}`);
166
+ this.emit({
167
+ type: "llm_usage",
168
+ usage: { input_tokens: input, output_tokens: output, cached_tokens: cached },
169
+ });
129
170
  this.logger.log({
130
171
  type: "llm_usage",
131
172
  model: this.client.model,
@@ -161,6 +202,7 @@ export class Agent {
161
202
  started = true;
162
203
  }
163
204
  ui.llmStreamChunk(chunk.contentDelta);
205
+ this.emit({ type: "llm_delta", delta: chunk.contentDelta });
164
206
  streamedText += chunk.contentDelta;
165
207
  }
166
208
  if (chunk.final)
@@ -217,9 +259,13 @@ export class Agent {
217
259
  throw new Error("No API key configured. Set SCODER_API_KEY or OPENAI_API_KEY (or put apiKey in .scoderrc.json).");
218
260
  }
219
261
  const buildCmd = this.buildCmd;
220
- ui.header("TASK");
221
- ui.info(task);
222
- ui.info(`model=${this.client.model} workdir=${this.config.workdir} build="${buildCmd}" maxSteps=${this.config.maxSteps}`);
262
+ ui.banner("scoder", [
263
+ ["task", task],
264
+ ["model", `${this.client.model} (${this.config.provider}${this.config.stream ? ", stream" : ""})`],
265
+ ["workdir", this.config.workdir],
266
+ ["build", buildCmd],
267
+ ["steps", `max ${this.config.maxSteps} · permission ${this.config.permissionMode}`],
268
+ ]);
223
269
  this.currentTask = task;
224
270
  this.resetRunSignal();
225
271
  this.resetUsageTotals();
@@ -231,6 +277,14 @@ export class Agent {
231
277
  },
232
278
  { role: "user", content: task },
233
279
  ];
280
+ this.emit({
281
+ type: "run_start",
282
+ task,
283
+ model: this.client.model,
284
+ provider: this.config.provider,
285
+ workdir: this.config.workdir,
286
+ buildCmd,
287
+ });
234
288
  try {
235
289
  await this.connectMcp();
236
290
  return await this.runLoop(0, buildCmd, null);
@@ -304,6 +358,7 @@ export class Agent {
304
358
  while (steps < this.config.maxSteps && !this.aborted) {
305
359
  steps++;
306
360
  ui.step(steps, this.config.maxSteps, "thinking...");
361
+ this.emit({ type: "step", step: steps, maxSteps: this.config.maxSteps });
307
362
  const { message, usage } = await this.chatWithStream().catch((err) => {
308
363
  // SIGINT during the LLM call — stop gracefully instead of crashing
309
364
  if (this.aborted) {
@@ -363,6 +418,8 @@ export class Agent {
363
418
  continue;
364
419
  }
365
420
  ui.toolCall(tc.name, args);
421
+ this.currentToolCall = { step: steps, id: tc.id, name: tc.name };
422
+ this.emit({ type: "tool_call", step: steps, id: tc.id, name: tc.name, args });
366
423
  if (!tool && !(this.mcp?.hasTool(tc.name))) {
367
424
  this.rejectToolCall(steps, tc, `ERROR: unknown tool "${tc.name}". Available: ${AGENT_TOOLS.map((t) => t.spec.function.name).join(", ")}${this.mcp ? " + MCP tools (mcp__<server>__<tool>)" : ""}`, { error: "unknown tool" });
368
425
  continue;
@@ -426,7 +483,17 @@ export class Agent {
426
483
  this.aborted = true;
427
484
  ui.warn("edit confirmation aborted by user ([q]uit) — stopping the run");
428
485
  }
486
+ this.endToolStream();
487
+ this.currentToolCall = null;
429
488
  ui.toolResult(tc.name, result.ok, result.output);
489
+ this.emit({
490
+ type: "tool_result",
491
+ step: steps,
492
+ name: tc.name,
493
+ ok: result.ok,
494
+ output: result.output,
495
+ ...(result.diff ? { diff: result.diff } : {}),
496
+ });
430
497
  stepToolResults.set(tc.id, { ok: result.ok, output: result.output });
431
498
  this.pushToolResult(tc, summariseOutput(result.output));
432
499
  }
@@ -483,8 +550,10 @@ export class Agent {
483
550
  }
484
551
  /** Explicitly verify the build (independent of what the LLM claims). */
485
552
  async checkBuild(buildCmd) {
486
- const res = await runBuild(this.config.workdir, buildCmd);
553
+ const res = await runBuild(this.config.workdir, buildCmd, (stream, delta) => this.handleToolOutput("build", stream, delta));
554
+ this.endToolStream();
487
555
  ui.buildStatus(res.success, res.output);
556
+ this.emit({ type: "build", success: res.success, command: buildCmd, output: res.output });
488
557
  this.logger.log({ type: "build", success: res.success, command: buildCmd, output: res.output.slice(0, 10_000) });
489
558
  return res;
490
559
  }
@@ -495,6 +564,7 @@ export class Agent {
495
564
  else {
496
565
  ui.error(`Task NOT complete. ${summary}`);
497
566
  }
567
+ this.emit({ type: "finish", success, steps, summary: summary.slice(0, 2000) });
498
568
  this.logger.log({ type: "finish", success, steps });
499
569
  return { success, steps, buildCmd, summary: summary.slice(0, 2000) };
500
570
  }
package/dist/build.js CHANGED
@@ -30,9 +30,10 @@ export function detectBuildCommand(workdir) {
30
30
  /**
31
31
  * Run the build command. This is the agent's stop condition:
32
32
  * exit code 0 == task complete.
33
+ * `onOutput` (optional) receives stdout/stderr chunks live while the build runs.
33
34
  */
34
- export async function runBuild(workdir, command) {
35
- const res = await toolRunCommand(workdir, { command, timeout_secs: 600 });
35
+ export async function runBuild(workdir, command, onOutput) {
36
+ const res = await toolRunCommand(workdir, { command, timeout_secs: 600 }, undefined, onOutput);
36
37
  return {
37
38
  success: res.ok,
38
39
  command,