@itsaplan/runner 0.4.0 → 0.5.1

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.
Files changed (3) hide show
  1. package/README.md +10 -6
  2. package/dist/cli.js +202 -6
  3. package/package.json +4 -2
package/README.md CHANGED
@@ -7,8 +7,8 @@ This package runs such agents on your own machine — one, or several at once.
7
7
 
8
8
  - Polls your instance for the agent's queued runs. Runs each one with a coding agent CLI.
9
9
  Reports the result.
10
- - Has presets for Claude Code, Codex, opencode, Antigravity CLI and GitHub Copilot CLI.
11
- Each preset sets the unattended flags and the session resume.
10
+ - Has presets for Claude Code, Codex, opencode, Antigravity CLI, GitHub Copilot CLI,
11
+ Pi, and Oh My Pi (`omp`). Each preset sets the unattended flags and the session resume.
12
12
  - Runs your own `command` instead, if you prefer. The command takes the task on stdin.
13
13
  - Answers the agent's chat. Sends the text and the tool calls while the CLI prints them.
14
14
 
@@ -20,8 +20,9 @@ of kind External. The key appears one time only, at creation.
20
20
  **2. Enable MCP** for the project, in Settings, MCP Server. MCP is off by default. The
21
21
  agent reads the issue and writes its result through it.
22
22
 
