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.
@@ -1,137 +1,144 @@
1
1
  import { execFile } from 'node:child_process';
2
2
  import { promisify } from 'node:util';
3
- import type { Harness, BuildArgsOpts, NormalizedPermission, ParseOutcome, ParseState, StreamedResult } from './types.ts';
3
+ import type {
4
+ BuildArgsOpts,
5
+ Harness,
6
+ NormalizedPermission,
7
+ ParseOutcome,
8
+ ParseState,
9
+ StreamedResult,
10
+ } from './types.ts';
4
11
 
5
12
  const execFileAsync = promisify(execFile);
6
13
 
7
14
  function isRecord(v: unknown): v is Record<string, unknown> {
8
- return typeof v === 'object' && v !== null && !Array.isArray(v);
15
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
9
16
  }
10
17
 
11
18
  const PERMISSION_MAP: Record<NormalizedPermission, string> = {
12
- readonly: 'read-only',
13
- edit: 'workspace',
14
- danger: 'danger',
19
+ readonly: 'read-only',
20
+ edit: 'workspace',
21
+ danger: 'danger',
15
22
  };
16
23
 
17
24
  function extractAmpText(o: Record<string, unknown>): string | undefined {
18
- if (typeof o.text === 'string') return o.text;
19
- if (typeof o.output === 'string') return o.output;
20
- if (typeof o.delta === 'string') return o.delta;
21
- if (typeof o.content === 'string') return o.content;
22
- return undefined;
25
+ if (typeof o.text === 'string') return o.text;
26
+ if (typeof o.output === 'string') return o.output;
27
+ if (typeof o.delta === 'string') return o.delta;
28
+ if (typeof o.content === 'string') return o.content;
29
+ return undefined;
23
30
  }
24
31
 
