@plimeor/harness 0.1.0 → 0.1.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/package.json +1 -1
- package/src/adapters/claude.ts +35 -39
- package/src/adapters/codex-extensions.ts +266 -0
- package/src/adapters/codex.ts +16 -19
- package/src/adapters/extensions.ts +168 -450
- package/src/adapters/kiro-extensions.ts +182 -0
- package/src/adapters/kiro.ts +20 -19
package/package.json
CHANGED
package/src/adapters/claude.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type {
|
|
|
8
8
|
StructuredOutputRequest,
|
|
9
9
|
TextOutputRequest
|
|
10
10
|
} from '../types'
|
|
11
|
-
import { configDirectory, createExtensionFacet } from './extensions'
|
|
11
|
+
import { configDirectory, createExtensionFacet, createJsonHooksDriver, createJsonMcpDriver } from './extensions'
|
|
12
12
|
import { createBuiltInAdapter, planCommand, planTextCommand, unsupportedOutputMode } from './shared'
|
|
13
13
|
|
|
14
14
|
const HARNESS_ID = 'claude'
|
|
@@ -25,44 +25,40 @@ export const claudeAdapter = createBuiltInAdapter({
|
|
|
25
25
|
configDirectory: directory,
|
|
26
26
|
context,
|
|
27
27
|
harnessId: HARNESS_ID,
|
|
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
|
-
'ElicitationResult',
|
|
63
|
-
'SessionEnd'
|
|
64
|
-
]
|
|
65
|
-
}
|
|
28
|
+
hooks: createJsonHooksDriver(`${directory}/settings.json`, [
|
|
29
|
+
'SessionStart',
|
|
30
|
+
'Setup',
|
|
31
|
+
'UserPromptSubmit',
|
|
32
|
+
'UserPromptExpansion',
|
|
33
|
+
'PreToolUse',
|
|
34
|
+
'PermissionRequest',
|
|
35
|
+
'PermissionDenied',
|
|
36
|
+
'PostToolUse',
|
|
37
|
+
'PostToolUseFailure',
|
|
38
|
+
'PostToolBatch',
|
|
39
|
+
'Notification',
|
|
40
|
+
'MessageDisplay',
|
|
41
|
+
'SubagentStart',
|
|
42
|
+
'SubagentStop',
|
|
43
|
+
'TaskCreated',
|
|
44
|
+
'TaskCompleted',
|
|
45
|
+
'Stop',
|
|
46
|
+
'StopFailure',
|
|
47
|
+
'TeammateIdle',
|
|
48
|
+
'InstructionsLoaded',
|
|
49
|
+
'ConfigChange',
|
|
50
|
+
'CwdChanged',
|
|
51
|
+
'FileChanged',
|
|
52
|
+
'WorktreeCreate',
|
|
53
|
+
'WorktreeRemove',
|
|
54
|
+
'PreCompact',
|
|
55
|
+
'PostCompact',
|
|
56
|
+
'Elicitation',
|
|
57
|
+
'ElicitationResult',
|
|
58
|
+
'SessionEnd'
|
|
59
|
+
]),
|
|
60
|
+
mcp: createJsonMcpDriver(`${directory}/mcp.json`),
|
|
61
|
+
skillsDirectory: `${directory}/skills`
|
|
66
62
|
})
|
|
67
63
|
},
|
|
68
64
|
plan(request: RunRequest<RunOutputRequest>, command: string, cwd: string) {
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { basename, dirname, join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
import type { McpServerResource } from '../types'
|
|
6
|
+
import type { McpExtensionDriver } from './extensions'
|
|
7
|
+
import { mcpServerRecord } from './extensions'
|
|
8
|
+
|
|
9
|
+
type CodexMcpProof = {
|
|
10
|
+
extensionId?: string
|
|
11
|
+
fingerprint: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function createCodexMcpDriver(configFile: string): McpExtensionDriver {
|
|
15
|
+
return {
|
|
16
|
+
configFile,
|
|
17
|
+
async canReclaimOnInstall({ extension, name }) {
|
|
18
|
+
const entry = codexMcpServerEntry(await readTextFile(configFile), name)
|
|
19
|
+
const proof = entry ? codexMcpServerProof(entry, name) : undefined
|
|
20
|
+
return extension.resources.mcpServers?.[name] !== undefined && proof?.extensionId === extension.id
|
|
21
|
+
},
|
|
22
|
+
async currentFingerprint(name: string) {
|
|
23
|
+
const entry = codexMcpServerEntry(await readTextFile(configFile), name)
|
|
24
|
+
return entry ? codexMcpServerProof(entry, name)?.fingerprint : undefined
|
|
25
|
+
},
|
|
26
|
+
async install({ extensionId, name, server }) {
|
|
27
|
+
const current = await readTextFile(configFile)
|
|
28
|
+
const entry = codexMcpEntry(extensionId, name, server)
|
|
29
|
+
const next = `${removeCodexMcpServerEntry(current, name).trimEnd()}\n\n${entry}\n`
|
|
30
|
+
await writeTextFile(configFile, next)
|
|
31
|
+
const proof = codexMcpServerProof(entry, name)
|
|
32
|
+
if (!proof) {
|
|
33
|
+
throw new Error(`Codex MCP server ${name} proof could not be generated.`)
|
|
34
|
+
}
|
|
35
|
+
return { fingerprint: proof.fingerprint, name, server: mcpServerRecord(server) }
|
|
36
|
+
},
|
|
37
|
+
async remove(name: string) {
|
|
38
|
+
const current = await readTextFile(configFile)
|
|
39
|
+
await writeTextFile(configFile, `${removeCodexMcpServerEntry(current, name).trimEnd()}\n`)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function codexMcpEntry(extensionId: string, name: string, server: McpServerResource): string {
|
|
45
|
+
const lines = [
|
|
46
|
+
`[mcp_servers.${tomlKey(name)}]`,
|
|
47
|
+
`command = ${tomlString(server.command)}`,
|
|
48
|
+
`args = ${tomlArray(server.args ?? [])}`
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
const env = server.env ?? {}
|
|
52
|
+
if (Object.keys(env).length > 0) {
|
|
53
|
+
lines.push('', `[mcp_servers.${tomlKey(name)}.env]`)
|
|
54
|
+
for (const [key, value] of Object.entries(env)) {
|
|
55
|
+
lines.push(`${tomlKey(key)} = ${tomlString(value)}`)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
lines.push(`# @plimeor/harness extension = ${extensionId}`)
|
|
60
|
+
return lines.join('\n')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function removeCodexMcpServerEntry(config: string, name: string): string {
|
|
64
|
+
const kept: string[] = []
|
|
65
|
+
let inOwnedTable = false
|
|
66
|
+
let inLegacyBlock = false
|
|
67
|
+
|
|
68
|
+
for (const line of config.split('\n')) {
|
|
69
|
+
if (line === codexLegacyBeginMarker(name)) {
|
|
70
|
+
inLegacyBlock = true
|
|
71
|
+
inOwnedTable = false
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (line === codexLegacyEndMarker(name)) {
|
|
76
|
+
inLegacyBlock = false
|
|
77
|
+
inOwnedTable = false
|
|
78
|
+
continue
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (codexExtensionId(line) !== undefined && (inOwnedTable || inLegacyBlock)) {
|
|
82
|
+
inOwnedTable = false
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const tableName = tomlTableName(line)
|
|
87
|
+
if (tableName !== undefined) {
|
|
88
|
+
inOwnedTable = isCodexMcpServerTableName(tableName, name)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (inOwnedTable) {
|
|
92
|
+
continue
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
kept.push(line)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return kept.join('\n')
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function codexMcpServerEntry(config: string, name: string): string | undefined {
|
|
102
|
+
const lines = config.split('\n')
|
|
103
|
+
const legacyEntry: string[] = []
|
|
104
|
+
let inLegacyBlock = false
|
|
105
|
+
|
|
106
|
+
for (const line of lines) {
|
|
107
|
+
if (line === codexLegacyBeginMarker(name)) {
|
|
108
|
+
inLegacyBlock = true
|
|
109
|
+
legacyEntry.push(line)
|
|
110
|
+
continue
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (inLegacyBlock) {
|
|
114
|
+
legacyEntry.push(line)
|
|
115
|
+
if (line === codexLegacyEndMarker(name)) {
|
|
116
|
+
return legacyEntry.join('\n')
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const entry: string[] = []
|
|
122
|
+
let inEntry = false
|
|
123
|
+
|
|
124
|
+
for (const line of lines) {
|
|
125
|
+
const tableName = tomlTableName(line)
|
|
126
|
+
if (tableName !== undefined) {
|
|
127
|
+
const isOwnedTable = isCodexMcpServerTableName(tableName, name)
|
|
128
|
+
|
|
129
|
+
if (!inEntry && isOwnedTable) {
|
|
130
|
+
inEntry = true
|
|
131
|
+
entry.push(line)
|
|
132
|
+
continue
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (inEntry && !isOwnedTable) {
|
|
136
|
+
break
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (inEntry) {
|
|
141
|
+
entry.push(line)
|
|
142
|
+
if (codexExtensionId(line) !== undefined) {
|
|
143
|
+
break
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return entry.length > 0 ? entry.join('\n') : undefined
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function codexMcpServerProof(entry: string, name: string): CodexMcpProof | undefined {
|
|
152
|
+
const proofLines: string[] = []
|
|
153
|
+
let extensionId: string | undefined
|
|
154
|
+
let inOwnedTable = false
|
|
155
|
+
let sawOwnedTable = false
|
|
156
|
+
|
|
157
|
+
for (const line of entry.split('\n')) {
|
|
158
|
+
if (line === codexLegacyBeginMarker(name) || line === codexLegacyEndMarker(name)) {
|
|
159
|
+
inOwnedTable = false
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const markerExtensionId = codexExtensionId(line)
|
|
164
|
+
if (markerExtensionId !== undefined) {
|
|
165
|
+
extensionId = markerExtensionId
|
|
166
|
+
proofLines.push(line)
|
|
167
|
+
inOwnedTable = false
|
|
168
|
+
continue
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const tableName = tomlTableName(line)
|
|
172
|
+
if (tableName !== undefined) {
|
|
173
|
+
inOwnedTable = isCodexMcpServerTableName(tableName, name)
|
|
174
|
+
sawOwnedTable = sawOwnedTable || inOwnedTable
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (inOwnedTable) {
|
|
178
|
+
proofLines.push(line)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!sawOwnedTable) {
|
|
183
|
+
return undefined
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
extensionId,
|
|
188
|
+
fingerprint: textFingerprint(proofLines.join('\n'))
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function codexLegacyBeginMarker(name: string): string {
|
|
193
|
+
return `# @plimeor/harness begin mcpServers ${name}`
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function codexLegacyEndMarker(name: string): string {
|
|
197
|
+
return `# @plimeor/harness end mcpServers ${name}`
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function codexExtensionId(line: string): string | undefined {
|
|
201
|
+
return line.match(/^# @plimeor\/harness extension = (.*)$/)?.[1]
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// code-lean: line-based scan for Codex MCP entries, upgrade when Codex TOML writes need arbitrary table edits.
|
|
205
|
+
function tomlTableName(line: string): string | undefined {
|
|
206
|
+
return line.match(/^\s*\[(.*)]\s*(?:#.*)?$/)?.[1]
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function isCodexMcpServerTableName(tableName: string, serverName: string): boolean {
|
|
210
|
+
const tableNames = new Set([`mcp_servers.${serverName}`, `mcp_servers.${tomlKey(serverName)}`])
|
|
211
|
+
|
|
212
|
+
for (const owned of tableNames) {
|
|
213
|
+
if (tableName === owned || tableName.startsWith(`${owned}.`)) {
|
|
214
|
+
return true
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return false
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function tomlKey(value: string): string {
|
|
222
|
+
return /^[A-Za-z0-9_-]+$/.test(value) ? value : tomlString(value)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function tomlArray(values: string[]): string {
|
|
226
|
+
return `[${values.map(tomlString).join(', ')}]`
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function tomlString(value: string): string {
|
|
230
|
+
return JSON.stringify(value)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function textFingerprint(value: string): string {
|
|
234
|
+
return createHash('sha256').update(value).digest('hex')
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function readTextFile(path: string): Promise<string> {
|
|
238
|
+
try {
|
|
239
|
+
return await readFile(path, 'utf8')
|
|
240
|
+
} catch (error) {
|
|
241
|
+
if (isNotFound(error)) {
|
|
242
|
+
return ''
|
|
243
|
+
}
|
|
244
|
+
throw error
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function writeTextFile(path: string, text: string): Promise<void> {
|
|
249
|
+
await mkdir(dirname(path), { recursive: true })
|
|
250
|
+
await writeFileAtomically(path, text)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function writeFileAtomically(path: string, text: string): Promise<void> {
|
|
254
|
+
const temporaryPath = join(dirname(path), `.${basename(path)}.${process.pid}.${Date.now()}.tmp`)
|
|
255
|
+
await writeFile(temporaryPath, text)
|
|
256
|
+
try {
|
|
257
|
+
await rename(temporaryPath, path)
|
|
258
|
+
} catch (error) {
|
|
259
|
+
await rm(temporaryPath, { force: true })
|
|
260
|
+
throw error
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function isNotFound(error: unknown): boolean {
|
|
265
|
+
return error instanceof Error && 'code' in error && error.code === 'ENOENT'
|
|
266
|
+
}
|
package/src/adapters/codex.ts
CHANGED
|
@@ -8,7 +8,8 @@ import type {
|
|
|
8
8
|
StructuredOutputRequest,
|
|
9
9
|
TextOutputRequest
|
|
10
10
|
} from '../types'
|
|
11
|
-
import {
|
|
11
|
+
import { createCodexMcpDriver } from './codex-extensions'
|
|
12
|
+
import { configDirectory, createExtensionFacet, createJsonHooksDriver } from './extensions'
|
|
12
13
|
import { createBuiltInAdapter, planCommand, planTextCommand, shellQuote, unsupportedOutputMode } from './shared'
|
|
13
14
|
|
|
14
15
|
const HARNESS_ID = 'codex'
|
|
@@ -25,24 +26,20 @@ export const codexAdapter = createBuiltInAdapter({
|
|
|
25
26
|
configDirectory: directory,
|
|
26
27
|
context,
|
|
27
28
|
harnessId: HARNESS_ID,
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
'SubagentStop',
|
|
43
|
-
'UserPromptSubmit'
|
|
44
|
-
]
|
|
45
|
-
}
|
|
29
|
+
hooks: createJsonHooksDriver(`${directory}/hooks.json`, [
|
|
30
|
+
'PermissionRequest',
|
|
31
|
+
'PostCompact',
|
|
32
|
+
'PostToolUse',
|
|
33
|
+
'PreCompact',
|
|
34
|
+
'PreToolUse',
|
|
35
|
+
'SessionStart',
|
|
36
|
+
'Stop',
|
|
37
|
+
'SubagentStart',
|
|
38
|
+
'SubagentStop',
|
|
39
|
+
'UserPromptSubmit'
|
|
40
|
+
]),
|
|
41
|
+
mcp: createCodexMcpDriver(`${directory}/config.toml`),
|
|
42
|
+
skillsDirectory: `${directory}/skills`
|
|
46
43
|
})
|
|
47
44
|
},
|
|
48
45
|
plan(request: RunRequest<RunOutputRequest>, command: string, cwd: string) {
|