pi-harness-delegate 0.2.0 → 0.2.2
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/extensions/config.ts +12 -8
- package/extensions/harnesses/amp.ts +132 -37
- package/extensions/harnesses/codex.ts +64 -49
- package/extensions/harnesses/opencode.ts +84 -36
- package/extensions/index.ts +6 -5
- package/extensions/run-claude.ts +1 -1
- package/package.json +3 -3
package/extensions/config.ts
CHANGED
|
@@ -61,16 +61,17 @@ export function loadConfig(): DelegateConfig {
|
|
|
61
61
|
// Legacy claudeDelegate -> delegate.harnesses.claude migration
|
|
62
62
|
if (settings.claudeDelegate && !settings.delegate) {
|
|
63
63
|
const c = settings.claudeDelegate as Partial<DelegateConfig>;
|
|
64
|
-
if (typeof c.model === 'string') cfg.harnesses
|
|
64
|
+
if (typeof c.model === 'string') cfg.harnesses.claude = { ...(cfg.harnesses.claude ?? {}), model: c.model };
|
|
65
65
|
if (typeof c.timeoutMs === 'number' && c.timeoutMs > 0) cfg.timeoutMs = c.timeoutMs;
|
|
66
66
|
if (typeof c.defaultMode === 'string') cfg.defaultMode = c.defaultMode;
|
|
67
67
|
if (typeof c.allowDangerous === 'boolean') cfg.allowDangerous = c.allowDangerous;
|
|
68
68
|
if (typeof c.inspectThinking === 'boolean') cfg.inspectThinking = c.inspectThinking;
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
const maxBudgetLegacy = (c as DelegateConfig).maxBudgetUsd;
|
|
70
|
+
if (typeof maxBudgetLegacy === 'number' && maxBudgetLegacy > 0) cfg.maxBudgetUsd = maxBudgetLegacy;
|
|
71
71
|
if (typeof c.autoDelegateHints === 'boolean') cfg.autoDelegateHints = c.autoDelegateHints;
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
const legacyAliases = (c as DelegateConfig).modelAliases;
|
|
73
|
+
if (legacyAliases && typeof legacyAliases === 'object') {
|
|
74
|
+
for (const [k, v] of Object.entries(legacyAliases)) {
|
|
74
75
|
if (typeof v === 'string' && v) cfg.modelAliases[k] = v;
|
|
75
76
|
}
|
|
76
77
|
}
|
|
@@ -134,8 +135,8 @@ export function loadConfig(): DelegateConfig {
|
|
|
134
135
|
// also support legacy claudeDelegate merged when delegate also present (delegate wins)
|
|
135
136
|
if (settings.claudeDelegate) {
|
|
136
137
|
const c = settings.claudeDelegate as Partial<DelegateConfig>;
|
|
137
|
-
if (typeof c.model === 'string' && !cfg.harnesses
|
|
138
|
-
cfg.harnesses
|
|
138
|
+
if (typeof c.model === 'string' && !cfg.harnesses.claude?.model) {
|
|
139
|
+
cfg.harnesses.claude = { ...(cfg.harnesses.claude ?? {}), model: c.model };
|
|
139
140
|
}
|
|
140
141
|
}
|
|
141
142
|
} catch {
|
|
@@ -158,7 +159,10 @@ export function getMaxConcurrent(cfg: DelegateConfig, harness?: string): number
|
|
|
158
159
|
if (typeof cfg.maxConcurrent === 'number') return cfg.maxConcurrent;
|
|
159
160
|
// if object shape {global, perHarness}
|
|
160
161
|
const mc = cfg.maxConcurrent as unknown as { global?: number; perHarness?: Record<string, number> };
|
|
161
|
-
if (harness && mc.perHarness && typeof mc.perHarness[harness] === 'number')
|
|
162
|
+
if (harness && mc.perHarness && typeof mc.perHarness[harness] === 'number') {
|
|
163
|
+
const v = mc.perHarness[harness];
|
|
164
|
+
if (typeof v === 'number') return v;
|
|
165
|
+
}
|
|
162
166
|
if (typeof mc.global === 'number') return mc.global;
|
|
163
167
|
return 1;
|
|
164
168
|
}
|
|
@@ -10,82 +10,179 @@ import type {
|
|
|
10
10
|
} from './types.ts';
|
|
11
11
|
|
|
12
12
|
const execFileAsync = promisify(execFile);
|
|
13
|
-
|
|
14
13
|
function isRecord(v: unknown): v is Record<string, unknown> {
|
|
15
14
|
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
16
15
|
}
|
|
17
|
-
|
|
18
16
|
const PERMISSION_MAP: Record<NormalizedPermission, string> = {
|
|
19
17
|
readonly: 'read-only',
|
|
20
18
|
edit: 'workspace',
|
|
21
19
|
danger: 'danger',
|
|
22
20
|
};
|
|
23
|
-
|
|
24
21
|
function extractAmpText(o: Record<string, unknown>): string | undefined {
|
|
25
22
|
if (typeof o.text === 'string') return o.text;
|
|
26
23
|
if (typeof o.output === 'string') return o.output;
|
|
27
24
|
if (typeof o.delta === 'string') return o.delta;
|
|
28
25
|
if (typeof o.content === 'string') return o.content;
|
|
26
|
+
if (isRecord(o.part) && typeof o.part.text === 'string') return o.part.text;
|
|
29
27
|
return undefined;
|
|
30
28
|
}
|
|
31
|
-
|
|
32
29
|
export function parseAmpLine(line: string, state: ParseState): ParseOutcome {
|
|
33
30
|
let o: unknown;
|
|
34
31
|
try {
|
|
35
32
|
o = JSON.parse(line);
|
|
36
33
|
} catch {
|
|
37
|
-
if (line.trim().length > 0) return { streamedText: line
|
|
34
|
+
if (line.trim().length > 0) return { streamedText: `${line}\n`, activities: [] };
|
|
38
35
|
return { activities: [] };
|
|
39
36
|
}
|
|
40
37
|
if (!isRecord(o)) return { activities: [] };
|
|
41
38
|
const activities: ParseOutcome['activities'] = [];
|
|
42
39
|
let streamedText: string | undefined;
|
|
43
40
|
const typeStr = typeof o.type === 'string' ? o.type : '';
|
|
44
|
-
|
|
41
|
+
// latch session id
|
|
42
|
+
if (typeStr === 'session' && typeof o.id === 'string') {
|
|
43
|
+
(state as unknown as Record<string, unknown>)._harness = {
|
|
44
|
+
...(((state as unknown as Record<string, unknown>)._harness as Record<string, unknown>) ?? {}),
|
|
45
|
+
sessionId: o.id,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (isRecord(o.part) && typeof (o.part as Record<string, unknown>).sessionID === 'string') {
|
|
49
|
+
(state as unknown as Record<string, unknown>)._harness = {
|
|
50
|
+
...(((state as unknown as Record<string, unknown>)._harness as Record<string, unknown>) ?? {}),
|
|
51
|
+
sessionId: (o.part as Record<string, unknown>).sessionID as string,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (typeStr === 'session' && typeof (o as Record<string, unknown>).sessionID === 'string') {
|
|
55
|
+
(state as unknown as Record<string, unknown>)._harness = {
|
|
56
|
+
...(((state as unknown as Record<string, unknown>)._harness as Record<string, unknown>) ?? {}),
|
|
57
|
+
sessionId: (o as Record<string, unknown>).sessionID as string,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
45
60
|
if (typeStr.includes('tool') || o.type === 'tool_use') {
|
|
46
61
|
const name = typeof o.name === 'string' ? o.name : 'tool';
|
|
47
62
|
if (typeStr.includes('start') || o.type === 'tool_use') {
|
|
48
63
|
activities.push({ kind: 'tool_start', name });
|
|
49
64
|
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
|
-
}
|
|
65
|
+
} else activities.push({ kind: 'tool_result', isError: o.is_error === true });
|
|
53
66
|
}
|
|
54
67
|
if (typeStr.includes('thinking')) activities.push({ kind: 'thinking', chars: 10 });
|
|
55
|
-
|
|
68
|
+
if (typeStr === 'message_update' && isRecord(o.assistantMessageEvent)) {
|
|
69
|
+
const ev = o.assistantMessageEvent as Record<string, unknown>;
|
|
70
|
+
if (ev.type === 'thinking_delta' && typeof ev.delta === 'string')
|
|
71
|
+
activities.push({ kind: 'thinking', chars: ev.delta.length });
|
|
72
|
+
else if (ev.type === 'text_delta' && typeof ev.delta === 'string') streamedText = ev.delta;
|
|
73
|
+
else if (ev.type === 'thinking_start') activities.push({ kind: 'thinking', chars: 5 });
|
|
74
|
+
}
|
|
75
|
+
if (typeStr === 'turn_end' || typeStr === 'agent_end') {
|
|
76
|
+
const msg = isRecord(o.message) ? o.message : null;
|
|
77
|
+
if (msg && Array.isArray(msg.content)) {
|
|
78
|
+
for (const block of msg.content as unknown[]) {
|
|
79
|
+
if (isRecord(block) && block.type === 'text' && typeof block.text === 'string') streamedText = block.text;
|
|
80
|
+
if (isRecord(block) && block.type === 'thinking' && typeof block.thinking === 'string')
|
|
81
|
+
activities.push({ kind: 'thinking', chars: (block.thinking as string).length });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (Array.isArray(o.messages)) {
|
|
85
|
+
const last = o.messages[o.messages.length - 1] as unknown;
|
|
86
|
+
if (isRecord(last) && Array.isArray(last.content)) {
|
|
87
|
+
for (const block of last.content as unknown[]) {
|
|
88
|
+
if (isRecord(block) && block.type === 'text' && typeof block.text === 'string' && !streamedText)
|
|
89
|
+
streamedText = block.text as string;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
56
94
|
const text = extractAmpText(o);
|
|
57
|
-
if (text && !typeStr.includes('tool')) streamedText = text;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
95
|
+
if (text && !typeStr.includes('tool') && !streamedText) streamedText = text;
|
|
96
|
+
if (
|
|
97
|
+
o.type === 'result' ||
|
|
98
|
+
o.type === 'done' ||
|
|
99
|
+
o.type === 'completed' ||
|
|
100
|
+
typeStr === 'turn_end' ||
|
|
101
|
+
typeStr === 'agent_end'
|
|
102
|
+
) {
|
|
103
|
+
const usage = isRecord(o.usage)
|
|
104
|
+
? o.usage
|
|
105
|
+
: isRecord(o.message) && isRecord((o.message as Record<string, unknown>).usage)
|
|
106
|
+
? ((o.message as Record<string, unknown>).usage as Record<string, unknown>)
|
|
107
|
+
: null;
|
|
108
|
+
const actualUsage = isRecord(usage)
|
|
109
|
+
? usage
|
|
110
|
+
: isRecord(o.message) && isRecord((o.message as Record<string, unknown>).usage)
|
|
111
|
+
? ((o.message as Record<string, unknown>).usage as Record<string, unknown>)
|
|
112
|
+
: null;
|
|
113
|
+
const msg = isRecord(o.message) ? o.message : null;
|
|
114
|
+
const latched = ((state as unknown as Record<string, unknown>)._harness as Record<string, unknown> | undefined)
|
|
115
|
+
?.sessionId as string | undefined;
|
|
116
|
+
const cost =
|
|
117
|
+
isRecord(actualUsage) && typeof (actualUsage as Record<string, unknown>).total === 'number'
|
|
118
|
+
? ((actualUsage as Record<string, unknown>).total as number)
|
|
119
|
+
: typeof o.total_cost_usd === 'number'
|
|
120
|
+
? o.total_cost_usd
|
|
121
|
+
: 0;
|
|
122
|
+
const inputTokens = isRecord(actualUsage)
|
|
123
|
+
? typeof (actualUsage as Record<string, unknown>).input === 'number'
|
|
124
|
+
? ((actualUsage as Record<string, unknown>).input as number)
|
|
125
|
+
: typeof (actualUsage as Record<string, unknown>).input_tokens === 'number'
|
|
126
|
+
? ((actualUsage as Record<string, unknown>).input_tokens as number)
|
|
127
|
+
: 0
|
|
128
|
+
: 0;
|
|
129
|
+
const outputTokens = isRecord(actualUsage)
|
|
130
|
+
? typeof (actualUsage as Record<string, unknown>).output === 'number'
|
|
131
|
+
? ((actualUsage as Record<string, unknown>).output as number)
|
|
132
|
+
: typeof (actualUsage as Record<string, unknown>).output_tokens === 'number'
|
|
133
|
+
? ((actualUsage as Record<string, unknown>).output_tokens as number)
|
|
134
|
+
: 0
|
|
135
|
+
: 0;
|
|
61
136
|
const result: StreamedResult = {
|
|
62
|
-
result:
|
|
137
|
+
result:
|
|
138
|
+
typeof o.result === 'string'
|
|
139
|
+
? o.result
|
|
140
|
+
: streamedText
|
|
141
|
+
? state.streamedText + streamedText
|
|
142
|
+
: state.streamedText || (text ?? ''),
|
|
63
143
|
isError: o.is_error === true,
|
|
64
|
-
numTurns:
|
|
65
|
-
totalCostUsd: typeof
|
|
66
|
-
sessionId: typeof o.session_id === 'string' ? o.session_id : null,
|
|
67
|
-
stopReason:
|
|
144
|
+
numTurns: 1,
|
|
145
|
+
totalCostUsd: typeof cost === 'number' ? cost : 0,
|
|
146
|
+
sessionId: typeof o.session_id === 'string' ? o.session_id : typeof o.id === 'string' ? o.id : (latched ?? null),
|
|
147
|
+
stopReason:
|
|
148
|
+
typeof o.stop_reason === 'string'
|
|
149
|
+
? o.stop_reason
|
|
150
|
+
: typeof msg?.stopReason === 'string'
|
|
151
|
+
? (msg.stopReason as string)
|
|
152
|
+
: null,
|
|
68
153
|
permissionDenials: [],
|
|
69
|
-
durationMs:
|
|
154
|
+
durationMs:
|
|
155
|
+
typeof o.duration_ms === 'number'
|
|
156
|
+
? o.duration_ms
|
|
157
|
+
: typeof (o as Record<string, unknown>).duration === 'number'
|
|
158
|
+
? ((o as Record<string, unknown>).duration as number)
|
|
159
|
+
: null,
|
|
70
160
|
durationApiMs: null,
|
|
71
|
-
ttftMs:
|
|
72
|
-
|
|
161
|
+
ttftMs:
|
|
162
|
+
typeof (o as Record<string, unknown>).ttft === 'number'
|
|
163
|
+
? ((o as Record<string, unknown>).ttft as number)
|
|
164
|
+
: null,
|
|
165
|
+
model: typeof o.model === 'string' ? o.model : typeof msg?.model === 'string' ? (msg.model as string) : null,
|
|
73
166
|
contextWindow: null,
|
|
74
167
|
maxOutputTokens: null,
|
|
75
|
-
usage:
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
168
|
+
usage:
|
|
169
|
+
actualUsage && (inputTokens || outputTokens)
|
|
170
|
+
? {
|
|
171
|
+
inputTokens,
|
|
172
|
+
outputTokens,
|
|
173
|
+
cacheCreationInputTokens: 0,
|
|
174
|
+
cacheReadInputTokens:
|
|
175
|
+
typeof (actualUsage as Record<string, unknown>).cacheRead === 'number'
|
|
176
|
+
? ((actualUsage as Record<string, unknown>).cacheRead as number)
|
|
177
|
+
: 0,
|
|
178
|
+
}
|
|
179
|
+
: null,
|
|
83
180
|
};
|
|
181
|
+
if (!result.result) result.result = state.streamedText + (streamedText ?? '');
|
|
84
182
|
return { activities, streamedText, result };
|
|
85
183
|
}
|
|
86
184
|
return { activities, streamedText };
|
|
87
185
|
}
|
|
88
|
-
|
|
89
186
|
export const ampHarness: Harness = {
|
|
90
187
|
name: 'amp',
|
|
91
188
|
displayName: 'Amp',
|
|
@@ -117,12 +214,14 @@ export const ampHarness: Harness = {
|
|
|
117
214
|
extractResult(state: ParseState): StreamedResult | null {
|
|
118
215
|
if (state.result) return state.result;
|
|
119
216
|
if (state.streamedText.trim().length > 0) {
|
|
217
|
+
const latched = ((state as unknown as Record<string, unknown>)._harness as Record<string, unknown> | undefined)
|
|
218
|
+
?.sessionId as string | undefined;
|
|
120
219
|
return {
|
|
121
220
|
result: state.streamedText,
|
|
122
221
|
isError: false,
|
|
123
222
|
numTurns: 1,
|
|
124
223
|
totalCostUsd: 0,
|
|
125
|
-
sessionId: null,
|
|
224
|
+
sessionId: latched ?? null,
|
|
126
225
|
stopReason: null,
|
|
127
226
|
permissionDenials: [],
|
|
128
227
|
durationMs: null,
|
|
@@ -136,9 +235,5 @@ export const ampHarness: Harness = {
|
|
|
136
235
|
}
|
|
137
236
|
return null;
|
|
138
237
|
},
|
|
139
|
-
permissionMap: {
|
|
140
|
-
readonly: ['read-only'],
|
|
141
|
-
edit: ['workspace'],
|
|
142
|
-
danger: ['danger'],
|
|
143
|
-
},
|
|
238
|
+
permissionMap: { readonly: ['read-only'], edit: ['workspace'], danger: ['danger'] },
|
|
144
239
|
};
|
|
@@ -10,23 +10,15 @@ import type {
|
|
|
10
10
|
} from './types.ts';
|
|
11
11
|
|
|
12
12
|
const execFileAsync = promisify(execFile);
|
|
13
|
-
|
|
14
13
|
function isRecord(v: unknown): v is Record<string, unknown> {
|
|
15
14
|
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
16
15
|
}
|
|
17
|
-
|
|
18
16
|
const SANDBOX_MAP: Record<NormalizedPermission, string> = {
|
|
19
17
|
readonly: 'read-only',
|
|
20
18
|
edit: 'workspace-write',
|
|
21
19
|
danger: 'danger-full-access',
|
|
22
20
|
};
|
|
23
|
-
|
|
24
|
-
function extractTextFromCodexEvent(o: Record<string, unknown>, state: ParseState): string | undefined {
|
|
25
|
-
// Codex JSON variants: try common shapes
|
|
26
|
-
// 1. {type:"item.completed", item:{type:"agent_message", text:"..."}}
|
|
27
|
-
// 2. {type:"thread.item.completed", item:{type:"agent_message", ...}}
|
|
28
|
-
// 3. {type:"event", event:{type:"response.output_text.delta", delta:"..."}}
|
|
29
|
-
// 4. Plain {"text":"..."} or {"output":"..."}
|
|
21
|
+
function extractTextFromCodexEvent(o: Record<string, unknown>, _state: ParseState): string | undefined {
|
|
30
22
|
if (typeof o.text === 'string' && o.type !== 'tool_use') return o.text;
|
|
31
23
|
if (typeof o.output === 'string') return o.output;
|
|
32
24
|
if (typeof o.delta === 'string') return o.delta;
|
|
@@ -35,31 +27,66 @@ function extractTextFromCodexEvent(o: Record<string, unknown>, state: ParseState
|
|
|
35
27
|
return (o.event as Record<string, unknown>).delta as string;
|
|
36
28
|
if (isRecord(o.item) && typeof o.item.text === 'string') return o.item.text;
|
|
37
29
|
if (isRecord(o.item) && typeof o.item.output === 'string') return o.item.output;
|
|
38
|
-
// agent_message
|
|
39
30
|
if (o.type === 'agent_message' && typeof o.text === 'string') return o.text;
|
|
31
|
+
if (o.type === 'error' && typeof o.message === 'string') return o.message;
|
|
32
|
+
if (typeof o.error === 'string') return o.error;
|
|
40
33
|
return undefined;
|
|
41
34
|
}
|
|
42
|
-
|
|
43
35
|
export function parseCodexLine(line: string, state: ParseState): ParseOutcome {
|
|
44
36
|
let o: unknown;
|
|
45
37
|
try {
|
|
46
38
|
o = JSON.parse(line);
|
|
47
39
|
} catch {
|
|
48
|
-
|
|
49
|
-
if (line.trim().length > 0) return { streamedText: line + '\n', activities: [] };
|
|
40
|
+
if (line.trim().length > 0) return { streamedText: `${line}\n`, activities: [] };
|
|
50
41
|
return { activities: [] };
|
|
51
42
|
}
|
|
52
43
|
if (!isRecord(o)) return { activities: [] };
|
|
53
|
-
|
|
54
44
|
const activities: ParseOutcome['activities'] = [];
|
|
55
45
|
let streamedText: string | undefined;
|
|
56
|
-
|
|
57
|
-
// Tool activity heuristics
|
|
58
|
-
// Codex may emit: {type:"item.started", item:{type:"tool_use", name:"...", input:{}}}
|
|
59
|
-
// or {type:"tool_use", name:"..."}
|
|
60
|
-
const item = isRecord(o.item) ? o.item : null;
|
|
61
46
|
const typeStr = typeof o.type === 'string' ? o.type : '';
|
|
62
|
-
|
|
47
|
+
const item = isRecord(o.item) ? o.item : null;
|
|
48
|
+
// latch thread_id from thread.started
|
|
49
|
+
if (typeStr === 'thread.started' && typeof o.thread_id === 'string') {
|
|
50
|
+
(state as unknown as Record<string, unknown>)._harness = {
|
|
51
|
+
...(((state as unknown as Record<string, unknown>)._harness as Record<string, unknown>) ?? {}),
|
|
52
|
+
sessionId: o.thread_id,
|
|
53
|
+
};
|
|
54
|
+
return { activities, streamedText };
|
|
55
|
+
}
|
|
56
|
+
if (typeStr === 'turn.started') return { activities, streamedText };
|
|
57
|
+
if (typeStr === 'error' && typeof o.message === 'string') {
|
|
58
|
+
streamedText = o.message;
|
|
59
|
+
}
|
|
60
|
+
if (typeof o.error === 'string' && !streamedText) streamedText = o.error;
|
|
61
|
+
if (typeStr === 'turn.failed') {
|
|
62
|
+
const msg =
|
|
63
|
+
isRecord(o.error) && typeof o.error.message === 'string'
|
|
64
|
+
? o.error.message
|
|
65
|
+
: typeof o.error === 'string'
|
|
66
|
+
? o.error
|
|
67
|
+
: typeof o.message === 'string'
|
|
68
|
+
? o.message
|
|
69
|
+
: state.streamedText + (streamedText ?? '');
|
|
70
|
+
const latched = ((state as unknown as Record<string, unknown>)._harness as Record<string, unknown> | undefined)
|
|
71
|
+
?.sessionId as string | undefined;
|
|
72
|
+
const result: StreamedResult = {
|
|
73
|
+
result: msg,
|
|
74
|
+
isError: true,
|
|
75
|
+
numTurns: 1,
|
|
76
|
+
totalCostUsd: 0,
|
|
77
|
+
sessionId: typeof o.thread_id === 'string' ? o.thread_id : (latched ?? null),
|
|
78
|
+
stopReason: 'error',
|
|
79
|
+
permissionDenials: [],
|
|
80
|
+
durationMs: null,
|
|
81
|
+
durationApiMs: null,
|
|
82
|
+
ttftMs: null,
|
|
83
|
+
model: null,
|
|
84
|
+
contextWindow: null,
|
|
85
|
+
maxOutputTokens: null,
|
|
86
|
+
usage: null,
|
|
87
|
+
};
|
|
88
|
+
return { activities, streamedText, result };
|
|
89
|
+
}
|
|
63
90
|
if (typeStr.includes('tool') || typeStr.includes('item.started') || typeStr.includes('function_call')) {
|
|
64
91
|
const toolName = (item && typeof item.name === 'string' ? item.name : typeof o.name === 'string' ? o.name : null) as
|
|
65
92
|
| string
|
|
@@ -68,38 +95,33 @@ export function parseCodexLine(line: string, state: ParseState): ParseOutcome {
|
|
|
68
95
|
if (typeStr.includes('started') || o.type === 'tool_use') {
|
|
69
96
|
activities.push({ kind: 'tool_start', name: toolName });
|
|
70
97
|
if (item && isRecord(item.input))
|
|
71
|
-
activities.push({
|
|
72
|
-
kind: 'tool_input',
|
|
73
|
-
name: toolName,
|
|
74
|
-
input: item.input as Record<string, unknown>,
|
|
75
|
-
});
|
|
98
|
+
activities.push({ kind: 'tool_input', name: toolName, input: item.input as Record<string, unknown> });
|
|
76
99
|
else if (isRecord(o.input))
|
|
77
100
|
activities.push({ kind: 'tool_input', name: toolName, input: o.input as Record<string, unknown> });
|
|
78
|
-
} else if (typeStr.includes('completed') || typeStr.includes('result'))
|
|
101
|
+
} else if (typeStr.includes('completed') || typeStr.includes('result'))
|
|
79
102
|
activities.push({ kind: 'tool_result', isError: o.is_error === true || o.error === true });
|
|
80
|
-
}
|
|
81
103
|
}
|
|
82
104
|
}
|
|
83
|
-
if (o.type === 'tool_result' || (typeStr === 'item.completed' && item?.type === 'tool_result'))
|
|
105
|
+
if (o.type === 'tool_result' || (typeStr === 'item.completed' && item?.type === 'tool_result'))
|
|
84
106
|
activities.push({ kind: 'tool_result', isError: (o as Record<string, unknown>).is_error === true });
|
|
85
|
-
}
|
|
86
107
|
if (typeStr.includes('thinking') || o.type === 'reasoning' || item?.type === 'reasoning') {
|
|
87
108
|
const thinkingText = extractTextFromCodexEvent(o, state);
|
|
88
109
|
if (thinkingText) activities.push({ kind: 'thinking', chars: thinkingText.length });
|
|
89
110
|
else activities.push({ kind: 'thinking', chars: 10 });
|
|
90
111
|
}
|
|
91
|
-
|
|
92
|
-
// Text extraction
|
|
93
112
|
const text = extractTextFromCodexEvent(o, state);
|
|
94
|
-
if (
|
|
95
|
-
|
|
113
|
+
if (
|
|
114
|
+
text &&
|
|
115
|
+
!typeStr.includes('tool') &&
|
|
116
|
+
!typeStr.includes('thinking') &&
|
|
117
|
+
typeStr !== 'error' &&
|
|
118
|
+
typeStr !== 'turn.failed'
|
|
119
|
+
)
|
|
96
120
|
streamedText = text;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Result detection: final result often {type:"result", result:"...", total_cost_usd, usage, ...}
|
|
100
|
-
// or {type:"thread.completed", ...}
|
|
101
121
|
if (o.type === 'result' || o.type === 'thread.completed' || o.type === 'task.completed') {
|
|
102
122
|
const usage = isRecord(o.usage) ? o.usage : null;
|
|
123
|
+
const latched = ((state as unknown as Record<string, unknown>)._harness as Record<string, unknown> | undefined)
|
|
124
|
+
?.sessionId as string | undefined;
|
|
103
125
|
const result: StreamedResult = {
|
|
104
126
|
result:
|
|
105
127
|
typeof o.result === 'string'
|
|
@@ -117,7 +139,7 @@ export function parseCodexLine(line: string, state: ParseState): ParseOutcome {
|
|
|
117
139
|
? o.thread_id
|
|
118
140
|
: typeof o.id === 'string'
|
|
119
141
|
? o.id
|
|
120
|
-
: null,
|
|
142
|
+
: (latched ?? null),
|
|
121
143
|
stopReason: typeof o.stop_reason === 'string' ? o.stop_reason : null,
|
|
122
144
|
permissionDenials: Array.isArray(o.permission_denials) ? o.permission_denials : [],
|
|
123
145
|
durationMs: typeof o.duration_ms === 'number' ? o.duration_ms : null,
|
|
@@ -153,10 +175,8 @@ export function parseCodexLine(line: string, state: ParseState): ParseOutcome {
|
|
|
153
175
|
};
|
|
154
176
|
return { activities, streamedText, result };
|
|
155
177
|
}
|
|
156
|
-
|
|
157
178
|
return { activities, streamedText };
|
|
158
179
|
}
|
|
159
|
-
|
|
160
180
|
export const codexHarness: Harness = {
|
|
161
181
|
name: 'codex',
|
|
162
182
|
displayName: 'Muse',
|
|
@@ -170,7 +190,6 @@ export const codexHarness: Harness = {
|
|
|
170
190
|
}
|
|
171
191
|
},
|
|
172
192
|
buildArgs(opts: BuildArgsOpts): string[] {
|
|
173
|
-
// codex exec --json <prompt> --sandbox <level> --ask-for-approval <level>
|
|
174
193
|
const sandbox = opts.nativePermission ?? SANDBOX_MAP[opts.permission] ?? 'workspace-write';
|
|
175
194
|
const args = ['exec', '--json', opts.prompt, '--sandbox', sandbox];
|
|
176
195
|
if (opts.permission === 'danger' || sandbox === 'danger-full-access') args.push('--ask-for-approval', 'never');
|
|
@@ -178,7 +197,6 @@ export const codexHarness: Harness = {
|
|
|
178
197
|
else args.push('--ask-for-approval', 'on-request');
|
|
179
198
|
if (opts.model) args.push('--model', opts.model);
|
|
180
199
|
if (opts.resumeSessionId) args.push('--thread-id', opts.resumeSessionId);
|
|
181
|
-
// maxBudgetUsd not natively supported; pass as env hint via --config if needed
|
|
182
200
|
for (const dir of opts.addDirs ?? []) args.push('--add-dir', dir);
|
|
183
201
|
return args;
|
|
184
202
|
},
|
|
@@ -187,14 +205,15 @@ export const codexHarness: Harness = {
|
|
|
187
205
|
},
|
|
188
206
|
extractResult(state: ParseState): StreamedResult | null {
|
|
189
207
|
if (state.result) return state.result;
|
|
190
|
-
// Fallback: if no explicit result, synthesize from streamed text if any
|
|
191
208
|
if (state.streamedText.trim().length > 0) {
|
|
209
|
+
const latched = ((state as unknown as Record<string, unknown>)._harness as Record<string, unknown> | undefined)
|
|
210
|
+
?.sessionId as string | undefined;
|
|
192
211
|
return {
|
|
193
212
|
result: state.streamedText,
|
|
194
213
|
isError: false,
|
|
195
214
|
numTurns: 1,
|
|
196
215
|
totalCostUsd: 0,
|
|
197
|
-
sessionId: null,
|
|
216
|
+
sessionId: latched ?? null,
|
|
198
217
|
stopReason: null,
|
|
199
218
|
permissionDenials: [],
|
|
200
219
|
durationMs: null,
|
|
@@ -208,9 +227,5 @@ export const codexHarness: Harness = {
|
|
|
208
227
|
}
|
|
209
228
|
return null;
|
|
210
229
|
},
|
|
211
|
-
permissionMap: {
|
|
212
|
-
readonly: ['read-only'],
|
|
213
|
-
edit: ['workspace-write'],
|
|
214
|
-
danger: ['danger-full-access'],
|
|
215
|
-
},
|
|
230
|
+
permissionMap: { readonly: ['read-only'], edit: ['workspace-write'], danger: ['danger-full-access'] },
|
|
216
231
|
};
|
|
@@ -10,40 +10,53 @@ import type {
|
|
|
10
10
|
} from './types.ts';
|
|
11
11
|
|
|
12
12
|
const execFileAsync = promisify(execFile);
|
|
13
|
-
|
|
14
13
|
function isRecord(v: unknown): v is Record<string, unknown> {
|
|
15
14
|
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
16
15
|
}
|
|
17
|
-
|
|
18
16
|
const PERMISSION_MAP: Record<NormalizedPermission, string> = {
|
|
19
17
|
readonly: 'read-only',
|
|
20
18
|
edit: 'allow-edit',
|
|
21
19
|
danger: 'danger',
|
|
22
20
|
};
|
|
23
|
-
|
|
24
21
|
function extractOpencodeText(o: Record<string, unknown>): string | undefined {
|
|
25
22
|
if (typeof o.text === 'string') return o.text;
|
|
26
23
|
if (typeof o.output === 'string') return o.output;
|
|
27
24
|
if (typeof o.delta === 'string') return o.delta;
|
|
28
25
|
if (typeof o.content === 'string') return o.content;
|
|
26
|
+
if (isRecord(o.part) && typeof o.part.text === 'string') return o.part.text;
|
|
29
27
|
if (isRecord(o.event) && typeof o.event.text === 'string') return o.event.text;
|
|
30
28
|
if (isRecord(o.message) && typeof o.message.text === 'string') return o.message.text;
|
|
31
29
|
return undefined;
|
|
32
30
|
}
|
|
33
|
-
|
|
34
31
|
export function parseOpencodeLine(line: string, state: ParseState): ParseOutcome {
|
|
35
32
|
let o: unknown;
|
|
36
33
|
try {
|
|
37
34
|
o = JSON.parse(line);
|
|
38
35
|
} catch {
|
|
39
|
-
if (line.trim().length > 0) return { streamedText: line
|
|
36
|
+
if (line.trim().length > 0) return { streamedText: `${line}\n`, activities: [] };
|
|
40
37
|
return { activities: [] };
|
|
41
38
|
}
|
|
42
39
|
if (!isRecord(o)) return { activities: [] };
|
|
43
40
|
const activities: ParseOutcome['activities'] = [];
|
|
44
41
|
let streamedText: string | undefined;
|
|
45
42
|
const typeStr = typeof o.type === 'string' ? o.type : '';
|
|
46
|
-
|
|
43
|
+
// latch sessionID from step_start
|
|
44
|
+
if (typeStr === 'step_start' && typeof o.sessionID === 'string') {
|
|
45
|
+
(state as unknown as Record<string, unknown>)._harness = {
|
|
46
|
+
...(((state as unknown as Record<string, unknown>)._harness as Record<string, unknown>) ?? {}),
|
|
47
|
+
sessionId: o.sessionID,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (
|
|
51
|
+
isRecord(o.part) &&
|
|
52
|
+
typeof (o.part as Record<string, unknown>).sessionID === 'string' &&
|
|
53
|
+
!((state as unknown as Record<string, unknown>)._harness as Record<string, unknown> | undefined)?.sessionId
|
|
54
|
+
) {
|
|
55
|
+
(state as unknown as Record<string, unknown>)._harness = {
|
|
56
|
+
...(((state as unknown as Record<string, unknown>)._harness as Record<string, unknown>) ?? {}),
|
|
57
|
+
sessionId: (o.part as Record<string, unknown>).sessionID as string,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
47
60
|
if (typeStr.includes('tool') || o.type === 'tool_use' || o.type === 'tool_result') {
|
|
48
61
|
const name =
|
|
49
62
|
typeof o.name === 'string'
|
|
@@ -54,19 +67,59 @@ export function parseOpencodeLine(line: string, state: ParseState): ParseOutcome
|
|
|
54
67
|
if (typeStr.includes('start') || o.type === 'tool_use') {
|
|
55
68
|
activities.push({ kind: 'tool_start', name });
|
|
56
69
|
if (isRecord(o.input)) activities.push({ kind: 'tool_input', name, input: o.input as Record<string, unknown> });
|
|
57
|
-
} else if (typeStr.includes('result') || typeStr.includes('completed') || o.type === 'tool_result')
|
|
70
|
+
} else if (typeStr.includes('result') || typeStr.includes('completed') || o.type === 'tool_result')
|
|
58
71
|
activities.push({ kind: 'tool_result', isError: o.is_error === true || o.error === true });
|
|
59
|
-
}
|
|
60
72
|
}
|
|
61
|
-
if (typeStr.includes('thinking') || o.type === 'reasoning') {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
73
|
+
if (typeStr.includes('thinking') || o.type === 'reasoning') activities.push({ kind: 'thinking', chars: 10 });
|
|
74
|
+
const directText = extractOpencodeText(o);
|
|
75
|
+
if (directText && !typeStr.includes('tool') && !typeStr.includes('thinking')) streamedText = directText;
|
|
76
|
+
if (o.type === 'text' && isRecord(o.part) && typeof o.part.text === 'string') streamedText = o.part.text;
|
|
77
|
+
if (o.type === 'result' || o.type === 'completed' || o.type === 'done' || o.type === 'step_finish') {
|
|
78
|
+
const part = isRecord(o.part) ? (o.part as Record<string, unknown>) : null;
|
|
79
|
+
const usageRaw = isRecord(o.usage)
|
|
80
|
+
? o.usage
|
|
81
|
+
: part && isRecord((part as Record<string, unknown>).tokens)
|
|
82
|
+
? ((part as Record<string, unknown>).tokens as Record<string, unknown>)
|
|
83
|
+
: isRecord(o.part) && isRecord((o.part as Record<string, unknown>).tokens)
|
|
84
|
+
? ((o.part as Record<string, unknown>).tokens as Record<string, unknown>)
|
|
85
|
+
: null;
|
|
86
|
+
const usage = isRecord(usageRaw) ? usageRaw : null;
|
|
87
|
+
const tokensInput = isRecord(usage)
|
|
88
|
+
? typeof usage.input === 'number'
|
|
89
|
+
? usage.input
|
|
90
|
+
: typeof usage.input_tokens === 'number'
|
|
91
|
+
? usage.input_tokens
|
|
92
|
+
: 0
|
|
93
|
+
: 0;
|
|
94
|
+
const tokensOutput = isRecord(usage)
|
|
95
|
+
? typeof usage.output === 'number'
|
|
96
|
+
? usage.output
|
|
97
|
+
: typeof usage.output_tokens === 'number'
|
|
98
|
+
? usage.output_tokens
|
|
99
|
+
: 0
|
|
100
|
+
: 0;
|
|
101
|
+
const cost =
|
|
102
|
+
typeof o.total_cost_usd === 'number'
|
|
103
|
+
? o.total_cost_usd
|
|
104
|
+
: part && typeof (part as Record<string, unknown>).cost === 'number'
|
|
105
|
+
? ((part as Record<string, unknown>).cost as number)
|
|
106
|
+
: typeof o.cost === 'number'
|
|
107
|
+
? (o.cost as number)
|
|
108
|
+
: 0;
|
|
109
|
+
const latched = ((state as unknown as Record<string, unknown>)._harness as Record<string, unknown> | undefined)
|
|
110
|
+
?.sessionId as string | undefined;
|
|
111
|
+
const sessionId =
|
|
112
|
+
typeof o.session_id === 'string'
|
|
113
|
+
? o.session_id
|
|
114
|
+
: typeof o.sessionID === 'string'
|
|
115
|
+
? o.sessionID
|
|
116
|
+
: typeof (o as Record<string, unknown>).sessionID === 'string'
|
|
117
|
+
? ((o as Record<string, unknown>).sessionID as string)
|
|
118
|
+
: part && typeof (part as Record<string, unknown>).sessionID === 'string'
|
|
119
|
+
? ((part as Record<string, unknown>).sessionID as string)
|
|
120
|
+
: typeof o.id === 'string'
|
|
121
|
+
? o.id
|
|
122
|
+
: (latched ?? null);
|
|
70
123
|
const result: StreamedResult = {
|
|
71
124
|
result:
|
|
72
125
|
typeof o.result === 'string'
|
|
@@ -76,9 +129,14 @@ export function parseOpencodeLine(line: string, state: ParseState): ParseOutcome
|
|
|
76
129
|
: state.streamedText + (streamedText ?? ''),
|
|
77
130
|
isError: o.is_error === true,
|
|
78
131
|
numTurns: typeof o.num_turns === 'number' ? o.num_turns : 0,
|
|
79
|
-
totalCostUsd:
|
|
80
|
-
sessionId
|
|
81
|
-
stopReason:
|
|
132
|
+
totalCostUsd: cost,
|
|
133
|
+
sessionId,
|
|
134
|
+
stopReason:
|
|
135
|
+
typeof o.stop_reason === 'string'
|
|
136
|
+
? o.stop_reason
|
|
137
|
+
: part && typeof (part as Record<string, unknown>).reason === 'string'
|
|
138
|
+
? ((part as Record<string, unknown>).reason as string)
|
|
139
|
+
: null,
|
|
82
140
|
permissionDenials: [],
|
|
83
141
|
durationMs: typeof o.duration_ms === 'number' ? o.duration_ms : null,
|
|
84
142
|
durationApiMs: null,
|
|
@@ -87,19 +145,13 @@ export function parseOpencodeLine(line: string, state: ParseState): ParseOutcome
|
|
|
87
145
|
contextWindow: null,
|
|
88
146
|
maxOutputTokens: null,
|
|
89
147
|
usage: usage
|
|
90
|
-
? {
|
|
91
|
-
inputTokens: typeof usage.input_tokens === 'number' ? usage.input_tokens : 0,
|
|
92
|
-
outputTokens: typeof usage.output_tokens === 'number' ? usage.output_tokens : 0,
|
|
93
|
-
cacheCreationInputTokens: 0,
|
|
94
|
-
cacheReadInputTokens: 0,
|
|
95
|
-
}
|
|
148
|
+
? { inputTokens: tokensInput, outputTokens: tokensOutput, cacheCreationInputTokens: 0, cacheReadInputTokens: 0 }
|
|
96
149
|
: null,
|
|
97
150
|
};
|
|
98
151
|
return { activities, streamedText, result };
|
|
99
152
|
}
|
|
100
153
|
return { activities, streamedText };
|
|
101
154
|
}
|
|
102
|
-
|
|
103
155
|
export const opencodeHarness: Harness = {
|
|
104
156
|
name: 'opencode',
|
|
105
157
|
displayName: 'OpenCode',
|
|
@@ -113,10 +165,8 @@ export const opencodeHarness: Harness = {
|
|
|
113
165
|
}
|
|
114
166
|
},
|
|
115
167
|
buildArgs(opts: BuildArgsOpts): string[] {
|
|
116
|
-
const args = ['run', '--format', 'json', opts.prompt];
|
|
117
|
-
// permission not fully standardized; pass as --permission if supported
|
|
118
168
|
const perm = opts.nativePermission ?? PERMISSION_MAP[opts.permission] ?? 'allow-edit';
|
|
119
|
-
args.
|
|
169
|
+
const args = ['run', '--format', 'json', opts.prompt, '--permission', perm];
|
|
120
170
|
if (opts.model) args.push('--model', opts.model);
|
|
121
171
|
if (opts.resumeSessionId) args.push('--session', opts.resumeSessionId);
|
|
122
172
|
for (const dir of opts.addDirs ?? []) args.push('--add-dir', dir);
|
|
@@ -128,12 +178,14 @@ export const opencodeHarness: Harness = {
|
|
|
128
178
|
extractResult(state: ParseState): StreamedResult | null {
|
|
129
179
|
if (state.result) return state.result;
|
|
130
180
|
if (state.streamedText.trim().length > 0) {
|
|
181
|
+
const latched = ((state as unknown as Record<string, unknown>)._harness as Record<string, unknown> | undefined)
|
|
182
|
+
?.sessionId as string | undefined;
|
|
131
183
|
return {
|
|
132
184
|
result: state.streamedText,
|
|
133
185
|
isError: false,
|
|
134
186
|
numTurns: 1,
|
|
135
187
|
totalCostUsd: 0,
|
|
136
|
-
sessionId: null,
|
|
188
|
+
sessionId: latched ?? null,
|
|
137
189
|
stopReason: null,
|
|
138
190
|
permissionDenials: [],
|
|
139
191
|
durationMs: null,
|
|
@@ -147,9 +199,5 @@ export const opencodeHarness: Harness = {
|
|
|
147
199
|
}
|
|
148
200
|
return null;
|
|
149
201
|
},
|
|
150
|
-
permissionMap: {
|
|
151
|
-
readonly: ['read-only'],
|
|
152
|
-
edit: ['allow-edit'],
|
|
153
|
-
danger: ['danger'],
|
|
154
|
-
},
|
|
202
|
+
permissionMap: { readonly: ['read-only'], edit: ['allow-edit'], danger: ['danger'] },
|
|
155
203
|
};
|
package/extensions/index.ts
CHANGED
|
@@ -451,10 +451,11 @@ async function delegate(
|
|
|
451
451
|
throw new Error('another delegate run is already in progress (global limit)');
|
|
452
452
|
// per-harness limit if configured as object
|
|
453
453
|
const perHarnessLimit = (() => {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
return
|
|
454
|
+
const mc = config.maxConcurrent as unknown as { perHarness?: Record<string, number> };
|
|
455
|
+
if (mc && typeof mc === 'object' && mc.perHarness && typeof mc.perHarness[harnessName] === 'number') {
|
|
456
|
+
const v = mc.perHarness[harnessName];
|
|
457
|
+
if (typeof v === 'number') return v;
|
|
458
|
+
}
|
|
458
459
|
return maxGlobal;
|
|
459
460
|
})();
|
|
460
461
|
if (perHarnessLimit > 0 && perHarnessCount >= perHarnessLimit)
|
|
@@ -1034,7 +1035,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1034
1035
|
} else if (ev.kind === 'tool_result') {
|
|
1035
1036
|
if (chipActivity.startsWith('▶')) chipActivity += ev.isError ? ' ✗' : ' ✓';
|
|
1036
1037
|
const last = feed.length - 1;
|
|
1037
|
-
if (last >= 0 && feed[last].kind === 'tool') feed[last] = { ...feed[last], ok: ev.isError
|
|
1038
|
+
if (last >= 0 && feed[last].kind === 'tool') feed[last] = { ...feed[last], ok: !ev.isError };
|
|
1038
1039
|
} else if (ev.kind === 'thinking') {
|
|
1039
1040
|
chipActivity = '💭 thinking…';
|
|
1040
1041
|
thinkingChars += ev.chars;
|
package/extensions/run-claude.ts
CHANGED
|
@@ -32,7 +32,7 @@ function mapPerm(mode: string): 'readonly' | 'edit' | 'danger' {
|
|
|
32
32
|
|
|
33
33
|
export function runClaude(opts: RunClaudeOptions): Promise<ClaudeResult> {
|
|
34
34
|
const perm = mapPerm(opts.permissionMode);
|
|
35
|
-
const
|
|
35
|
+
const _nativePerm =
|
|
36
36
|
perm === 'readonly' && opts.permissionMode !== 'plan'
|
|
37
37
|
? opts.permissionMode
|
|
38
38
|
: perm === 'danger' && opts.permissionMode !== 'bypassPermissions'
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-harness-delegate",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Delegate work to any harness (Claude Code, Muse, OpenCode, Amp) from the pi coding agent \u2014 code reviews, plans, implementation, security audits, docs, or your own custom templates.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "bun@1.3.14",
|
|
7
7
|
"engines": {
|
|
8
|
-
"node": "26
|
|
8
|
+
"node": "22 || 24 || 26"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"extensions",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"@earendil-works/pi-ai": "0.84.3",
|
|
59
59
|
"@earendil-works/pi-coding-agent": "0.84.3",
|
|
60
60
|
"@earendil-works/pi-tui": "0.84.3",
|
|
61
|
-
"@types/node": "
|
|
61
|
+
"@types/node": "22.15.32",
|
|
62
62
|
"typebox": "1.3.18",
|
|
63
63
|
"typescript": "7.0.2"
|
|
64
64
|
},
|