23
- **3. Give the coding agent the address of that server.** Claude Code reads `.mcp.json`
24
- from the folder it runs in. For the other four CLIs, see
23
+ **3. Give the coding agent the address of that server.** Claude Code, Oh My Pi, and Pi
24
+ (through `pi-mcp-adapter`) read `.mcp.json` from the folder they run in. For the other
25
+ CLIs, see
25
26
  [Coding agent setup](https://github.com/croffasia/itsaplan/blob/main/docs/runner.md).
26
27
 
27
28
  ```json
@@ -70,6 +71,8 @@ flags for an unattended run, and the session resume. Every preset keeps a chat s
70
71
  | `opencode` | opencode |
71
72
  | `antigravity` | Antigravity CLI |
72
73
  | `copilot` | GitHub Copilot CLI |
74
+ | `pi` | Pi |
75
+ | `omp` | Oh My Pi |
73
76
 
74
77
  Each preset needs the CLI's own MCP config.
75
78
  [Coding agent setup](https://github.com/croffasia/itsaplan/blob/main/docs/runner.md) holds
@@ -188,7 +191,7 @@ npx -y @itsaplan/runner --agent claude
188
191
  The server writes the task text. The text states what occurred, what to do, and to post
189
192
  the result as a comment on the issue. Your own `command` receives the text on **stdin**. A
190
193
  preset gives it to its CLI in the way that CLI accepts it: on stdin for `claude` and
191
- `codex`, as an argument for `antigravity`, `copilot` and `opencode`.
194
+ `codex`, as an argument for `antigravity`, `copilot`, `opencode`, `pi` and `omp`.
192
195
 
193
196
  The agent reads all other data through the MCP server at `$ITSAPLAN_URL/mcp`. The agent
194
197
  sends `$ITSAPLAN_API_KEY` as a bearer token, and acts as its own user with its role.
@@ -235,7 +238,8 @@ calls with the answer. `outputFormat` tells the runner how to read the output:
235
238
 
236
239
  - `text` — all the output is the answer.
237
240
  - `claude-stream-json`, `codex-jsonl`, `opencode-json`, `antigravity-stream-json`,
238
- `copilot-json` — the event stream of that CLI.
241
+ `copilot-json`, `pi-json` — the event stream of that CLI. `pi` and `omp` both use
242
+ `pi-json`.
239
243
 
240
244
  A preset sets `outputFormat` for you. Set it yourself only with your own `command`. If the
241
245
  output does not agree with the format, the runner sends the output as plain text.
package/dist/cli.js CHANGED
@@ -25,11 +25,13 @@ class AnswerStream {
25
25
  sawPartialText = false;
26
26
  sawAnyText = false;
27
27
  sending = Promise.resolve();
28
+ usage;
28
29
  constructor(format, threadId, runId, send) {
29
30
  this.format = format;
30
31
  this.threadId = threadId;
31
32
  this.runId = runId;
32
33
  this.send = send;
34
+ this.usage = new UsageReader(format);
33
35
  this.messageId = `msg-${runId}`;
34
36
  this.queued.push({ type: "RUN_STARTED", threadId, runId });
35
37
  }
@@ -48,6 +50,9 @@ class AnswerStream {
48
50
  startedSession() {
49
51
  return this.sessionId;
50
52
  }
53
+ contextUsage() {
54
+ return this.usage.value();
55
+ }
51
56
  async flush() {
52
57
  const sending = this.sending.catch(() => {}).then(() => this.sendQueued());
53
58
  this.sending = sending.catch(() => {});
@@ -127,6 +132,7 @@ class AnswerStream {
127
132
  }
128
133
  if (!parsed || typeof parsed !== "object")
129
134
  return;
135
+ this.usage.readLine(parsed);
130
136
  switch (this.format) {
131
137
  case "claude-stream-json":
132
138
  this.readClaudeLine(parsed);
@@ -143,6 +149,43 @@ class AnswerStream {
143
149
  case "copilot-json":
144
150
  this.readCopilotLine(parsed);
145
151
  break;
152
+ case "pi-json":
153
+ this.readPiLine(parsed);
154
+ break;
155
+ }
156
+ }
157
+ readPiLine(message) {
158
+ if (message.type === "session" && message.id)
159
+ this.sessionId ??= message.id;
160
+ switch (message.type) {
161
+ case "message_update": {
162
+ const event = message.assistantMessageEvent;
163
+ if (event?.type === "text_delta" && event.delta) {
164
+ this.sawPartialText = true;
165
+ this.appendText(event.delta);
166
+ }
167
+ return;
168
+ }
169
+ case "tool_execution_start":
170
+ if (message.toolCallId && !this.openToolCalls.has(message.toolCallId)) {
171
+ this.openToolCalls.add(message.toolCallId);
172
+ this.pushToolCall(message.toolCallId, message.toolName ?? "tool", JSON.stringify(message.args ?? {}));
173
+ }
174
+ return;
175
+ case "tool_execution_end": {
176
+ if (!message.toolCallId || this.closedToolCalls.has(message.toolCallId))
177
+ return;
178
+ this.closedToolCalls.add(message.toolCallId);
179
+ this.pushToolResult(message.toolCallId, piResultText(message.result));
180
+ return;
181
+ }
182
+ case "message_end":
183
+ if (message.message?.role === "assistant" && !this.sawAnyText && message.message.content) {
184
+ const body = textOfResult(message.message.content);
185
+ if (body)
186
+ this.appendText(body);
187
+ }
188
+ return;
146
189
  }
147
190
  }
148
191
  readCopilotLine(message) {
@@ -275,7 +318,9 @@ class AnswerStream {
275
318
  }
276
319
  }
277
320
  readPartial(event) {
278
- if (event?.type !== "content_block_delta")
321
+ if (!event)
322
+ return;
323
+ if (event.type !== "content_block_delta")
279
324
  return;
280
325
  if (event.delta?.type === "text_delta" && event.delta.text) {
281
326
  this.sawPartialText = true;
@@ -313,6 +358,120 @@ class AnswerStream {
313
358
  });
314
359
  }
315
360
  }
361
+
362
+ class UsageReader {
363
+ format;
364
+ last = null;
365
+ buffered = "";
366
+ constructor(format) {
367
+ this.format = format;
368
+ }
369
+ write(chunk) {
370
+ if (this.format === "text")
371
+ return;
372
+ this.buffered += chunk;
373
+ const lines = this.buffered.split(`
374
+ `);
375
+ this.buffered = lines.pop() ?? "";
376
+ for (const line of lines)
377
+ this.readText(line);
378
+ }
379
+ end() {
380
+ if (!this.buffered)
381
+ return;
382
+ this.readText(this.buffered);
383
+ this.buffered = "";
384
+ }
385
+ value() {
386
+ if (this.format === "copilot-json")
387
+ return null;
388
+ return this.last ?? undefined;
389
+ }
390
+ readText(line) {
391
+ const trimmed = line.trim();
392
+ if (!trimmed)
393
+ return;
394
+ try {
395
+ const parsed = JSON.parse(trimmed);
396
+ if (parsed && typeof parsed === "object")
397
+ this.readLine(parsed);
398
+ } catch {}
399
+ }
400
+ readLine(parsed) {
401
+ switch (this.format) {
402
+ case "claude-stream-json":
403
+ this.readClaude(parsed);
404
+ return;
405
+ case "codex-jsonl":
406
+ this.readCodex(parsed);
407
+ return;
408
+ case "opencode-json":
409
+ this.readOpencode(parsed);
410
+ return;
411
+ case "antigravity-stream-json":
412
+ this.readAntigravity(parsed);
413
+ return;
414
+ case "pi-json":
415
+ this.readPi(parsed);
416
+ return;
417
+ }
418
+ }
419
+ readClaude(message) {
420
+ const event = message.type === "stream_event" ? message.event : undefined;
421
+ if (!event)
422
+ return;
423
+ const usage = event.type === "message_start" ? event.message?.usage : event.usage;
424
+ if (!usage)
425
+ return;
426
+ const read = (usage.input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0);
427
+ const written = usage.output_tokens ?? 0;
428
+ if (event.type === "message_start") {
429
+ this.last = { inputTokens: read, outputTokens: written };
430
+ return;
431
+ }
432
+ if (event.type !== "message_delta")
433
+ return;
434
+ this.last = { inputTokens: read || (this.last?.inputTokens ?? 0), outputTokens: written };
435
+ }
436
+ readCodex(message) {
437
+ if (message.type !== "turn.completed" || !message.usage)
438
+ return;
439
+ this.last = {
440
+ inputTokens: message.usage.input_tokens ?? 0,
441
+ outputTokens: message.usage.output_tokens ?? 0
442
+ };
443
+ }
444
+ readOpencode(message) {
445
+ const tokens = message.part?.type === "step-finish" ? message.part.tokens : undefined;
446
+ if (!tokens)
447
+ return;
448
+ this.last = {
449
+ inputTokens: (tokens.input ?? 0) + (tokens.cache?.read ?? 0) + (tokens.cache?.write ?? 0),
450
+ outputTokens: (tokens.output ?? 0) + (tokens.reasoning ?? 0)
451
+ };
452
+ }
453
+ readAntigravity(message) {
454
+ const step = message.step_update;
455
+ if (!step)
456
+ return;
457
+ const usage = step.usage ?? message.usage;
458
+ if (!usage)
459
+ return;
460
+ this.last = {
461
+ inputTokens: (usage.input_tokens ?? 0) + (usage.cache_read_tokens ?? 0),
462
+ outputTokens: (usage.output_tokens ?? 0) + (usage.thinking_tokens ?? 0)
463
+ };
464
+ }
465
+ readPi(message) {
466
+ const usage = message.usage;
467
+ if (!usage)
468
+ return;
469
+ this.last = {
470
+ inputTokens: (usage.input ?? usage.input_tokens ?? 0) + (usage.cacheRead ?? 0),
471
+ outputTokens: usage.output ?? usage.output_tokens ?? 0
472
+ };
473
+ }
474
+ }
316
475
  function tail(text, limit) {
317
476
  return text.length <= limit ? text : `…${text.slice(-limit)}`;
318
477
  }
@@ -324,6 +483,13 @@ function textOfResult(content) {
324
483
  }
325
484
  return "";
326
485
  }
486
+ function piResultText(result) {
487
+ if (result && typeof result === "object" && !Array.isArray(result) && "content" in result) {
488
+ return textOfResult(result.content);
489
+ }
490
+ const text = textOfResult(result);
491
+ return text || (result == null ? "" : JSON.stringify(result));
492
+ }
327
493
 
328
494
  // src/execute.ts
329
495
  import { spawn } from "node:child_process";
@@ -400,6 +566,33 @@ var PRESETS = {
400
566
  ...sessionId ? ["--session-id", sessionId] : []
401
567
  ],
402
568
  tail: ["-p"]
569
+ },
570
+ pi: {
571
+ bin: "pi",
572
+ outputFormat: "pi-json",
573
+ promptVia: "arg",
574
+ systemPromptFlag: "--append-system-prompt",
575
+ head: (sessionId) => [
576
+ "-p",
577
+ "--mode",
578
+ "json",
579
+ ...sessionId ? ["--session-id", sessionId] : []
580
+ ],
581
+ tail: []
582
+ },
583
+ omp: {
584
+ bin: "omp",
585
+ outputFormat: "pi-json",
586
+ promptVia: "arg",
587
+ systemPromptFlag: "--append-system-prompt",
588
+ head: (sessionId) => [
589
+ "-p",
590
+ "--mode",
591
+ "json",
592
+ "--auto-approve",
593
+ ...sessionId ? ["--resume", sessionId] : []
594
+ ],
595
+ tail: []
403
596
  }
404
597
  };
405
598
  var PRESET_NAMES = Object.keys(PRESETS);
@@ -430,7 +623,8 @@ var OUTPUT_FORMATS = [
430
623
  "codex-jsonl",
431
624
  "opencode-json",
432
625
  "antigravity-stream-json",
433
- "copilot-json"
626
+ "copilot-json",
627
+ "pi-json"
434
628
  ];
435
629
  function presetOf(config) {
436
630
  if (config.command || !config.agent)
@@ -705,12 +899,12 @@ async function answer(config, client, message, stop) {
705
899
  return;
706
900
  if (outcome.status === "success") {
707
901
  await stream.finish(outcome.output);
708
- await client.chatResult(message.id, { status: "success" });
902
+ await client.chatResult(message.id, { status: "success", usage: stream.contextUsage() });
709
903
  return;
710
904
  }
711
905
  const error = outcome.error ?? "The command failed";
712
906
  await stream.fail(error, outcome.output);
713
- await client.chatResult(message.id, { status: "failed", error });
907
+ await client.chatResult(message.id, { status: "failed", error, usage: stream.contextUsage() });
714
908
  }
715
909
 
716
910
  // src/client.ts
@@ -816,8 +1010,10 @@ async function handle(config, client, log2, run) {
816
1010
  const label = run.issueIdentifier ?? `run ${run.id}`;
817
1011
  log2(`${label}: started (${run.trigger})`);
818
1012
  try {
819
- const outcome = await withHeartbeat(log2, () => client.heartbeat(run.id), execute(config, taskOf(run)));
820
- await client.report(run.id, outcome);
1013
+ const usage = new UsageReader(config.outputFormat);
1014
+ const outcome = await withHeartbeat(log2, () => client.heartbeat(run.id), execute(config, taskOf(run), { onData: (chunk) => usage.write(chunk) }));
1015
+ usage.end();
1016
+ await client.report(run.id, { ...outcome, usage: usage.value() });
821
1017
  log2(`${label}: ${outcome.status}${outcome.error ? ` — ${outcome.error}` : ""}`);
822
1018
  } catch (err) {
823
1019
  const message = err instanceof Error ? err.message : String(err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itsaplan/runner",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Runs an Itsaplan external agent's queued tasks and answers its chat on your own machine",
5
5
  "keywords": [
6
6
  "itsaplan",
@@ -18,7 +18,9 @@
18
18
  "claude-code",
19
19
  "codex",
20
20
  "antigravity",
21
- "opencode"
21
+ "opencode",
22
+ "pi",
23
+ "omp"
22
24
  ],
23
25
  "homepage": "https://github.com/croffasia/itsaplan/tree/main/packages/runner#readme",
24
26
  "repository": {