pi-harness-delegate 0.1.0 → 0.2.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 +8 -6
- package/extensions/activity.ts +225 -201
- package/extensions/command.ts +61 -61
- package/extensions/config.ts +137 -131
- package/extensions/harnesses/amp.ts +122 -115
- package/extensions/harnesses/claude.ts +136 -111
- package/extensions/harnesses/codex.ts +191 -147
- package/extensions/harnesses/opencode.ts +133 -116
- package/extensions/harnesses/registry.ts +22 -22
- package/extensions/harnesses/types.ts +60 -60
- package/extensions/hint.ts +22 -19
- package/extensions/index.ts +1128 -743
- package/extensions/progress.ts +104 -104
- package/extensions/run-claude.ts +44 -35
- package/extensions/runner.ts +111 -99
- package/extensions/stream-parse.ts +32 -24
- package/extensions/templates.ts +134 -117
- package/extensions/usage.ts +24 -24
- package/package.json +69 -56
package/extensions/progress.ts
CHANGED
|
@@ -8,135 +8,135 @@
|
|
|
8
8
|
* re-show with /delegate watch)
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { Key, matchesKey, truncateToWidth, visibleWidth, type Component, type TUI } from '@earendil-works/pi-tui';
|
|
12
11
|
import type { Theme } from '@earendil-works/pi-coding-agent';
|
|
12
|
+
import { type Component, Key, matchesKey, type TUI, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
|
|
13
13
|
|
|
14
14
|
const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
15
15
|
const SPIN_INTERVAL_MS = 100;
|
|
16
16
|
const MAX_VISIBLE_ENTRIES = 12;
|
|
17
17
|
|
|
18
18
|
export type FeedEntry =
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
| { kind: 'tool'; text: string; ok?: boolean }
|
|
20
|
+
| { kind: 'thinking'; text: string }
|
|
21
|
+
| { kind: 'text'; text: string };
|
|
22
22
|
|
|
23
23
|
export interface ProgressWindowOptions {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
24
|
+
/** Harness name shown in title. */
|
|
25
|
+
harness?: string;
|
|
26
|
+
/** Mode name, shown in the title bar (e.g. "review"). */
|
|
27
|
+
mode: string;
|
|
28
|
+
/** Model (or alias) shown in the title bar. */
|
|
29
|
+
model?: string | null;
|
|
30
|
+
/** Epoch ms when the run started — drives the live elapsed timer. */
|
|
31
|
+
startedAt: number;
|
|
32
|
+
/** Live feed entries (tool calls, thinking, text tail). */
|
|
33
|
+
getEntries: () => FeedEntry[];
|
|
34
|
+
/** Show an "unrestricted permissions" warning banner. */
|
|
35
|
+
dangerous?: boolean;
|
|
36
|
+
/** Called when the user confirms cancel. */
|
|
37
|
+
onCancel: () => void;
|
|
38
|
+
/** Called when the user presses `m` (minimize — keep the run going). */
|
|
39
|
+
onMinimize: () => void;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
export function fmtElapsed(ms: number): string {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
const total = Math.max(0, Math.floor(ms / 1000));
|
|
44
|
+
const m = Math.floor(total / 60);
|
|
45
|
+
const s = total % 60;
|
|
46
|
+
return m > 0 ? `${m}:${String(s).padStart(2, '0')}` : `0:${String(s).padStart(2, '0')}`;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/** Style one feed entry; the returned string may contain ANSI colors. */
|
|
50
50
|
export function renderEntry(entry: FeedEntry, theme: Theme): string {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
51
|
+
switch (entry.kind) {
|
|
52
|
+
case 'tool': {
|
|
53
|
+
const mark = entry.ok === undefined ? '' : entry.ok ? theme.fg('success', ' ✓') : theme.fg('error', ' ✗');
|
|
54
|
+
return theme.fg('accent', '▶ ') + theme.fg('muted', entry.text) + mark;
|
|
55
|
+
}
|
|
56
|
+
case 'thinking':
|
|
57
|
+
return theme.fg('dim', entry.text);
|
|
58
|
+
case 'text':
|
|
59
|
+
return theme.fg('text', entry.text);
|
|
60
|
+
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/** Create the overlay component; disposes the spinner timer. */
|
|
64
64
|
export function progressWindow(tui: TUI, theme: Theme, opts: ProgressWindowOptions): Component & { dispose(): void } {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
65
|
+
let frame = 0;
|
|
66
|
+
let armed = false;
|
|
67
|
+
let armTimer: ReturnType<typeof setTimeout> | null = null;
|
|
68
|
+
const timer = setInterval(() => {
|
|
69
|
+
frame++;
|
|
70
|
+
tui.requestRender();
|
|
71
|
+
}, SPIN_INTERVAL_MS);
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
73
|
+
const disarm = () => {
|
|
74
|
+
armed = false;
|
|
75
|
+
if (armTimer) {
|
|
76
|
+
clearTimeout(armTimer);
|
|
77
|
+
armTimer = null;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
81
|
+
return {
|
|
82
|
+
render(width: number): string[] {
|
|
83
|
+
const inner = Math.max(10, width - 4);
|
|
84
|
+
const padTo = (s: string, w: number) => `${s}${' '.repeat(Math.max(1, w - visibleWidth(s)))}`;
|
|
85
|
+
const out: string[] = [];
|
|
86
86
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
87
|
+
// title bar: ⠋ <harness> <mode> · <model> · ⏱ elapsed
|
|
88
|
+
const harness = opts.harness ?? 'delegate';
|
|
89
|
+
const title = `${SPINNER[frame % SPINNER.length]} ${harness} ${opts.mode}`;
|
|
90
|
+
const status = [opts.model ?? '', `⏱ ${fmtElapsed(Date.now() - opts.startedAt)}`].filter(Boolean).join(' · ');
|
|
91
|
+
const titleStr = status ? `${title} · ${status}` : title;
|
|
92
|
+
const dash = '─'.repeat(Math.max(1, inner - visibleWidth(titleStr) - 2));
|
|
93
|
+
out.push(theme.fg('accent', `╭─ ${titleStr} ${dash}─╮`));
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
95
|
+
// danger banner
|
|
96
|
+
if (opts.dangerous) {
|
|
97
|
+
const banner = theme.fg('error', '⚠ danger — unrestricted access');
|
|
98
|
+
out.push(`│ ${padTo(banner, inner)} │`);
|
|
99
|
+
}
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
101
|
+
// feed
|
|
102
|
+
for (const entry of opts.getEntries().slice(-MAX_VISIBLE_ENTRIES)) {
|
|
103
|
+
out.push(`│ ${padTo(truncateToWidth(renderEntry(entry, theme), inner), inner)} │`);
|
|
104
|
+
}
|
|
105
105
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
106
|
+
// hint row (double-ESC guard + minimize)
|
|
107
|
+
const hint = armed
|
|
108
|
+
? theme.fg('warning', 'press esc again to cancel') + theme.fg('dim', ' · m minimize')
|
|
109
|
+
: theme.fg('dim', 'esc cancel') + theme.fg('dim', ' · m minimize');
|
|
110
|
+
out.push(`│ ${padTo(hint, inner)} │`);
|
|
111
111
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
112
|
+
out.push(theme.fg('accent', `╰${'─'.repeat(Math.max(1, width - 2))}╯`));
|
|
113
|
+
return out;
|
|
114
|
+
},
|
|
115
|
+
handleInput(data: string): void {
|
|
116
|
+
if (matchesKey(data, Key.escape)) {
|
|
117
|
+
if (armed) {
|
|
118
|
+
disarm();
|
|
119
|
+
opts.onCancel();
|
|
120
|
+
} else {
|
|
121
|
+
armed = true;
|
|
122
|
+
armTimer = setTimeout(() => {
|
|
123
|
+
armed = false;
|
|
124
|
+
armTimer = null;
|
|
125
|
+
tui.requestRender();
|
|
126
|
+
}, 1500);
|
|
127
|
+
tui.requestRender();
|
|
128
|
+
}
|
|
129
|
+
} else if (data === 'm') {
|
|
130
|
+
disarm();
|
|
131
|
+
opts.onMinimize();
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
invalidate(): void {
|
|
135
|
+
// stateless render — nothing to clear
|
|
136
|
+
},
|
|
137
|
+
dispose(): void {
|
|
138
|
+
clearInterval(timer);
|
|
139
|
+
disarm();
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
142
|
}
|
package/extensions/run-claude.ts
CHANGED
|
@@ -1,52 +1,61 @@
|
|
|
1
1
|
/** @deprecated use extensions/runner.ts + harnesses/claude.ts directly */
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
import { claudeHarness } from './harnesses/claude.ts';
|
|
4
|
+
import type { ActivityEvent, StreamedResult } from './harnesses/types.ts';
|
|
4
5
|
import { runHarness } from './runner.ts';
|
|
5
6
|
|
|
6
7
|
export interface RunClaudeOptions {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
8
|
+
prompt: string;
|
|
9
|
+
cwd: string;
|
|
10
|
+
permissionMode: string;
|
|
11
|
+
model?: string;
|
|
12
|
+
maxBudgetUsd?: number;
|
|
13
|
+
addDirs?: string[];
|
|
14
|
+
signal?: AbortSignal;
|
|
15
|
+
timeoutMs?: number;
|
|
16
|
+
resumeSessionId?: string;
|
|
17
|
+
onStream?: (text: string) => void;
|
|
18
|
+
onActivity?: (ev: ActivityEvent) => void;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export interface ClaudeResult extends StreamedResult {
|
|
21
|
-
|
|
22
|
+
streamedText: string;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export const DEFAULT_TIMEOUT_MS = 600_000;
|
|
25
26
|
|
|
26
27
|
function mapPerm(mode: string): 'readonly' | 'edit' | 'danger' {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
if (mode === 'plan') return 'readonly';
|
|
29
|
+
if (mode === 'bypassPermissions') return 'danger';
|
|
30
|
+
return 'edit';
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
export function runClaude(opts: RunClaudeOptions): Promise<ClaudeResult> {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
34
|
+
const perm = mapPerm(opts.permissionMode);
|
|
35
|
+
const nativePerm =
|
|
36
|
+
perm === 'readonly' && opts.permissionMode !== 'plan'
|
|
37
|
+
? opts.permissionMode
|
|
38
|
+
: perm === 'danger' && opts.permissionMode !== 'bypassPermissions'
|
|
39
|
+
? opts.permissionMode
|
|
40
|
+
: undefined;
|
|
41
|
+
// normalize: if caller passed a non-standard mode, preserve as native
|
|
42
|
+
const isStandard =
|
|
43
|
+
opts.permissionMode === 'plan' ||
|
|
44
|
+
opts.permissionMode === 'acceptEdits' ||
|
|
45
|
+
opts.permissionMode === 'bypassPermissions';
|
|
46
|
+
return runHarness({
|
|
47
|
+
harness: claudeHarness,
|
|
48
|
+
prompt: opts.prompt,
|
|
49
|
+
cwd: opts.cwd,
|
|
50
|
+
permission: perm,
|
|
51
|
+
nativePermission: isStandard ? undefined : opts.permissionMode,
|
|
52
|
+
model: opts.model,
|
|
53
|
+
maxBudgetUsd: opts.maxBudgetUsd,
|
|
54
|
+
addDirs: opts.addDirs,
|
|
55
|
+
signal: opts.signal,
|
|
56
|
+
timeoutMs: opts.timeoutMs,
|
|
57
|
+
resumeSessionId: opts.resumeSessionId,
|
|
58
|
+
onStream: opts.onStream,
|
|
59
|
+
onActivity: opts.onActivity,
|
|
60
|
+
}).then(r => ({ ...r, streamedText: r.streamedText }));
|
|
52
61
|
}
|
package/extensions/runner.ts
CHANGED
|
@@ -3,118 +3,130 @@ import { createInterface } from 'node:readline';
|
|
|
3
3
|
import type { Harness, ParseState, StreamedResult } from './harnesses/types.ts';
|
|
4
4
|
|
|
5
5
|
export interface RunHarnessOptions {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
6
|
+
harness: Harness;
|
|
7
|
+
prompt: string;
|
|
8
|
+
cwd: string;
|
|
9
|
+
permission: import('./harnesses/types.ts').NormalizedPermission;
|
|
10
|
+
nativePermission?: string;
|
|
11
|
+
model?: string;
|
|
12
|
+
maxBudgetUsd?: number;
|
|
13
|
+
addDirs?: string[];
|
|
14
|
+
signal?: AbortSignal;
|
|
15
|
+
timeoutMs?: number;
|
|
16
|
+
resumeSessionId?: string;
|
|
17
|
+
onStream?: (text: string) => void;
|
|
18
|
+
onActivity?: (ev: import('./harnesses/types.ts').ActivityEvent) => void;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export interface HarnessResult extends StreamedResult {
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
streamedText: string;
|
|
23
|
+
harness: string;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
import { DEFAULT_TIMEOUT_MS } from './harnesses/types.ts';
|
|
27
27
|
|
|
28
28
|
export function runHarness(opts: RunHarnessOptions): Promise<HarnessResult> {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const args = opts.harness.buildArgs({
|
|
31
|
+
prompt: opts.prompt,
|
|
32
|
+
cwd: opts.cwd,
|
|
33
|
+
permission: opts.permission,
|
|
34
|
+
nativePermission: opts.nativePermission,
|
|
35
|
+
model: opts.model,
|
|
36
|
+
maxBudgetUsd: opts.maxBudgetUsd,
|
|
37
|
+
addDirs: opts.addDirs,
|
|
38
|
+
resumeSessionId: opts.resumeSessionId,
|
|
39
|
+
});
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
const proc = spawn(opts.harness.binary, args, { cwd: opts.cwd, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
const state: ParseState = { streamedText: '', activities: [], result: null, _harness: {} };
|
|
44
|
+
let stderr = '';
|
|
45
|
+
let settled = false;
|
|
46
|
+
let firstTokenAt: number | null = null;
|
|
47
|
+
const startAt = Date.now();
|
|
48
|
+
const MAX_STREAMED = 5 * 1024 * 1024; // 5MB cap to prevent OOM on compromised harness
|
|
49
|
+
const MAX_ACTIVITIES = 5000;
|
|
48
50
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
51
|
+
const finish = (r: StreamedResult) => {
|
|
52
|
+
if (settled) return;
|
|
53
|
+
settled = true;
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
const ttft = firstTokenAt !== null ? firstTokenAt - startAt : r.ttftMs;
|
|
56
|
+
resolve({ ...r, ttftMs: ttft, streamedText: state.streamedText, harness: opts.harness.name });
|
|
57
|
+
};
|
|
58
|
+
const fail = (err: Error) => {
|
|
59
|
+
if (settled) return;
|
|
60
|
+
settled = true;
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
reject(err);
|
|
63
|
+
};
|
|
62
64
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
65
|
+
const rl = createInterface({ input: proc.stdout });
|
|
66
|
+
rl.on('line', line => {
|
|
67
|
+
const outcome = opts.harness.parseLine(line, state);
|
|
68
|
+
if (outcome.streamedText) {
|
|
69
|
+
if (firstTokenAt === null) firstTokenAt = Date.now();
|
|
70
|
+
// Cap streamedText to prevent OOM on compromised harness
|
|
71
|
+
if (state.streamedText.length < MAX_STREAMED) {
|
|
72
|
+
const remaining = MAX_STREAMED - state.streamedText.length;
|
|
73
|
+
const chunk =
|
|
74
|
+
outcome.streamedText.length > remaining
|
|
75
|
+
? `${outcome.streamedText.slice(0, remaining)} [truncated ${outcome.streamedText.length - remaining} chars]`
|
|
76
|
+
: outcome.streamedText;
|
|
77
|
+
state.streamedText += chunk;
|
|
78
|
+
opts.onStream?.(chunk);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (outcome.activities) {
|
|
82
|
+
for (const a of outcome.activities) {
|
|
83
|
+
if (state.activities.length < MAX_ACTIVITIES) {
|
|
84
|
+
state.activities.push(a);
|
|
85
|
+
opts.onActivity?.(a);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (outcome.result) {
|
|
90
|
+
// merge streamedText into result if empty
|
|
91
|
+
if (!outcome.result.result) outcome.result.result = state.streamedText;
|
|
92
|
+
state.result = outcome.result;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
83
95
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
96
|
+
proc.stderr.on('data', (d: Buffer) => (stderr += d.toString()));
|
|
97
|
+
proc.on('close', code => {
|
|
98
|
+
// Don't synthesize fallback on non-zero exit without explicit result — surface the error
|
|
99
|
+
if (code !== 0 && !state.result) {
|
|
100
|
+
fail(new Error(stderr.trim() || `${opts.harness.binary} exited with code ${code}`));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const final = opts.harness.extractResult(state);
|
|
104
|
+
if (final) {
|
|
105
|
+
if (!final.result) final.result = state.streamedText;
|
|
106
|
+
finish(final);
|
|
107
|
+
} else if (code !== 0) {
|
|
108
|
+
fail(new Error(stderr.trim() || `${opts.harness.binary} exited with code ${code}`));
|
|
109
|
+
} else {
|
|
110
|
+
fail(new Error(`${opts.harness.binary} finished without emitting a result`));
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
proc.on('error', err => {
|
|
114
|
+
fail(new Error(`failed to start ${opts.harness.binary}: ${err.message}`));
|
|
115
|
+
});
|
|
104
116
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
117
|
+
const timer = setTimeout(() => {
|
|
118
|
+
proc.kill('SIGKILL');
|
|
119
|
+
fail(new Error(`${opts.harness.binary} timed out after ${opts.timeoutMs ?? DEFAULT_TIMEOUT_MS}ms`));
|
|
120
|
+
}, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
121
|
+
timer.unref?.();
|
|
110
122
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
123
|
+
opts.signal?.addEventListener(
|
|
124
|
+
'abort',
|
|
125
|
+
() => {
|
|
126
|
+
proc.kill('SIGKILL');
|
|
127
|
+
fail(new Error('cancelled'));
|
|
128
|
+
},
|
|
129
|
+
{ once: true },
|
|
130
|
+
);
|
|
131
|
+
});
|
|
120
132
|
}
|
|
@@ -1,29 +1,37 @@
|
|
|
1
1
|
/** @deprecated use extensions/harnesses/claude.ts parse directly */
|
|
2
|
-
export type {
|
|
3
|
-
|
|
2
|
+
export type {
|
|
3
|
+
ActivityEvent,
|
|
4
|
+
ParseOutcome,
|
|
5
|
+
ParseState,
|
|
6
|
+
StreamedResult,
|
|
7
|
+
StreamedUsage,
|
|
8
|
+
StreamParseOutcome,
|
|
9
|
+
} from './harnesses/types.ts';
|
|
10
|
+
|
|
4
11
|
import { parseClaudeLine } from './harnesses/claude.ts';
|
|
12
|
+
import type { ParseState, StreamParseOutcome } from './harnesses/types.ts';
|
|
5
13
|
|
|
6
14
|
export function parseStreamLines(lines: Iterable<string>): StreamParseOutcome {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
15
|
+
const state: ParseState = { streamedText: '', activities: [], result: null };
|
|
16
|
+
let streamedText = '';
|
|
17
|
+
const activities: StreamParseOutcome['activities'] = [];
|
|
18
|
+
let result: StreamParseOutcome['result'] = null;
|
|
19
|
+
for (const line of lines) {
|
|
20
|
+
const out = parseClaudeLine(line, state);
|
|
21
|
+
if (out.streamedText) {
|
|
22
|
+
streamedText += out.streamedText;
|
|
23
|
+
state.streamedText += out.streamedText;
|
|
24
|
+
}
|
|
25
|
+
if (out.activities) {
|
|
26
|
+
for (const a of out.activities) {
|
|
27
|
+
activities.push(a);
|
|
28
|
+
state.activities.push(a);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (out.result) {
|
|
32
|
+
result = out.result;
|
|
33
|
+
state.result = out.result;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { streamedText, result, activities };
|
|
29
37
|
}
|