pi-harness-delegate 0.1.0 → 0.1.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.
@@ -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
  };