25
32
  export function parseAmpLine(line: string, state: ParseState): ParseOutcome {
26
- let o: unknown;
27
- try {
28
- o = JSON.parse(line);
29
- } catch {
30
- if (line.trim().length > 0) return { streamedText: line + '\n', activities: [] };
31
- return { activities: [] };
32
- }
33
- if (!isRecord(o)) return { activities: [] };
34
- const activities: ParseOutcome['activities'] = [];
35
- let streamedText: string | undefined;
36
- const typeStr = typeof o.type === 'string' ? o.type : '';
33
+ let o: unknown;
34
+ try {
35
+ o = JSON.parse(line);
36
+ } catch {
37
+ if (line.trim().length > 0) return { streamedText: line + '\n', activities: [] };
38
+ return { activities: [] };
39
+ }
40
+ if (!isRecord(o)) return { activities: [] };
41
+ const activities: ParseOutcome['activities'] = [];
42
+ let streamedText: string | undefined;
43
+ const typeStr = typeof o.type === 'string' ? o.type : '';
37
44
 
38
- if (typeStr.includes('tool') || o.type === 'tool_use') {
39
- const name = typeof o.name === 'string' ? o.name : 'tool';
40
- if (typeStr.includes('start') || o.type === 'tool_use') {
41
- activities.push({ kind: 'tool_start', name });
42
- if (isRecord(o.input)) activities.push({ kind: 'tool_input', name, input: o.input as Record<string, unknown> });
43
- } else {
44
- activities.push({ kind: 'tool_result', isError: o.is_error === true });
45
- }
46
- }
47
- if (typeStr.includes('thinking')) activities.push({ kind: 'thinking', chars: 10 });
45
+ if (typeStr.includes('tool') || o.type === 'tool_use') {
46
+ const name = typeof o.name === 'string' ? o.name : 'tool';
47
+ if (typeStr.includes('start') || o.type === 'tool_use') {
48
+ activities.push({ kind: 'tool_start', name });
49
+ if (isRecord(o.input)) activities.push({ kind: 'tool_input', name, input: o.input as Record<string, unknown> });
50
+ } else {
51
+ activities.push({ kind: 'tool_result', isError: o.is_error === true });
52
+ }
53
+ }
54
+ if (typeStr.includes('thinking')) activities.push({ kind: 'thinking', chars: 10 });
48
55
 
49
- const text = extractAmpText(o);
50
- if (text && !typeStr.includes('tool')) streamedText = text;
56
+ const text = extractAmpText(o);
57
+ if (text && !typeStr.includes('tool')) streamedText = text;
51
58
 
52
- if (o.type === 'result' || o.type === 'done' || o.type === 'completed') {
53
- const usage = isRecord(o.usage) ? o.usage : null;
54
- const result: StreamedResult = {
55
- result: typeof o.result === 'string' ? o.result : state.streamedText + (streamedText ?? ''),
56
- isError: o.is_error === true,
57
- numTurns: typeof o.num_turns === 'number' ? o.num_turns : 0,
58
- totalCostUsd: typeof o.total_cost_usd === 'number' ? o.total_cost_usd : 0,
59
- sessionId: typeof o.session_id === 'string' ? o.session_id : null,
60
- stopReason: typeof o.stop_reason === 'string' ? o.stop_reason : null,
61
- permissionDenials: [],
62
- durationMs: typeof o.duration_ms === 'number' ? o.duration_ms : null,
63
- durationApiMs: null,
64
- ttftMs: null,
65
- model: typeof o.model === 'string' ? o.model : null,
66
- contextWindow: null,
67
- maxOutputTokens: null,
68
- usage: usage
69
- ? {
70
- inputTokens: typeof usage.input_tokens === 'number' ? usage.input_tokens : 0,
71
- outputTokens: typeof usage.output_tokens === 'number' ? usage.output_tokens : 0,
72
- cacheCreationInputTokens: 0,
73
- cacheReadInputTokens: 0,
74
- }
75
- : null,
76
- };
77
- return { activities, streamedText, result };
78
- }
79
- return { activities, streamedText };
59
+ if (o.type === 'result' || o.type === 'done' || o.type === 'completed') {
60
+ const usage = isRecord(o.usage) ? o.usage : null;
61
+ const result: StreamedResult = {
62
+ result: typeof o.result === 'string' ? o.result : state.streamedText + (streamedText ?? ''),
63
+ isError: o.is_error === true,
64
+ numTurns: typeof o.num_turns === 'number' ? o.num_turns : 0,
65
+ totalCostUsd: typeof o.total_cost_usd === 'number' ? o.total_cost_usd : 0,
66
+ sessionId: typeof o.session_id === 'string' ? o.session_id : null,
67
+ stopReason: typeof o.stop_reason === 'string' ? o.stop_reason : null,
68
+ permissionDenials: [],
69
+ durationMs: typeof o.duration_ms === 'number' ? o.duration_ms : null,
70
+ durationApiMs: null,
71
+ ttftMs: null,
72
+ model: typeof o.model === 'string' ? o.model : null,
73
+ contextWindow: null,
74
+ maxOutputTokens: null,
75
+ usage: usage
76
+ ? {
77
+ inputTokens: typeof usage.input_tokens === 'number' ? usage.input_tokens : 0,
78
+ outputTokens: typeof usage.output_tokens === 'number' ? usage.output_tokens : 0,
79
+ cacheCreationInputTokens: 0,
80
+ cacheReadInputTokens: 0,
81
+ }
82
+ : null,
83
+ };
84
+ return { activities, streamedText, result };
85
+ }
86
+ return { activities, streamedText };
80
87
  }
81
88
 
