@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 CHANGED
@@ -4,7 +4,7 @@
4
4
  "name": "@plimeor/harness",
5
5
  "type": "module",
6
6
  "types": "./src/index.ts",
7
- "version": "0.1.0",
7
+ "version": "0.1.2",
8
8
  "bugs": {
9
9
  "url": "https://github.com/plimeor/labs/issues"
10
10
  },
@@ -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
- mcp: { configFile: `${directory}/mcp.json`, kind: 'claude-json' },
29
- skillsDirectory: `${directory}/skills`,
30
- hooks: {
31
- kind: 'json-hooks',
32
- settingsFile: `${directory}/settings.json`,
33
- events: [
34
- 'SessionStart',
35
- 'Setup',
36
- 'UserPromptSubmit',
37
- 'UserPromptExpansion',
38
- 'PreToolUse',
39
- 'PermissionRequest',
40
- 'PermissionDenied',
41
- 'PostToolUse',
42
- 'PostToolUseFailure',
43
- 'PostToolBatch',
44
- 'Notification',
45
- 'MessageDisplay',
46
- 'SubagentStart',
47
- 'SubagentStop',
48
- 'TaskCreated',
49
- 'TaskCompleted',
50
- 'Stop',
51
- 'StopFailure',
52
- 'TeammateIdle',
53
- 'InstructionsLoaded',
54
- 'ConfigChange',
55
- 'CwdChanged',
56
- 'FileChanged',
57
- 'WorktreeCreate',
58
- 'WorktreeRemove',
59
- 'PreCompact',
60
- 'PostCompact',
61
- 'Elicitation',
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
+ }
@@ -8,7 +8,8 @@ import type {
8
8
  StructuredOutputRequest,
9
9
  TextOutputRequest
10
10
  } from '../types'
11
- import { configDirectory, createExtensionFacet } from './extensions'
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
- mcp: { configFile: `${directory}/config.toml`, kind: 'codex-toml' },
29
- skillsDirectory: `${directory}/skills`,
30
- hooks: {
31
- kind: 'json-hooks',
32
- settingsFile: `${directory}/hooks.json`,
33
- events: [
34
- 'PermissionRequest',
35
- 'PostCompact',
36
- 'PostToolUse',
37
- 'PreCompact',
38
- 'PreToolUse',
39
- 'SessionStart',
40
- 'Stop',
41
- 'SubagentStart',
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) {