@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.
- package/README.md +10 -6
- package/dist/cli.js +202 -6
- 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
|
|
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
|
|
24
|
-
from the folder
|
|
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 `
|
|
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
|
|
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
|
|
820
|
-
await client.
|
|
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.
|
|
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": {
|