82
89
  export const ampHarness: Harness = {
83
- name: 'amp',
84
- displayName: 'Amp',
85
- binary: 'amp',
86
- async detect() {
87
- try {
88
- const { stdout } = await execFileAsync('amp', ['--version'], { timeout: 5000 });
89
- return { ok: true, version: stdout.trim() };
90
- } catch {
91
- try {
92
- const { stdout } = await execFileAsync('omp', ['--version'], { timeout: 5000 });
93
- return { ok: true, version: stdout.trim() };
94
- } catch {
95
- return { ok: false, hint: 'Install Amp: https://ampcode.com' };
96
- }
97
- }
98
- },
99
- buildArgs(opts: BuildArgsOpts): string[] {
100
- const perm = opts.nativePermission ?? PERMISSION_MAP[opts.permission] ?? 'workspace';
101
- const args = ['--output', 'jsonl', opts.prompt, '--permission', perm];
102
- if (opts.model) args.push('--model', opts.model);
103
- if (opts.resumeSessionId) args.push('--resume', opts.resumeSessionId);
104
- for (const dir of opts.addDirs ?? []) args.push('--add-dir', dir);
105
- return args;
106
- },
107
- parseLine(line: string, state: ParseState): ParseOutcome {
108
- return parseAmpLine(line, state);
109
- },
110
- extractResult(state: ParseState): StreamedResult | null {
111
- if (state.result) return state.result;
112
- if (state.streamedText.trim().length > 0) {
113
- return {
114
- result: state.streamedText,
115
- isError: false,
116
- numTurns: 1,
117
- totalCostUsd: 0,
118
- sessionId: null,
119
- stopReason: null,
120
- permissionDenials: [],
121
- durationMs: null,
122
- durationApiMs: null,
123
- ttftMs: null,
124
- model: null,
125
- contextWindow: null,
126
- maxOutputTokens: null,
127
- usage: null,
128
- };
129
- }
130
- return null;
131
- },
132
- permissionMap: {
133
- readonly: ['read-only'],
134
- edit: ['workspace'],
135
- danger: ['danger'],
136
- },
90
+ name: 'amp',
91
+ displayName: 'Amp',
92
+ binary: 'amp',
93
+ async detect() {
94
+ try {
95
+ const { stdout } = await execFileAsync('amp', ['--version'], { timeout: 5000 });
96
+ return { ok: true, version: stdout.trim() };
97
+ } catch {
98
+ try {
99
+ const { stdout } = await execFileAsync('omp', ['--version'], { timeout: 5000 });
100
+ return { ok: true, version: stdout.trim() };
101
+ } catch {
102
+ return { ok: false, hint: 'Install Amp: https://ampcode.com' };
103
+ }
104
+ }
105
+ },
106
+ buildArgs(opts: BuildArgsOpts): string[] {
107
+ const perm = opts.nativePermission ?? PERMISSION_MAP[opts.permission] ?? 'workspace';
108
+ const args = ['--output', 'jsonl', opts.prompt, '--permission', perm];
109
+ if (opts.model) args.push('--model', opts.model);
110
+ if (opts.resumeSessionId) args.push('--resume', opts.resumeSessionId);
111
+ for (const dir of opts.addDirs ?? []) args.push('--add-dir', dir);
112
+ return args;
113
+ },
114
+ parseLine(line: string, state: ParseState): ParseOutcome {
115
+ return parseAmpLine(line, state);
116
+ },
117
+ extractResult(state: ParseState): StreamedResult | null {
118
+ if (state.result) return state.result;
119
+ if (state.streamedText.trim().length > 0) {
120
+ return {
121
+ result: state.streamedText,
122
+ isError: false,
123
+ numTurns: 1,
124
+ totalCostUsd: 0,
125
+ sessionId: null,
126
+ stopReason: null,
127
+ permissionDenials: [],
128
+ durationMs: null,
129
+ durationApiMs: null,
130
+ ttftMs: null,
131
+ model: null,
132
+ contextWindow: null,
133
+ maxOutputTokens: null,
134
+ usage: null,
135
+ };
136
+ }
137
+ return null;
138
+ },
139
+ permissionMap: {
140
+ readonly: ['read-only'],
141
+ edit: ['workspace'],
142
+ danger: ['danger'],
143
+ },
137
144
  };
@@ -1,129 +1,154 @@
1
1
  import { execFile } from 'node:child_process';
2
2
  import { promisify } from 'node:util';
3
- import type { Harness, BuildArgsOpts, NormalizedPermission, ParseOutcome, ParseState, StreamedResult } from './types.ts';
3
+ import type {
4
+ BuildArgsOpts,
5
+ Harness,
6
+ NormalizedPermission,
7
+ ParseOutcome,
8
+ ParseState,
9
+ StreamedResult,
10
+ } from './types.ts';
4
11
 
5
12
  const execFileAsync = promisify(execFile);
6
13
 
7
14
  function isRecord(v: unknown): v is Record<string, unknown> {
8
- return typeof v === 'object' && v !== null && !Array.isArray(v);
15
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
9
16
  }
10
17
 
