@spacek33z/autoauto 0.0.1

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.
Files changed (67) hide show
  1. package/README.md +197 -0
  2. package/package.json +51 -0
  3. package/src/App.tsx +224 -0
  4. package/src/cli.ts +772 -0
  5. package/src/components/AgentPanel.tsx +254 -0
  6. package/src/components/Chat.test.tsx +71 -0
  7. package/src/components/Chat.tsx +308 -0
  8. package/src/components/CycleField.tsx +23 -0
  9. package/src/components/ModelPicker.tsx +97 -0
  10. package/src/components/PostUpdatePrompt.tsx +46 -0
  11. package/src/components/ResultsTable.tsx +172 -0
  12. package/src/components/RunCompletePrompt.tsx +90 -0
  13. package/src/components/RunSettingsOverlay.tsx +49 -0
  14. package/src/components/RunsTable.tsx +219 -0
  15. package/src/components/StatsHeader.tsx +100 -0
  16. package/src/daemon.ts +264 -0
  17. package/src/index.tsx +8 -0
  18. package/src/lib/agent/agent-provider.test.ts +133 -0
  19. package/src/lib/agent/claude-provider.ts +277 -0
  20. package/src/lib/agent/codex-provider.ts +413 -0
  21. package/src/lib/agent/default-providers.ts +10 -0
  22. package/src/lib/agent/index.ts +32 -0
  23. package/src/lib/agent/mock-provider.ts +61 -0
  24. package/src/lib/agent/opencode-provider.ts +424 -0
  25. package/src/lib/agent/types.ts +73 -0
  26. package/src/lib/auth.ts +11 -0
  27. package/src/lib/config.ts +152 -0
  28. package/src/lib/daemon-callbacks.ts +59 -0
  29. package/src/lib/daemon-client.ts +16 -0
  30. package/src/lib/daemon-lifecycle.ts +368 -0
  31. package/src/lib/daemon-spawn.ts +122 -0
  32. package/src/lib/daemon-status.ts +189 -0
  33. package/src/lib/daemon-watcher.ts +192 -0
  34. package/src/lib/experiment-loop.ts +679 -0
  35. package/src/lib/experiment.ts +356 -0
  36. package/src/lib/finalize.test.ts +143 -0
  37. package/src/lib/finalize.ts +511 -0
  38. package/src/lib/format.test.ts +32 -0
  39. package/src/lib/format.ts +44 -0
  40. package/src/lib/git.ts +176 -0
  41. package/src/lib/ideas-backlog.test.ts +54 -0
  42. package/src/lib/ideas-backlog.ts +109 -0
  43. package/src/lib/measure.ts +472 -0
  44. package/src/lib/model-options.ts +24 -0
  45. package/src/lib/programs.ts +247 -0
  46. package/src/lib/push-stream.ts +48 -0
  47. package/src/lib/run-context.ts +112 -0
  48. package/src/lib/run-setup.ts +34 -0
  49. package/src/lib/run.ts +383 -0
  50. package/src/lib/syntax-theme.ts +39 -0
  51. package/src/lib/system-prompts/experiment.ts +77 -0
  52. package/src/lib/system-prompts/finalize.ts +90 -0
  53. package/src/lib/system-prompts/index.ts +7 -0
  54. package/src/lib/system-prompts/setup.ts +516 -0
  55. package/src/lib/system-prompts/update.ts +188 -0
  56. package/src/lib/tool-events.ts +99 -0
  57. package/src/lib/validate-measurement.ts +326 -0
  58. package/src/lib/worktree.ts +40 -0
  59. package/src/screens/AuthErrorScreen.tsx +31 -0
  60. package/src/screens/ExecutionScreen.tsx +851 -0
  61. package/src/screens/FirstSetupScreen.tsx +168 -0
  62. package/src/screens/HomeScreen.tsx +406 -0
  63. package/src/screens/PreRunScreen.tsx +206 -0
  64. package/src/screens/SettingsScreen.tsx +189 -0
  65. package/src/screens/SetupScreen.tsx +226 -0
  66. package/src/tui.tsx +17 -0
  67. package/tsconfig.json +17 -0
