@plimeor/harness 0.1.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 +221 -0
- package/package.json +39 -0
- package/src/adapters/claude.ts +112 -0
- package/src/adapters/codex.ts +118 -0
- package/src/adapters/extensions.ts +1235 -0
- package/src/adapters/index.ts +9 -0
- package/src/adapters/kiro.ts +57 -0
- package/src/adapters/pi.ts +65 -0
- package/src/adapters/shared.ts +338 -0
- package/src/errors.ts +43 -0
- package/src/index.ts +34 -0
- package/src/output.ts +183 -0
- package/src/process.ts +183 -0
- package/src/registry.ts +34 -0
- package/src/schema.ts +18 -0
- package/src/types.ts +166 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { harness } from '../registry'
|
|
2
|
+
import type { HarnessContext, RunOutputRequest, RunRequest, TextOutputRequest } from '../types'
|
|
3
|
+
import { configDirectory, createExtensionFacet } from './extensions'
|
|
4
|
+
import { createBuiltInAdapter, planTextCommand, unsupportedOutputMode } from './shared'
|
|
5
|
+
|
|
6
|
+
const HARNESS_ID = 'kiro'
|
|
7
|
+
|
|
8
|
+
export const kiroAdapter = createBuiltInAdapter({
|
|
9
|
+
commands: ['kiro-cli'],
|
|
10
|
+
id: HARNESS_ID,
|
|
11
|
+
identity: /kiro/i,
|
|
12
|
+
installHint: 'Install Kiro CLI and ensure `kiro-cli --version` is available on PATH.',
|
|
13
|
+
extensions(context: HarnessContext | undefined) {
|
|
14
|
+
const directory = context?.env?.KIRO_HOME ?? configDirectory(context?.home, '.kiro')
|
|
15
|
+
return createExtensionFacet({
|
|
16
|
+
configDirectory: directory,
|
|
17
|
+
context,
|
|
18
|
+
harnessId: HARNESS_ID,
|
|
19
|
+
mcp: { configFile: `${directory}/settings/mcp.json`, kind: 'kiro-cli' },
|
|
20
|
+
skillsDirectory: `${directory}/skills`,
|
|
21
|
+
hooks: {
|
|
22
|
+
hooksDirectory: `${directory}/hooks`,
|
|
23
|
+
kind: 'kiro-hook-files',
|
|
24
|
+
events: [
|
|
25
|
+
'Manual',
|
|
26
|
+
'PostFileCreate',
|
|
27
|
+
'PostFileDelete',
|
|
28
|
+
'PostFileSave',
|
|
29
|
+
'PostTaskExec',
|
|
30
|
+
'PostToolUse',
|
|
31
|
+
'PreTaskExec',
|
|
32
|
+
'PreToolUse',
|
|
33
|
+
'SessionStart',
|
|
34
|
+
'Stop',
|
|
35
|
+
'UserPromptSubmit'
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
},
|
|
40
|
+
plan(request: RunRequest<RunOutputRequest>, command: string, cwd: string) {
|
|
41
|
+
const output = request.output ?? ({ mode: 'text' } satisfies TextOutputRequest)
|
|
42
|
+
if (!output.mode || output.mode === 'text') {
|
|
43
|
+
return planTextCommand({
|
|
44
|
+
args: ['chat', '--no-interactive', '--wrap', 'never', request.prompt],
|
|
45
|
+
command,
|
|
46
|
+
cwd,
|
|
47
|
+
harnessId: HARNESS_ID,
|
|
48
|
+
output: { mode: 'text' },
|
|
49
|
+
request
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return unsupportedOutputMode(HARNESS_ID, output)
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
harness.use(kiroAdapter)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { harness } from '../registry'
|
|
2
|
+
import type { HarnessContext, JsonlOutputRequest, RunOutputRequest, RunRequest, TextOutputRequest } from '../types'
|
|
3
|
+
import { configDirectory, createExtensionFacet } from './extensions'
|
|
4
|
+
import { createBuiltInAdapter, planCommand, shellQuote, unsupportedOutputMode } from './shared'
|
|
5
|
+
|
|
6
|
+
const HARNESS_ID = 'pi'
|
|
7
|
+
|
|
8
|
+
export const piAdapter = createBuiltInAdapter({
|
|
9
|
+
commands: ['pi'],
|
|
10
|
+
id: HARNESS_ID,
|
|
11
|
+
identity: /^\d+\.\d+\.\d+/,
|
|
12
|
+
installHint: 'Install pi and ensure `pi --version` is available on PATH.',
|
|
13
|
+
extensions(context: HarnessContext | undefined) {
|
|
14
|
+
const directory = configDirectory(context?.home, '.pi/agent')
|
|
15
|
+
return createExtensionFacet({
|
|
16
|
+
configDirectory: directory,
|
|
17
|
+
context,
|
|
18
|
+
harnessId: HARNESS_ID,
|
|
19
|
+
skillsDirectory: `${directory}/skills`
|
|
20
|
+
})
|
|
21
|
+
},
|
|
22
|
+
plan(request: RunRequest<RunOutputRequest>, command: string, cwd: string) {
|
|
23
|
+
const output = request.output ?? ({ mode: 'text' } satisfies TextOutputRequest)
|
|
24
|
+
if (output.mode === 'jsonl') {
|
|
25
|
+
return planCommand({
|
|
26
|
+
args: ['-lc', piCommand(command, request.prompt, 'json')],
|
|
27
|
+
command: 'fish',
|
|
28
|
+
cwd,
|
|
29
|
+
harnessId: HARNESS_ID,
|
|
30
|
+
output: output satisfies JsonlOutputRequest,
|
|
31
|
+
request
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!output.mode || output.mode === 'text') {
|
|
36
|
+
return planCommand({
|
|
37
|
+
args: ['-lc', piCommand(command, request.prompt, 'text')],
|
|
38
|
+
command: 'fish',
|
|
39
|
+
cwd,
|
|
40
|
+
harnessId: HARNESS_ID,
|
|
41
|
+
output: { mode: 'text' },
|
|
42
|
+
request
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return unsupportedOutputMode(HARNESS_ID, output)
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
harness.use(piAdapter)
|
|
51
|
+
|
|
52
|
+
function piCommand(command: string, prompt: string, mode: 'json' | 'text'): string {
|
|
53
|
+
return [
|
|
54
|
+
shellQuote(command),
|
|
55
|
+
'-p',
|
|
56
|
+
'--no-session',
|
|
57
|
+
'--no-tools',
|
|
58
|
+
'--no-extensions',
|
|
59
|
+
'--no-skills',
|
|
60
|
+
'--no-context-files',
|
|
61
|
+
'--mode',
|
|
62
|
+
mode,
|
|
63
|
+
shellQuote(prompt)
|
|
64
|
+
].join(' ')
|
|
65
|
+
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { access } from 'node:fs/promises'
|
|
2
|
+
import { delimiter, join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { HarnessPlanError } from '../errors'
|
|
5
|
+
import { createProcessRunner } from '../process'
|
|
6
|
+
import type {
|
|
7
|
+
CommandPlan,
|
|
8
|
+
ExtensionFacet,
|
|
9
|
+
HarnessAdapter,
|
|
10
|
+
HarnessContext,
|
|
11
|
+
HarnessDetection,
|
|
12
|
+
HarnessHandle,
|
|
13
|
+
HarnessId,
|
|
14
|
+
HarnessRun,
|
|
15
|
+
HealthReport,
|
|
16
|
+
JsonlOutputRequest,
|
|
17
|
+
ProcessFacet,
|
|
18
|
+
RunOutputRequest,
|
|
19
|
+
RunRequest,
|
|
20
|
+
StructuredOutputRequest,
|
|
21
|
+
TextOutputRequest
|
|
22
|
+
} from '../types'
|
|
23
|
+
|
|
24
|
+
type BuiltInAdapterConfig = {
|
|
25
|
+
id: Extract<HarnessId, 'claude' | 'codex' | 'kiro' | 'pi'>
|
|
26
|
+
commands: string[]
|
|
27
|
+
identity: RegExp
|
|
28
|
+
identityArgs?: string[]
|
|
29
|
+
installHint: string
|
|
30
|
+
requiresGoogleAccessBeforeSmoke?: boolean
|
|
31
|
+
extensions(context: HarnessContext | undefined): ExtensionFacet
|
|
32
|
+
plan(request: RunRequest<RunOutputRequest>, command: string, cwd: string): CommandPlan<RunOutputRequest>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const HEALTH_PROMPT_TIMEOUT_MS = 30_000
|
|
36
|
+
const GOOGLE_ACCESS_TIMEOUT_MS = 5_000
|
|
37
|
+
const GOOGLE_ACCESS_URL = 'https://www.google.com/generate_204'
|
|
38
|
+
|
|
39
|
+
export function createBuiltInAdapter(config: BuiltInAdapterConfig): HarnessAdapter {
|
|
40
|
+
return {
|
|
41
|
+
id: config.id,
|
|
42
|
+
async detect(context?: HarnessContext): Promise<HarnessDetection> {
|
|
43
|
+
return detectCommand(config, context)
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
async open(context?: HarnessContext): Promise<HarnessHandle> {
|
|
47
|
+
const detection = await detectCommand(config, context)
|
|
48
|
+
const runner = createProcessRunner(config.id)
|
|
49
|
+
const process: ProcessFacet = {
|
|
50
|
+
async plan<Output extends RunOutputRequest = TextOutputRequest>(
|
|
51
|
+
request: RunRequest<Output>
|
|
52
|
+
): Promise<CommandPlan<Output>> {
|
|
53
|
+
if (!detection.detected || !detection.binary) {
|
|
54
|
+
throw new HarnessPlanError({
|
|
55
|
+
harnessId: config.id,
|
|
56
|
+
kind: 'unsupported_operation',
|
|
57
|
+
message: `${config.id} CLI is not installed or was not recognized.`
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return config.plan(request, detection.binary.command, resolveCwd(context)) as CommandPlan<Output>
|
|
62
|
+
},
|
|
63
|
+
async run<Output extends RunOutputRequest = TextOutputRequest>(
|
|
64
|
+
input: RunRequest<Output> | CommandPlan<Output>
|
|
65
|
+
): Promise<HarnessRun<Output>> {
|
|
66
|
+
if (isCommandPlan(input)) {
|
|
67
|
+
return runner.run(input)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const plan = await process.plan(input)
|
|
71
|
+
return runner.run(plan)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
detection,
|
|
77
|
+
extensions: config.extensions(context),
|
|
78
|
+
health: {
|
|
79
|
+
async check(): Promise<HealthReport> {
|
|
80
|
+
if (!detection.detected || !detection.binary) {
|
|
81
|
+
return {
|
|
82
|
+
message: `${config.id} CLI is not installed or was not recognized. ${config.installHint}`,
|
|
83
|
+
success: false
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (config.requiresGoogleAccessBeforeSmoke) {
|
|
88
|
+
const googleAccess = await checkGoogleAccess(config.id)
|
|
89
|
+
if (!googleAccess.success) {
|
|
90
|
+
return googleAccess
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const plan = config.plan(
|
|
95
|
+
{
|
|
96
|
+
output: { mode: 'text' },
|
|
97
|
+
prompt: 'Reply with OK.',
|
|
98
|
+
timeoutMs: HEALTH_PROMPT_TIMEOUT_MS
|
|
99
|
+
},
|
|
100
|
+
detection.binary.command,
|
|
101
|
+
resolveCwd(context)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const run = await runner.run(plan)
|
|
106
|
+
const result = await run.result
|
|
107
|
+
if (result.finalText.trim().length > 0) {
|
|
108
|
+
return { success: true }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
message: `${config.id} CLI did not produce output for the health prompt within ${HEALTH_PROMPT_TIMEOUT_MS}ms.`,
|
|
113
|
+
success: false
|
|
114
|
+
}
|
|
115
|
+
} catch (error) {
|
|
116
|
+
return {
|
|
117
|
+
message: `${config.id} CLI did not respond to the health prompt within ${HEALTH_PROMPT_TIMEOUT_MS}ms: ${formatError(error)}`,
|
|
118
|
+
success: false
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
process
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function checkGoogleAccess(harnessId: HarnessId): Promise<HealthReport> {
|
|
130
|
+
const controller = new AbortController()
|
|
131
|
+
const timeout = setTimeout(() => {
|
|
132
|
+
controller.abort()
|
|
133
|
+
}, GOOGLE_ACCESS_TIMEOUT_MS)
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const response = await fetch(GOOGLE_ACCESS_URL, {
|
|
137
|
+
method: 'GET',
|
|
138
|
+
signal: controller.signal
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
if (response.ok) {
|
|
142
|
+
return { success: true }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
message: `${harnessId} CLI smoke prompt was skipped because google.com is not reachable. ${GOOGLE_ACCESS_URL} returned HTTP ${response.status}. Check local network access to google.com.`,
|
|
147
|
+
success: false
|
|
148
|
+
}
|
|
149
|
+
} catch (error) {
|
|
150
|
+
return {
|
|
151
|
+
message: `${harnessId} CLI smoke prompt was skipped because google.com is not reachable within ${GOOGLE_ACCESS_TIMEOUT_MS}ms. Check local network access to google.com: ${formatError(error)}`,
|
|
152
|
+
success: false
|
|
153
|
+
}
|
|
154
|
+
} finally {
|
|
155
|
+
clearTimeout(timeout)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function isCommandPlan<Output extends RunOutputRequest>(
|
|
160
|
+
input: RunRequest<Output> | CommandPlan<Output>
|
|
161
|
+
): input is CommandPlan<Output> {
|
|
162
|
+
return 'args' in input && 'command' in input && 'harnessId' in input
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function unsupportedOutputMode(harnessId: HarnessId, output: RunOutputRequest): never {
|
|
166
|
+
throw new HarnessPlanError({
|
|
167
|
+
harnessId,
|
|
168
|
+
kind: 'unsupported_output_mode',
|
|
169
|
+
message: unsupportedOutputModeMessage(harnessId, output)
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function unsupportedOutputModeMessage(harnessId: HarnessId, output: RunOutputRequest): string {
|
|
174
|
+
const mode = output.mode ?? 'text'
|
|
175
|
+
const prefix = `${harnessId} adapter does not support ${mode} output mode.`
|
|
176
|
+
|
|
177
|
+
if (mode === 'structured') {
|
|
178
|
+
return [
|
|
179
|
+
prefix,
|
|
180
|
+
'The current model path does not support structured output for this harness.',
|
|
181
|
+
'Use text output and put the required JSON shape, field names, constraints, and validation rules directly in the prompt.'
|
|
182
|
+
].join(' ')
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (mode === 'jsonl') {
|
|
186
|
+
return [
|
|
187
|
+
prefix,
|
|
188
|
+
'The current model path does not support native JSONL events for this harness.',
|
|
189
|
+
'Use text output and ask the model to emit one valid JSON object per line, or choose a harness with native JSONL support.'
|
|
190
|
+
].join(' ')
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return prefix
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function planTextCommand<Output extends RunOutputRequest>(input: {
|
|
197
|
+
harnessId: HarnessId
|
|
198
|
+
request: RunRequest<Output>
|
|
199
|
+
command: string
|
|
200
|
+
args: string[]
|
|
201
|
+
cwd: string
|
|
202
|
+
output: TextOutputRequest | JsonlOutputRequest
|
|
203
|
+
}): CommandPlan<Output> {
|
|
204
|
+
return {
|
|
205
|
+
args: input.args,
|
|
206
|
+
command: input.command,
|
|
207
|
+
cwd: input.request.cwd ?? input.cwd,
|
|
208
|
+
env: input.request.env,
|
|
209
|
+
harnessId: input.harnessId,
|
|
210
|
+
output: input.output as Output,
|
|
211
|
+
stdin: input.request.stdin,
|
|
212
|
+
timeoutMs: input.request.timeoutMs
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function planCommand<Output extends RunOutputRequest>(input: {
|
|
217
|
+
harnessId: HarnessId
|
|
218
|
+
request: RunRequest<Output>
|
|
219
|
+
command: string
|
|
220
|
+
args: string[]
|
|
221
|
+
cwd: string
|
|
222
|
+
output: TextOutputRequest | JsonlOutputRequest | StructuredOutputRequest
|
|
223
|
+
env?: Record<string, string | undefined>
|
|
224
|
+
}): CommandPlan<Output> {
|
|
225
|
+
return {
|
|
226
|
+
args: input.args,
|
|
227
|
+
command: input.command,
|
|
228
|
+
cwd: input.request.cwd ?? input.cwd,
|
|
229
|
+
env: mergeEnv(input.env, input.request.env),
|
|
230
|
+
harnessId: input.harnessId,
|
|
231
|
+
output: input.output as Output,
|
|
232
|
+
stdin: input.request.stdin,
|
|
233
|
+
timeoutMs: input.request.timeoutMs
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function mergeEnv(
|
|
238
|
+
base: Record<string, string | undefined> | undefined,
|
|
239
|
+
patch: Record<string, string | undefined> | undefined
|
|
240
|
+
): Record<string, string | undefined> | undefined {
|
|
241
|
+
const env = { ...base, ...patch }
|
|
242
|
+
return Object.keys(env).length > 0 ? env : undefined
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function shellQuote(value: string): string {
|
|
246
|
+
return `'${value.replaceAll("'", "'\\''")}'`
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function detectCommand(
|
|
250
|
+
config: BuiltInAdapterConfig,
|
|
251
|
+
context: HarnessContext | undefined
|
|
252
|
+
): Promise<HarnessDetection> {
|
|
253
|
+
for (const command of config.commands) {
|
|
254
|
+
const resolved = await resolveCommand(command, context?.env)
|
|
255
|
+
if (!resolved) {
|
|
256
|
+
continue
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const identity = await readIdentity(resolved, context, config.identityArgs ?? ['--version'])
|
|
260
|
+
if (identity && config.identity.test(identity)) {
|
|
261
|
+
return {
|
|
262
|
+
binary: { command: resolved, identity },
|
|
263
|
+
detected: true,
|
|
264
|
+
id: config.id
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return { detected: false, id: config.id }
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function resolveCommand(
|
|
273
|
+
command: string,
|
|
274
|
+
env: Record<string, string | undefined> | undefined
|
|
275
|
+
): Promise<string | undefined> {
|
|
276
|
+
if (command.includes('/')) {
|
|
277
|
+
return (await isExecutable(command)) ? command : undefined
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
for (const directory of (env?.PATH ?? process.env.PATH ?? '').split(delimiter)) {
|
|
281
|
+
if (!directory) {
|
|
282
|
+
continue
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const candidate = join(directory, command)
|
|
286
|
+
if (await isExecutable(candidate)) {
|
|
287
|
+
return candidate
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return undefined
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function isExecutable(path: string): Promise<boolean> {
|
|
295
|
+
try {
|
|
296
|
+
await access(path, 0b001)
|
|
297
|
+
return true
|
|
298
|
+
} catch {
|
|
299
|
+
return false
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function readIdentity(
|
|
304
|
+
command: string,
|
|
305
|
+
context: HarnessContext | undefined,
|
|
306
|
+
args: string[]
|
|
307
|
+
): Promise<string | undefined> {
|
|
308
|
+
try {
|
|
309
|
+
const subprocess = Bun.spawn({
|
|
310
|
+
cmd: [command, ...args],
|
|
311
|
+
cwd: resolveCwd(context),
|
|
312
|
+
env: Object.fromEntries(
|
|
313
|
+
Object.entries({ ...process.env, ...(context?.env ?? {}) }).filter((entry): entry is [string, string] => {
|
|
314
|
+
return typeof entry[1] === 'string'
|
|
315
|
+
})
|
|
316
|
+
),
|
|
317
|
+
stderr: 'pipe',
|
|
318
|
+
stdout: 'pipe'
|
|
319
|
+
})
|
|
320
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
321
|
+
subprocess.exited,
|
|
322
|
+
new Response(subprocess.stdout).text(),
|
|
323
|
+
new Response(subprocess.stderr).text()
|
|
324
|
+
])
|
|
325
|
+
const identity = `${stdout}\n${stderr}`.trim()
|
|
326
|
+
return exitCode === 0 && identity.length > 0 ? identity : undefined
|
|
327
|
+
} catch {
|
|
328
|
+
return undefined
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function resolveCwd(context: HarnessContext | undefined): string {
|
|
333
|
+
return context?.cwd ?? process.cwd()
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function formatError(error: unknown): string {
|
|
337
|
+
return error instanceof Error ? error.message : String(error)
|
|
338
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { HarnessId, RunOutputRequest } from './types'
|
|
2
|
+
|
|
3
|
+
export type OutputErrorKind = 'json_parse_failed' | 'structured_validation_failed'
|
|
4
|
+
export type OutputErrorMode = Exclude<RunOutputRequest['mode'], 'text' | undefined>
|
|
5
|
+
|
|
6
|
+
export class HarnessRunOutputError extends Error {
|
|
7
|
+
readonly kind: OutputErrorKind
|
|
8
|
+
readonly outputMode: OutputErrorMode
|
|
9
|
+
readonly finalText: string
|
|
10
|
+
readonly exitCode: number | null
|
|
11
|
+
readonly signal?: string
|
|
12
|
+
override readonly cause: unknown
|
|
13
|
+
|
|
14
|
+
constructor(input: {
|
|
15
|
+
kind: OutputErrorKind
|
|
16
|
+
outputMode: OutputErrorMode
|
|
17
|
+
finalText: string
|
|
18
|
+
exitCode: number | null
|
|
19
|
+
signal?: string
|
|
20
|
+
cause: unknown
|
|
21
|
+
}) {
|
|
22
|
+
super(`${input.outputMode} output failed: ${input.kind}`)
|
|
23
|
+
this.name = 'HarnessRunOutputError'
|
|
24
|
+
this.kind = input.kind
|
|
25
|
+
this.outputMode = input.outputMode
|
|
26
|
+
this.finalText = input.finalText
|
|
27
|
+
this.exitCode = input.exitCode
|
|
28
|
+
this.signal = input.signal
|
|
29
|
+
this.cause = input.cause
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class HarnessPlanError extends Error {
|
|
34
|
+
readonly harnessId: HarnessId
|
|
35
|
+
readonly kind: 'unsupported_output_mode' | 'unsupported_operation'
|
|
36
|
+
|
|
37
|
+
constructor(input: { harnessId: HarnessId; kind: HarnessPlanError['kind']; message: string }) {
|
|
38
|
+
super(input.message)
|
|
39
|
+
this.name = 'HarnessPlanError'
|
|
40
|
+
this.harnessId = input.harnessId
|
|
41
|
+
this.kind = input.kind
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import './adapters'
|
|
2
|
+
|
|
3
|
+
export { HarnessPlanError, HarnessRunOutputError } from './errors'
|
|
4
|
+
export { harness } from './registry'
|
|
5
|
+
export type {
|
|
6
|
+
CommandPlan,
|
|
7
|
+
ExtensionCheckResult,
|
|
8
|
+
ExtensionFacet,
|
|
9
|
+
ExtensionIssue,
|
|
10
|
+
ExtensionResourceKind,
|
|
11
|
+
ExtensionResources,
|
|
12
|
+
ExtensionResult,
|
|
13
|
+
HarnessAdapter,
|
|
14
|
+
HarnessContext,
|
|
15
|
+
HarnessDetection,
|
|
16
|
+
HarnessExtension,
|
|
17
|
+
HarnessHandle,
|
|
18
|
+
HarnessId,
|
|
19
|
+
HarnessRegistry,
|
|
20
|
+
HarnessRun,
|
|
21
|
+
HarnessRunEvent,
|
|
22
|
+
HarnessRunResult,
|
|
23
|
+
HealthFacet,
|
|
24
|
+
HealthReport,
|
|
25
|
+
HookResource,
|
|
26
|
+
JsonlOutputRequest,
|
|
27
|
+
McpServerResource,
|
|
28
|
+
ProcessFacet,
|
|
29
|
+
RunOutputRequest,
|
|
30
|
+
RunRequest,
|
|
31
|
+
StructuredOutputRequest,
|
|
32
|
+
StructuredRunResult,
|
|
33
|
+
TextOutputRequest
|
|
34
|
+
} from './types'
|