11
18
  const PERMISSION_MAP: Record<NormalizedPermission, string> = {
12
- readonly: 'plan',
13
- edit: 'acceptEdits',
14
- danger: 'bypassPermissions',
19
+ readonly: 'plan',
20
+ edit: 'acceptEdits',
21
+ danger: 'bypassPermissions',
15
22
  };
16
23
 
17
24
  export function parseClaudeLine(line: string, state: ParseState): ParseOutcome {
18
- let o: unknown;
19
- try {
20
- o = JSON.parse(line);
21
- } catch {
22
- return { activities: [] };
23
- }
24
- if (!isRecord(o)) return { activities: [] };
25
- const activities: ParseOutcome['activities'] = [];
26
- let streamedText: string | undefined;
25
+ let o: unknown;
26
+ try {
27
+ o = JSON.parse(line);
28
+ } catch {
29
+ return { activities: [] };
30
+ }
31
+ if (!isRecord(o)) return { activities: [] };
32
+ const activities: ParseOutcome['activities'] = [];
33
+ let streamedText: string | undefined;
27
34
 
28
- if (o.type === 'stream_event' && isRecord(o.event)) {
29
- const ev = o.event;
30
- const delta = isRecord(ev.delta) ? ev.delta : undefined;
31
- if (ev.type === 'content_block_delta' && delta?.type === 'text_delta' && typeof delta.text === 'string') {
32
- streamedText = delta.text;
33
- } else if (ev.type === 'content_block_delta' && delta?.type === 'thinking_delta' && typeof delta.thinking === 'string') {
34
- activities.push({ kind: 'thinking', chars: delta.thinking.length });
35
- } else if (ev.type === 'content_block_start' && isRecord(ev.content_block)) {
36
- const cb = ev.content_block;
37
- if (cb.type === 'tool_use' && typeof cb.name === 'string') {
38
- activities.push({ kind: 'tool_start', name: cb.name });
39
- }
40
- }
41
- } else if (o.type === 'assistant' && isRecord(o.message)) {
42
- for (const block of Array.isArray(o.message.content) ? o.message.content : []) {
43
- if (isRecord(block) && block.type === 'tool_use' && typeof block.name === 'string') {
44
- activities.push({ kind: 'tool_input', name: block.name, input: isRecord(block.input) ? block.input : {} });
45
- }
46
- }
47
- } else if (o.type === 'user' && isRecord(o.message)) {
48
- for (const block of Array.isArray(o.message.content) ? o.message.content : []) {
49
- if (isRecord(block) && block.type === 'tool_result') {
50
- activities.push({ kind: 'tool_result', isError: block.is_error === true });
51
- }
52
- }
53
- } else if (o.type === 'result') {
54
- const u = isRecord(o.usage) ? o.usage : null;
55
- let model: string | null = null;
56
- let contextWindow: number | null = null;
57
- let maxOutputTokens: number | null = null;
58
- if (isRecord(o.modelUsage)) {
59
- const first = Object.entries(o.modelUsage)[0]?.[1];
60
- const firstKey = Object.keys(o.modelUsage)[0];
61
- if (firstKey) model = firstKey;
62
- if (isRecord(first)) {
63
- if (typeof first.contextWindow === 'number') contextWindow = first.contextWindow;
64
- if (typeof first.maxOutputTokens === 'number') maxOutputTokens = first.maxOutputTokens;
65
- }
66
- }
67
- const result: StreamedResult = {
68
- result: typeof o.result === 'string' ? o.result : state.streamedText,
69
- isError: o.is_error === true,
70
- numTurns: typeof o.num_turns === 'number' ? o.num_turns : 0,
71
- totalCostUsd: typeof o.total_cost_usd === 'number' ? o.total_cost_usd : 0,
72
- sessionId: typeof o.session_id === 'string' ? o.session_id : null,
73
- stopReason: typeof o.stop_reason === 'string' ? o.stop_reason : null,
74
- permissionDenials: Array.isArray(o.permission_denials) ? o.permission_denials : [],
75
- durationMs: typeof o.duration_ms === 'number' ? o.duration_ms : null,
76
- durationApiMs: typeof o.duration_api_ms === 'number' ? o.duration_api_ms : null,
77
- ttftMs: typeof o.ttft_ms === 'number' ? o.ttft_ms : null,
78
- model,
79
- contextWindow,
80
- maxOutputTokens,
81
- usage: u
82
- ? {
83
- inputTokens: typeof u.input_tokens === 'number' ? u.input_tokens : 0,
84
- outputTokens: typeof u.output_tokens === 'number' ? u.output_tokens : 0,
85
- cacheCreationInputTokens: typeof u.cache_creation_input_tokens === 'number' ? u.cache_creation_input_tokens : 0,
86
- cacheReadInputTokens: typeof u.cache_read_input_tokens === 'number' ? u.cache_read_input_tokens : 0,
87
- }
88
- : null,
89
- };
90
- return { activities, streamedText, result };
91
- }
35
+ if (o.type === 'stream_event' && isRecord(o.event)) {
36
+ const ev = o.event;
37
+ const delta = isRecord(ev.delta) ? ev.delta : undefined;
38
+ if (ev.type === 'content_block_delta' && delta?.type === 'text_delta' && typeof delta.text === 'string') {
39
+ streamedText = delta.text;
40
+ } else if (
41
+ ev.type === 'content_block_delta' &&
42
+ delta?.type === 'thinking_delta' &&
43
+ typeof delta.thinking === 'string'
44
+ ) {
45
+ activities.push({ kind: 'thinking', chars: delta.thinking.length });
46
+ } else if (ev.type === 'content_block_start' && isRecord(ev.content_block)) {
47
+ const cb = ev.content_block;
48
+ if (cb.type === 'tool_use' && typeof cb.name === 'string') {
49
+ activities.push({ kind: 'tool_start', name: cb.name });
50
+ }
51
+ }
52
+ } else if (o.type === 'assistant' && isRecord(o.message)) {
53
+ for (const block of Array.isArray(o.message.content) ? o.message.content : []) {
54
+ if (isRecord(block) && block.type === 'tool_use' && typeof block.name === 'string') {
55
+ activities.push({
56
+ kind: 'tool_input',
57
+ name: block.name,
58
+ input: isRecord(block.input) ? block.input : {},
59
+ });
60
+ }
61
+ }
62
+ } else if (o.type === 'user' && isRecord(o.message)) {
63
+ for (const block of Array.isArray(o.message.content) ? o.message.content : []) {
64
+ if (isRecord(block) && block.type === 'tool_result') {
65
+ activities.push({ kind: 'tool_result', isError: block.is_error === true });
66
+ }
67
+ }
68
+ } else if (o.type === 'result') {
69
+ const u = isRecord(o.usage) ? o.usage : null;
70
+ let model: string | null = null;
71
+ let contextWindow: number | null = null;
72
+ let maxOutputTokens: number | null = null;
73
+ if (isRecord(o.modelUsage)) {
74
+ const first = Object.entries(o.modelUsage)[0]?.[1];
75
+ const firstKey = Object.keys(o.modelUsage)[0];
76
+ if (firstKey) model = firstKey;
77
+ if (isRecord(first)) {
78
+ if (typeof first.contextWindow === 'number') contextWindow = first.contextWindow;
79
+ if (typeof first.maxOutputTokens === 'number') maxOutputTokens = first.maxOutputTokens;
80
+ }
81
+ }
82
+ const result: StreamedResult = {
83
+ result: typeof o.result === 'string' ? o.result : state.streamedText,
84
+ isError: o.is_error === true,
85
+ numTurns: typeof o.num_turns === 'number' ? o.num_turns : 0,
86
+ totalCostUsd: typeof o.total_cost_usd === 'number' ? o.total_cost_usd : 0,
87
+ sessionId: typeof o.session_id === 'string' ? o.session_id : null,
88
+ stopReason: typeof o.stop_reason === 'string' ? o.stop_reason : null,
89
+ permissionDenials: Array.isArray(o.permission_denials) ? o.permission_denials : [],
90
+ durationMs: typeof o.duration_ms === 'number' ? o.duration_ms : null,
91
+ durationApiMs: typeof o.duration_api_ms === 'number' ? o.duration_api_ms : null,
92
+ ttftMs: typeof o.ttft_ms === 'number' ? o.ttft_ms : null,
93
+ model,
94
+ contextWindow,
95
+ maxOutputTokens,
96
+ usage: u
97
+ ? {
98
+ inputTokens: typeof u.input_tokens === 'number' ? u.input_tokens : 0,
99
+ outputTokens: typeof u.output_tokens === 'number' ? u.output_tokens : 0,
100
+ cacheCreationInputTokens:
101
+ typeof u.cache_creation_input_tokens === 'number' ? u.cache_creation_input_tokens : 0,
102
+ cacheReadInputTokens: typeof u.cache_read_input_tokens === 'number' ? u.cache_read_input_tokens : 0,
103
+ }
104
+ : null,
105
+ };
106
+ return { activities, streamedText, result };
107
+ }
92
108
 
93
- return { activities, streamedText };
109
+ return { activities, streamedText };
94
110
  }