@@ -0,0 +1,277 @@
1
+ import {
2
+ query,
3
+ type AccountInfo,
4
+ type SDKMessage,
5
+ type SDKUserMessage,
6
+ type SDKAssistantMessage,
7
+ type SDKPartialAssistantMessage,
8
+ type SDKResultMessage,
9
+ type Query,
10
+ } from "@anthropic-ai/claude-agent-sdk"
11
+ import { createPushStream } from "../push-stream.ts"
12
+ import type {
13
+ AgentProvider,
14
+ AgentSession,
15
+ AgentSessionConfig,
16
+ AgentEvent,
17
+ AgentCost,
18
+ AuthResult,
19
+ AgentModelOption,
20
+ } from "./types.ts"
21
+
22
+ // --- SDK message helpers (formerly in sdk-helpers.ts) ---
23
+
24
+ function getAssistantText(message: SDKAssistantMessage): string {
25
+ return message.message.content
26
+ .flatMap((block) => (block.type === "text" ? [block.text] : []))
27
+ .join("")
28
+ }
29
+
30
+ function getTextDelta(message: SDKPartialAssistantMessage): string | null {
31
+ const event = message.event
32
+ if (event.type !== "content_block_delta" || event.delta.type !== "text_delta") {
33
+ return null
34
+ }
35
+ return event.delta.text
36
+ }
37
+
38
+ /** Tracks an in-progress tool_use block while input JSON is being streamed. */
39
+ interface PendingToolUse {
40
+ tool: string
41
+ inputJson: string
42
+ }
43
+
44
+ /**
45
+ * Processes stream events for tool_use blocks. Returns the tool call only
46
+ * at content_block_stop, after all input_json_delta chunks have arrived,
47
+ * so formatToolEvent can produce descriptive messages (file paths, commands).
48
+ */
49
+ function processToolUseEvent(
50
+ event: unknown,
51
+ pending: PendingToolUse | null,
52
+ ): { pending: PendingToolUse | null; result: { tool: string; input?: Record<string, unknown> } | null } {
53
+ const ev = event as { type?: string; content_block?: { type?: string; name?: string }; delta?: { type?: string; partial_json?: string } }
54
+ const type = ev.type
55
+
56
+ if (type === "content_block_start") {
57
+ const block = ev.content_block
58
+ if (block?.type === "tool_use" && block.name) {
59
+ return { pending: { tool: block.name, inputJson: "" }, result: null }
60
+ }
61
+ return { pending, result: null }
62
+ }
63
+
64
+ if (type === "content_block_delta" && pending) {
65
+ const delta = ev.delta
66
+ if (delta?.type === "input_json_delta" && typeof delta.partial_json === "string") {
67
+ return { pending: { ...pending, inputJson: pending.inputJson + delta.partial_json }, result: null }
68
+ }
69
+ return { pending, result: null }
70
+ }
71
+
72
+ if (type === "content_block_stop" && pending) {
73
+ let input: Record<string, unknown> | undefined
74
+ try {
75
+ const parsed = JSON.parse(pending.inputJson)
76
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
77
+ input = parsed as Record<string, unknown>
78
+ }
79
+ } catch {
80
+ // Malformed JSON — emit without input
81
+ }
82
+ return { pending: null, result: { tool: pending.tool, input } }
83
+ }
84
+
85
+ return { pending, result: null }
86
+ }
87
+
88
+ function extractCost(message: SDKResultMessage): AgentCost {
89
+ return {
90
+ total_cost_usd: message.total_cost_usd ?? 0,
91
+ duration_ms: message.duration_ms ?? 0,
92
+ duration_api_ms: message.duration_api_ms ?? 0,
93
+ num_turns: message.num_turns ?? 0,
94
+ input_tokens: message.usage?.input_tokens ?? 0,
95
+ output_tokens: message.usage?.output_tokens ?? 0,
96
+ }
97
+ }
98
+
99
+ // --- ClaudeSession ---
100
+
101
+ class ClaudeSession implements AgentSession {
102
+ private inputStream = createPushStream<SDKUserMessage>()
103
+ private abortController = new AbortController()
104
+ private queryIterable: Query
105
+ private externalSignal?: AbortSignal
106
+ private signalHandler?: () => void
107
+
108
+ constructor(config: AgentSessionConfig) {
109
+ // Link external signal to our abort controller
110
+ if (config.signal) {
111
+ if (config.signal.aborted) {
112
+ this.abortController.abort()
113
+ } else {
114
+ this.externalSignal = config.signal
115
+ this.signalHandler = () => this.abortController.abort()
116
+ config.signal.addEventListener("abort", this.signalHandler, { once: true })
117
+ }
118
+ }
119
+
120
+ this.queryIterable = query({
121
+ prompt: this.inputStream,
122
+ options: {
123
+ systemPrompt: config.systemPrompt,
124
+ tools: config.tools ?? [],
125
+ allowedTools: config.allowedTools,
126
+ maxTurns: config.maxTurns,
127
+ cwd: config.cwd,
128
+ model: config.model,
129
+ effort: config.effort as "low" | "medium" | "high" | "max" | undefined,
130
+ permissionMode: "bypassPermissions",
131
+ allowDangerouslySkipPermissions: true,
132
+ persistSession: false,
133
+ includePartialMessages: true,
134
+ abortController: this.abortController,
135
+ },
136
+ })
137
+ }
138
+
139
+ pushMessage(content: string): void {
140
+ this.inputStream.push({
141
+ type: "user",
142
+ message: { role: "user", content },
143
+ parent_tool_use_id: null,
144
+ })
145
+ }
146
+
147
+ endInput(): void {
148
+ this.inputStream.end()
149
+ }
150
+
151
+ close(): void {
152
+ if (this.externalSignal && this.signalHandler) {
153
+ this.externalSignal.removeEventListener("abort", this.signalHandler)
154
+ }
155
+ this.abortController.abort()
156
+ this.inputStream.end()
157
+ }
158
+
159
+ async *[Symbol.asyncIterator](): AsyncIterator<AgentEvent> {
160
+ let pendingTool: PendingToolUse | null = null
161
+
162
+ try {
163
+ for await (const message of this.queryIterable as AsyncIterable<SDKMessage>) {
164
+ if (this.abortController.signal.aborted) break
165
+
166
+ if (message.type === "stream_event") {
167
+ const partial = message as unknown as SDKPartialAssistantMessage
168
+ const text = getTextDelta(partial)
169
+ if (text) {
170
+ yield { type: "text_delta", text }
171
+ }
172
+
173
+ // Accumulate tool input across stream events, emit at content_block_stop
174
+ const { pending, result } = processToolUseEvent(partial.event, pendingTool)
175
+ pendingTool = pending
176
+ if (result) {
177
+ yield { type: "tool_use", tool: result.tool, input: result.input }
178
+ }
179
+ } else if (message.type === "assistant") {
180
+ const text = getAssistantText(message as unknown as SDKAssistantMessage)
181
+ yield { type: "assistant_complete", text }
182
+ } else if (message.type === "result") {
183
+ const resultMsg = message as unknown as SDKResultMessage
184
+ const cost = extractCost(resultMsg)
185
+ const success = resultMsg.subtype === "success"
186
+ const error = success
187
+ ? undefined
188
+ : resultMsg.errors.join(", ") || resultMsg.subtype
189
+ yield { type: "result", success, error, cost }
190
+ }
191
+ // Skip "user" and "system" message types
192
+ }
193
+ } catch (err: unknown) {
194
+ if (!this.abortController.signal.aborted) {
195
+ yield {
196
+ type: "error",
197
+ error: err instanceof Error ? err.message : String(err),
198
+ retriable: false,
199
+ }
200
+ }
201
+ }
202
+ }
203
+ }
204
+
205
+ // --- ClaudeProvider ---
206
+
207
+ export class ClaudeProvider implements AgentProvider {
208
+ readonly name = "claude"
209
+
210
+ async listModels(): Promise<AgentModelOption[]> {
211
+ return [
212
+ {
213
+ provider: "claude",
214
+ model: "sonnet",
215
+ label: "Claude / Sonnet",
216
+ description: "Claude Sonnet via Claude Agent SDK",
217
+ isDefault: true,
218
+ },
219
+ {
220
+ provider: "claude",
221
+ model: "opus",
222
+ label: "Claude / Opus",
223
+ description: "Claude Opus via Claude Agent SDK",
224
+ },
225
+ ]
226
+ }
227
+
228
+ async getDefaultModel(): Promise<string> {
229
+ return "sonnet"
230
+ }
231
+
232
+ createSession(config: AgentSessionConfig): AgentSession {
233
+ return new ClaudeSession(config)
234
+ }
235
+
236
+ runOnce(prompt: string, config: AgentSessionConfig): AgentSession {
237
+ const session = this.createSession(config)
238
+ session.pushMessage(prompt)
239
+ session.endInput()
240
+ return session
241
+ }
242
+
243
+ async checkAuth(): Promise<AuthResult> {
244
+ const abortController = new AbortController()
245
+ const timeout = setTimeout(() => abortController.abort(), 10_000)
246
+
247
+ try {
248
+ const idleStream = createPushStream<SDKUserMessage>()
249
+
250
+ const q = query({
251
+ prompt: idleStream,
252
+ options: {
253
+ tools: [],
254
+ persistSession: false,
255
+ abortController,
256
+ },
257
+ })
258
+
259
+ const account: AccountInfo = await q.accountInfo()
260
+ q.close()
261
+ idleStream.end()
262
+ clearTimeout(timeout)
263
+
264
+ return {
265
+ authenticated: true,
266
+ account: { ...account } as Record<string, unknown> & { email?: string },
267
+ }
268
+ } catch (err) {
269
+ clearTimeout(timeout)
270
+ abortController.abort()
271
+ return {
272
+ authenticated: false,
273
+ error: err instanceof Error ? err.message : String(err),
274
+ }
275
+ }
276
+ }
277
+ }
@@ -0,0 +1,413 @@
1
+ import { $ } from "bun"
2
+ import { createRequire } from "node:module"
3
+ import { dirname, join } from "node:path"
4
+ import {
5
+ Codex,
6
+ type ModelReasoningEffort,
7
+ type Thread,
8
+ type ThreadEvent,
9
+ type ThreadItem,
10
+ type ThreadOptions,
11
+ type Usage,
12
+ } from "@openai/codex-sdk"
13
+ import { createPushStream } from "../push-stream.ts"
14
+ import type {
15
+ AgentCost,
16
+ AgentEvent,
17
+ AgentModelOption,
18
+ AgentProvider,
19
+ AgentSession,
20
+ AgentSessionConfig,
21
+ AuthResult,
22
+ } from "./types.ts"
23
+
24
+ const CODEX_DEFAULT_MODEL = "default"
25
+ const PLATFORM_PACKAGE_BY_TARGET: Record<string, string> = {
26
+ "x86_64-unknown-linux-musl": "@openai/codex-linux-x64",
27
+ "aarch64-unknown-linux-musl": "@openai/codex-linux-arm64",
28
+ "x86_64-apple-darwin": "@openai/codex-darwin-x64",
29
+ "aarch64-apple-darwin": "@openai/codex-darwin-arm64",
30
+ "x86_64-pc-windows-msvc": "@openai/codex-win32-x64",
31
+ "aarch64-pc-windows-msvc": "@openai/codex-win32-arm64",
32
+ }
33
+ const require = createRequire(import.meta.url)
34
+
35
+ function getCodexBinPath(): string {
36
+ const targetTriple = getCodexTargetTriple()
37
+ const platformPackage = PLATFORM_PACKAGE_BY_TARGET[targetTriple]
38
+ if (!platformPackage) throw new Error(`Unsupported Codex target triple: ${targetTriple}`)
39
+
40
+ const packageJsonPath = require.resolve(`${platformPackage}/package.json`)
41
+ const vendorRoot = join(dirname(packageJsonPath), "vendor")
42
+ const binaryName = process.platform === "win32" ? "codex.exe" : "codex"
43
+ return join(vendorRoot, targetTriple, "codex", binaryName)
44
+ }
45
+
46
+ function getCodexTargetTriple(): string {
47
+ switch (process.platform) {
48
+ case "darwin":
49
+ if (process.arch === "arm64") return "aarch64-apple-darwin"
50
+ if (process.arch === "x64") return "x86_64-apple-darwin"
51
+ break
52
+ case "linux":
53
+ case "android":
54
+ if (process.arch === "arm64") return "aarch64-unknown-linux-musl"
55
+ if (process.arch === "x64") return "x86_64-unknown-linux-musl"
56
+ break
57
+ case "win32":
58
+ if (process.arch === "arm64") return "aarch64-pc-windows-msvc"
59
+ if (process.arch === "x64") return "x86_64-pc-windows-msvc"
60
+ break
61
+ }
62
+
63
+ throw new Error(`Unsupported Codex platform: ${process.platform} (${process.arch})`)
64
+ }
65
+
66
+ function normalizeModel(model: string | undefined): string | undefined {
67
+ if (!model || model === CODEX_DEFAULT_MODEL) return undefined
68
+ return model
69
+ }
70
+
71
+ function mapEffort(effort: string | undefined): ModelReasoningEffort | undefined {
72
+ switch (effort) {
73
+ case "low":
74
+ case "medium":
75
+ case "high":
76
+ return effort
77
+ case "max":
78
+ return "xhigh"
79
+ default:
80
+ return undefined
81
+ }
82
+ }
83
+
84
+ function stringProviderOption<T extends string>(
85
+ config: AgentSessionConfig,
86
+ key: string,
87
+ allowed: readonly T[],
88
+ ): T | undefined {
89
+ const value = config.providerOptions?.[key]
90
+ return typeof value === "string" && (allowed as readonly string[]).includes(value)
91
+ ? value as T
92
+ : undefined
93
+ }
94
+
95
+ function booleanProviderOption(config: AgentSessionConfig, key: string): boolean | undefined {
96
+ const value = config.providerOptions?.[key]
97
+ return typeof value === "boolean" ? value : undefined
98
+ }
99
+
100
+ function hasWriteTools(config: AgentSessionConfig): boolean {
101
+ const tools = config.allowedTools ?? config.tools ?? []
102
+ return tools.some((tool) => {
103
+ const normalized = tool.toLowerCase()
104
+ return normalized === "write" || normalized === "edit"
105
+ })
106
+ }
107
+
108
+ function buildThreadOptions(config: AgentSessionConfig): ThreadOptions {
109
+ const sandboxMode =
110
+ stringProviderOption(config, "sandboxMode", ["read-only", "workspace-write", "danger-full-access"] as const)
111
+ ?? (hasWriteTools(config) ? "danger-full-access" : "read-only")
112
+
113
+ return {
114
+ model: normalizeModel(config.model),
115
+ modelReasoningEffort: mapEffort(config.effort),
116
+ workingDirectory: config.cwd,
117
+ approvalPolicy: stringProviderOption(config, "approvalPolicy", ["never", "on-request", "on-failure", "untrusted"] as const) ?? "never",
118
+ sandboxMode,
119
+ networkAccessEnabled: booleanProviderOption(config, "networkAccessEnabled") ?? true,
120
+ skipGitRepoCheck: booleanProviderOption(config, "skipGitRepoCheck"),
121
+ }
122
+ }
123
+
124
+ function buildPrompt(config: AgentSessionConfig, prompt: string, isFirstTurn: boolean): string {
125
+ const systemPrompt = config.systemPrompt?.trim()
126
+ if (!systemPrompt || !isFirstTurn) return prompt
127
+
128
+ return [
129
+ "System instructions:",
130
+ systemPrompt,
131
+ "",
132
+ "User request:",
133
+ prompt,
134
+ ].join("\n")
135
+ }
136
+
137
+ function getItemText(item: ThreadItem): string | null {
138
+ return item.type === "agent_message" ? item.text : null
139
+ }
140
+
141
+ function getToolUse(item: ThreadItem): { tool: string; input?: Record<string, unknown> } | null {
142
+ switch (item.type) {
143
+ case "command_execution":
144
+ return {
145
+ tool: "Bash",
146
+ input: {
147
+ command: item.command,
148
+ status: item.status,
149
+ exit_code: item.exit_code,
150
+ },
151
+ }
152
+ case "file_change":
153
+ return {
154
+ tool: "Edit",
155
+ input: {
156
+ file_path: item.changes[0]?.path,
157
+ changes: item.changes,
158
+ status: item.status,
159
+ },
160
+ }
161
+ case "mcp_tool_call":
162
+ return {
163
+ tool: `${item.server}.${item.tool}`,
164
+ input: {
165
+ arguments: item.arguments,
166
+ status: item.status,
167
+ },
168
+ }
169
+ case "web_search":
170
+ return {
171
+ tool: "WebSearch",
172
+ input: { query: item.query },
173
+ }
174
+ default:
175
+ return null
176
+ }
177
+ }
178
+
179
+ function extractCost(usage: Usage, startedAt: number, numTurns: number): AgentCost {
180
+ return {
181
+ total_cost_usd: 0,
182
+ duration_ms: Date.now() - startedAt,
183
+ duration_api_ms: 0,
184
+ num_turns: numTurns,
185
+ input_tokens: usage.input_tokens,
186
+ output_tokens: usage.output_tokens,
187
+ }
188
+ }
189
+
190
+ class CodexSession implements AgentSession {
191
+ private input = createPushStream<string>()
192
+ private events = createPushStream<AgentEvent>()
193
+ private abortController = new AbortController()
194
+ private externalSignal?: AbortSignal
195
+ private signalHandler?: () => void
196
+ private closed = false
197
+ private thread: Thread
198
+ private turnCount = 0
199
+
200
+ constructor(
201
+ codex: Codex,
202
+ private config: AgentSessionConfig,
203
+ ) {
204
+ this.thread = codex.startThread(buildThreadOptions(config))
205
+
206
+ if (config.signal) {
207
+ if (config.signal.aborted) {
208
+ this.abortController.abort()
209
+ } else {
210
+ this.externalSignal = config.signal
211
+ this.signalHandler = () => this.close()
212
+ config.signal.addEventListener("abort", this.signalHandler, { once: true })
213
+ }
214
+ }
215
+
216
+ this.run().catch((err: unknown) => {
217
+ if (!this.closed) {
218
+ const error = err instanceof Error ? err.message : String(err)
219
+ this.events.push({ type: "error", error, retriable: false })
220
+ this.events.push({ type: "result", success: false, error })
221
+ }
222
+ this.events.end()
223
+ })
224
+ }
225
+
226
+ pushMessage(content: string): void {
227
+ if (this.closed) return
228
+ this.input.push(content)
229
+ }
230
+
231
+ endInput(): void {
232
+ this.input.end()
233
+ }
234
+
235
+ close(): void {
236
+ if (this.closed) return
237
+ this.closed = true
238
+ if (this.externalSignal && this.signalHandler) {
239
+ this.externalSignal.removeEventListener("abort", this.signalHandler)
240
+ }
241
+ this.abortController.abort()
242
+ this.input.end()
243
+ this.events.end()
244
+ }
245
+
246
+ async *[Symbol.asyncIterator](): AsyncIterator<AgentEvent> {
247
+ yield* this.events
248
+ }
249
+
250
+ private async run(): Promise<void> {
251
+ for await (const rawPrompt of this.input) {
252
+ if (this.closed || this.abortController.signal.aborted) break
253
+
254
+ if (this.config.maxTurns != null && this.turnCount >= this.config.maxTurns) {
255
+ const error = `Codex session exceeded maxTurns (${this.config.maxTurns})`
256
+ this.events.push({ type: "error", error, retriable: false })
257
+ this.events.push({ type: "result", success: false, error })
258
+ break
259
+ }
260
+
261
+ const prompt = buildPrompt(this.config, rawPrompt, this.turnCount === 0)
262
+ await this.runTurn(prompt)
263
+ }
264
+
265
+ this.events.end()
266
+ }
267
+
268
+ private async runTurn(prompt: string): Promise<void> {
269
+ const startedAt = Date.now()
270
+ this.turnCount += 1
271
+ const textByItemID = new Map<string, string>()
272
+ const emittedTools = new Set<string>()
273
+
274
+ try {
275
+ const { events } = await this.thread.runStreamed(prompt, {
276
+ signal: this.abortController.signal,
277
+ })
278
+
279
+ for await (const event of events) {
280
+ if (this.closed || this.abortController.signal.aborted) break
281
+ this.handleEvent(event, textByItemID, emittedTools, startedAt)
282
+ }
283
+ } catch (err: unknown) {
284
+ if (!this.closed && !this.abortController.signal.aborted) {
285
+ const error = err instanceof Error ? err.message : String(err)
286
+ this.events.push({ type: "error", error, retriable: false })
287
+ this.events.push({ type: "result", success: false, error })
288
+ }
289
+ }
290
+ }
291
+
292
+ private handleEvent(
293
+ event: ThreadEvent,
294
+ textByItemID: Map<string, string>,
295
+ emittedTools: Set<string>,
296
+ startedAt: number,
297
+ ): void {
298
+ switch (event.type) {
299
+ case "item.started":
300
+ case "item.updated":
301
+ case "item.completed":
302
+ this.handleItem(event.item, event.type, textByItemID, emittedTools)
303
+ break
304
+ case "turn.completed":
305
+ this.events.push({
306
+ type: "result",
307
+ success: true,
308
+ cost: extractCost(event.usage, startedAt, this.turnCount),
309
+ })
310
+ break
311
+ case "turn.failed":
312
+ this.events.push({ type: "error", error: event.error.message, retriable: false })
313
+ this.events.push({ type: "result", success: false, error: event.error.message })
314
+ break
315
+ case "error":
316
+ this.events.push({ type: "error", error: event.message, retriable: false })
317
+ this.events.push({ type: "result", success: false, error: event.message })
318
+ break
319
+ }
320
+ }
321
+
322
+ private handleItem(
323
+ item: ThreadItem,
324
+ eventType: "item.started" | "item.updated" | "item.completed",
325
+ textByItemID: Map<string, string>,
326
+ emittedTools: Set<string>,
327
+ ): void {
328
+ const text = getItemText(item)
329
+ if (text != null) {
330
+ const previous = textByItemID.get(item.id) ?? ""
331
+ const delta = text.startsWith(previous) ? text.slice(previous.length) : text
332
+ textByItemID.set(item.id, text)
333
+ if (delta) this.events.push({ type: "text_delta", text: delta })
334
+ if (eventType === "item.completed" && text.trim()) {
335
+ this.events.push({ type: "assistant_complete", text })
336
+ }
337
+ return
338
+ }
339
+
340
+ if (item.type === "error") {
341
+ this.events.push({ type: "error", error: item.message, retriable: false })
342
+ return
343
+ }
344
+
345
+ const toolUse = getToolUse(item)
346
+ if (!toolUse) return
347
+
348
+ const status = "status" in item ? item.status : eventType
349
+ const key = `${item.id}:${item.type}:${status}`
350
+ if (emittedTools.has(key)) return
351
+ emittedTools.add(key)
352
+ this.events.push({ type: "tool_use", ...toolUse })
353
+ }
354
+ }
355
+
356
+ export class CodexProvider implements AgentProvider {
357
+ readonly name = "codex"
358
+ private codex = new Codex()
359
+
360
+ async listModels(): Promise<AgentModelOption[]> {
361
+ return [
362
+ {
363
+ provider: "codex",
364
+ model: CODEX_DEFAULT_MODEL,
365
+ label: "Codex / Default",
366
+ description: "Configured Codex CLI default model",
367
+ isDefault: true,
368
+ },
369
+ ]
370
+ }
371
+
372
+ async getDefaultModel(): Promise<string> {
373
+ return CODEX_DEFAULT_MODEL
374
+ }
375
+
376
+ createSession(config: AgentSessionConfig): AgentSession {
377
+ return new CodexSession(this.codex, config)
378
+ }
379
+
380
+ runOnce(prompt: string, config: AgentSessionConfig): AgentSession {
381
+ const session = this.createSession(config)
382
+ session.pushMessage(prompt)
383
+ session.endInput()
384
+ return session
385
+ }
386
+
387
+ async checkAuth(): Promise<AuthResult> {
388
+ const codexBinPath = getCodexBinPath()
389
+ const [statusResult, versionResult] = await Promise.all([
390
+ $`${codexBinPath} login status`.nothrow().quiet(),
391
+ $`${codexBinPath} --version`.nothrow().quiet(),
392
+ ])
393
+
394
+ const status = `${statusResult.stdout.toString()}${statusResult.stderr.toString()}`.trim()
395
+ const version = `${versionResult.stdout.toString()}${versionResult.stderr.toString()}`.trim()
396
+
397
+ if (statusResult.exitCode === 0) {
398
+ return {
399
+ authenticated: true,
400
+ account: {
401
+ provider: "codex",
402
+ status,
403
+ version,
404
+ },
405
+ }
406
+ }
407
+
408
+ return {
409
+ authenticated: false,
410
+ error: status || `Codex login status exited with code ${statusResult.exitCode}`,
411
+ }
412
+ }
413
+ }
@@ -0,0 +1,10 @@
1
+ import { setProvider } from "./index.ts"
2
+ import { ClaudeProvider } from "./claude-provider.ts"
3
+ import { OpenCodeProvider } from "./opencode-provider.ts"
4
+ import { CodexProvider } from "./codex-provider.ts"
5
+
6
+ export function registerDefaultProviders(): void {
7
+ setProvider("claude", new ClaudeProvider())
8
+ setProvider("opencode", new OpenCodeProvider())
9
+ setProvider("codex", new CodexProvider())
10
+ }