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.
@@ -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
- | { kind: 'tool'; text: string; ok?: boolean }
20
- | { kind: 'thinking'; text: string }
21
- | { kind: 'text'; text: string };
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
- /** 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;
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
- 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')}`;
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
- 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
- }
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
- 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);
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
- const disarm = () => {
74
- armed = false;
75
- if (armTimer) {
76
- clearTimeout(armTimer);
77
- armTimer = null;
78
- }
79
- };
73
+ const disarm = () => {
74
+ armed = false;
75
+ if (armTimer) {
76
+ clearTimeout(armTimer);
77
+ armTimer = null;
78
+ }
79
+ };
80
80
 
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[] = [];
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
- // 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}─╮`));
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
- // danger banner
96
- if (opts.dangerous) {
97
- const banner = theme.fg('error', '⚠ danger — unrestricted access');
98
- out.push(`│ ${padTo(banner, inner)} │`);
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
- // feed
102
- for (const entry of opts.getEntries().slice(-MAX_VISIBLE_ENTRIES)) {
103
- out.push(`│ ${padTo(truncateToWidth(renderEntry(entry, theme), inner), inner)} │`);
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
- // 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)} │`);
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
- 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
- };
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
  }
@@ -1,52 +1,61 @@
1
1
  /** @deprecated use extensions/runner.ts + harnesses/claude.ts directly */
2
- import type { ActivityEvent, StreamedResult } from './harnesses/types.ts';
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
- prompt: string;
8
- cwd: string;
9
- permissionMode: string;
10
- model?: string;
11
- maxBudgetUsd?: number;
12
- addDirs?: string[];
13
- signal?: AbortSignal;
14
- timeoutMs?: number;
15
- resumeSessionId?: string;
16
- onStream?: (text: string) => void;
17
- onActivity?: (ev: ActivityEvent) => void;
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
- streamedText: string;
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
- if (mode === 'plan') return 'readonly';
28
- if (mode === 'bypassPermissions') return 'danger';
29
- return 'edit';
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
- const perm = mapPerm(opts.permissionMode);
34
- const nativePerm = perm === 'readonly' && opts.permissionMode !== 'plan' ? opts.permissionMode : perm === 'danger' && opts.permissionMode !== 'bypassPermissions' ? opts.permissionMode : undefined;
35
- // normalize: if caller passed a non-standard mode, preserve as native
36
- const isStandard = opts.permissionMode === 'plan' || opts.permissionMode === 'acceptEdits' || opts.permissionMode === 'bypassPermissions';
37
- return runHarness({
38
- harness: claudeHarness,
39
- prompt: opts.prompt,
40
- cwd: opts.cwd,
41
- permission: perm,
42
- nativePermission: isStandard ? undefined : opts.permissionMode,
43
- model: opts.model,
44
- maxBudgetUsd: opts.maxBudgetUsd,
45
- addDirs: opts.addDirs,
46
- signal: opts.signal,
47
- timeoutMs: opts.timeoutMs,
48
- resumeSessionId: opts.resumeSessionId,
49
- onStream: opts.onStream,
50
- onActivity: opts.onActivity,
51
- }).then((r) => ({ ...r, streamedText: r.streamedText }));
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
  }
@@ -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
- 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;
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
- streamedText: string;
23
- harness: string;
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
- 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
- });
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
- const proc = spawn(opts.harness.binary, args, { cwd: opts.cwd, stdio: ['ignore', 'pipe', 'pipe'] });
41
+ const proc = spawn(opts.harness.binary, args, { cwd: opts.cwd, stdio: ['ignore', 'pipe', 'pipe'] });
42
42
 
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();
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
- const finish = (r: StreamedResult) => {
50
- if (settled) return;
51
- settled = true;
52
- clearTimeout(timer);
53
- const ttft = firstTokenAt !== null ? firstTokenAt - startAt : r.ttftMs;
54
- resolve({ ...r, ttftMs: ttft, streamedText: state.streamedText, harness: opts.harness.name });
55
- };
56
- const fail = (err: Error) => {
57
- if (settled) return;
58
- settled = true;
59
- clearTimeout(timer);
60
- reject(err);
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
- const rl = createInterface({ input: proc.stdout });
64
- rl.on('line', (line) => {
65
- const outcome = opts.harness.parseLine(line, state);
66
- if (outcome.streamedText) {
67
- if (firstTokenAt === null) firstTokenAt = Date.now();
68
- state.streamedText += outcome.streamedText;
69
- opts.onStream?.(outcome.streamedText);
70
- }
71
- if (outcome.activities) {
72
- for (const a of outcome.activities) {
73
- state.activities.push(a);
74
- opts.onActivity?.(a);
75
- }
76
- }
77
- if (outcome.result) {
78
- // merge streamedText into result if empty
79
- if (!outcome.result.result) outcome.result.result = state.streamedText;
80
- state.result = outcome.result;
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
- proc.stderr.on('data', (d: Buffer) => (stderr += d.toString()));
85
- proc.on('close', (code) => {
86
- // Don't synthesize fallback on non-zero exit without explicit result — surface the error
87
- if (code !== 0 && !state.result) {
88
- fail(new Error(stderr.trim() || `${opts.harness.binary} exited with code ${code}`));
89
- return;
90
- }
91
- const final = opts.harness.extractResult(state);
92
- if (final) {
93
- if (!final.result) final.result = state.streamedText;
94
- finish(final);
95
- } else if (code !== 0) {
96
- fail(new Error(stderr.trim() || `${opts.harness.binary} exited with code ${code}`));
97
- } else {
98
- fail(new Error(`${opts.harness.binary} finished without emitting a result`));
99
- }
100
- });
101
- proc.on('error', (err) => {
102
- fail(new Error(`failed to start ${opts.harness.binary}: ${err.message}`));
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
- const timer = setTimeout(() => {
106
- proc.kill('SIGKILL');
107
- fail(new Error(`${opts.harness.binary} timed out after ${opts.timeoutMs ?? DEFAULT_TIMEOUT_MS}ms`));
108
- }, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
109
- timer.unref?.();
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
- opts.signal?.addEventListener(
112
- 'abort',
113
- () => {
114
- proc.kill('SIGKILL');
115
- fail(new Error('cancelled'));
116
- },
117
- { once: true },
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 { StreamedUsage, StreamedResult, ActivityEvent, StreamParseOutcome, ParseState, ParseOutcome } from './harnesses/types.ts';
3
- import type { ParseState, StreamParseOutcome } from './harnesses/types.ts';
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
- const state: ParseState = { streamedText: '', activities: [], result: null };
8
- let streamedText = '';
9
- const activities: StreamParseOutcome['activities'] = [];
10
- let result: StreamParseOutcome['result'] = null;
11
- for (const line of lines) {
12
- const out = parseClaudeLine(line, state);
13
- if (out.streamedText) {
14
- streamedText += out.streamedText;
15
- state.streamedText += out.streamedText;
16
- }
17
- if (out.activities) {
18
- for (const a of out.activities) {
19
- activities.push(a);
20
- state.activities.push(a);
21
- }
22
- }
23
- if (out.result) {
24
- result = out.result;
25
- state.result = out.result;
26
- }
27
- }
28
- return { streamedText, result, activities };
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
  }