95
111
 
96
112
  export const claudeHarness: Harness = {
97
- name: 'claude',
98
- displayName: 'Claude Code',
99
- binary: 'claude',
100
- async detect() {
101
- try {
102
- const { stdout } = await execFileAsync('claude', ['--version'], { timeout: 5000 });
103
- return { ok: true, version: stdout.trim() };
104
- } catch {
105
- return { ok: false, hint: 'Install Claude Code: https://docs.anthropic.com/en/docs/claude-code' };
106
- }
107
- },
108
- buildArgs(opts: BuildArgsOpts): string[] {
109
- const mode = opts.nativePermission ?? PERMISSION_MAP[opts.permission] ?? 'acceptEdits';
110
- const args = ['-p', opts.prompt, '--output-format', 'stream-json', '--verbose', '--include-partial-messages', '--permission-mode', mode];
111
- if (opts.resumeSessionId) args.push('--resume', opts.resumeSessionId);
112
- else args.push('--no-session-persistence');
113
- if (opts.model) args.push('--model', opts.model);
114
- if (opts.maxBudgetUsd !== undefined) args.push('--max-budget-usd', String(opts.maxBudgetUsd));
115
- for (const dir of opts.addDirs ?? []) args.push('--add-dir', dir);
116
- return args;
117
- },
118
- parseLine(line: string, state: ParseState): ParseOutcome {
119
- return parseClaudeLine(line, state);
120
- },
121
- extractResult(state: ParseState): StreamedResult | null {
122
- return state.result;
123
- },
124
- permissionMap: {
125
- readonly: ['plan'],
126
- edit: ['acceptEdits'],
127
- danger: ['bypassPermissions'],
128
- },
113
+ name: 'claude',
114
+ displayName: 'Claude Code',
115
+ binary: 'claude',
116
+ async detect() {
117
+ try {
118
+ const { stdout } = await execFileAsync('claude', ['--version'], { timeout: 5000 });
119
+ return { ok: true, version: stdout.trim() };
120
+ } catch {
121
+ return { ok: false, hint: 'Install Claude Code: https://docs.anthropic.com/en/docs/claude-code' };
122
+ }
123
+ },
124
+ buildArgs(opts: BuildArgsOpts): string[] {
125
+ const mode = opts.nativePermission ?? PERMISSION_MAP[opts.permission] ?? 'acceptEdits';
126
+ const args = [
127
+ '-p',
128
+ opts.prompt,
129
+ '--output-format',
130
+ 'stream-json',
131
+ '--verbose',
132
+ '--include-partial-messages',
133
+ '--permission-mode',
134
+ mode,
135
+ ];
136
+ if (opts.resumeSessionId) args.push('--resume', opts.resumeSessionId);
137
+ else args.push('--no-session-persistence');
138
+ if (opts.model) args.push('--model', opts.model);
139
+ if (opts.maxBudgetUsd !== undefined) args.push('--max-budget-usd', String(opts.maxBudgetUsd));
140
+ for (const dir of opts.addDirs ?? []) args.push('--add-dir', dir);
141
+ return args;
142
+ },
143
+ parseLine(line: string, state: ParseState): ParseOutcome {
144
+ return parseClaudeLine(line, state);
145
+ },
146
+ extractResult(state: ParseState): StreamedResult | null {
147
+ return state.result;
148
+ },
149
+ permissionMap: {
150
+ readonly: ['plan'],
151
+ edit: ['acceptEdits'],
152
+ danger: ['bypassPermissions'],
153
+ },
129
154
  };