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
|
@@ -1,137 +1,144 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
2
|
import { promisify } from 'node:util';
|
|
3
|
-
import type {
|
|
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
|
-
|
|
15
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
9
16
|
}
|
|
10
17
|
|
|
11
18
|
const PERMISSION_MAP: Record<NormalizedPermission, string> = {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
50
|
-
|
|
56
|
+
const text = extractAmpText(o);
|
|
57
|
+
if (text && !typeStr.includes('tool')) streamedText = text;
|
|
51
58
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
15
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
9
16
|
}
|
|
10
17
|
|
|
11
18
|
const PERMISSION_MAP: Record<NormalizedPermission, string> = {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
19
|
+
readonly: 'plan',
|
|
20
|
+
edit: 'acceptEdits',
|
|
21
|
+
danger: 'bypassPermissions',
|
|
15
22
|
};
|
|
16
23
|
|
|
17
24
|
export function parseClaudeLine(line: string, state: ParseState): ParseOutcome {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
109
|
+
return { activities, streamedText };
|
|
94
110
|
}
|
|
95
111
|
|
|
96
112
|
export const claudeHarness: Harness = {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
};
|