@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.
- package/README.md +197 -0
- package/package.json +51 -0
- package/src/App.tsx +224 -0
- package/src/cli.ts +772 -0
- package/src/components/AgentPanel.tsx +254 -0
- package/src/components/Chat.test.tsx +71 -0
- package/src/components/Chat.tsx +308 -0
- package/src/components/CycleField.tsx +23 -0
- package/src/components/ModelPicker.tsx +97 -0
- package/src/components/PostUpdatePrompt.tsx +46 -0
- package/src/components/ResultsTable.tsx +172 -0
- package/src/components/RunCompletePrompt.tsx +90 -0
- package/src/components/RunSettingsOverlay.tsx +49 -0
- package/src/components/RunsTable.tsx +219 -0
- package/src/components/StatsHeader.tsx +100 -0
- package/src/daemon.ts +264 -0
- package/src/index.tsx +8 -0
- package/src/lib/agent/agent-provider.test.ts +133 -0
- package/src/lib/agent/claude-provider.ts +277 -0
- package/src/lib/agent/codex-provider.ts +413 -0
- package/src/lib/agent/default-providers.ts +10 -0
- package/src/lib/agent/index.ts +32 -0
- package/src/lib/agent/mock-provider.ts +61 -0
- package/src/lib/agent/opencode-provider.ts +424 -0
- package/src/lib/agent/types.ts +73 -0
- package/src/lib/auth.ts +11 -0
- package/src/lib/config.ts +152 -0
- package/src/lib/daemon-callbacks.ts +59 -0
- package/src/lib/daemon-client.ts +16 -0
- package/src/lib/daemon-lifecycle.ts +368 -0
- package/src/lib/daemon-spawn.ts +122 -0
- package/src/lib/daemon-status.ts +189 -0
- package/src/lib/daemon-watcher.ts +192 -0
- package/src/lib/experiment-loop.ts +679 -0
- package/src/lib/experiment.ts +356 -0
- package/src/lib/finalize.test.ts +143 -0
- package/src/lib/finalize.ts +511 -0
- package/src/lib/format.test.ts +32 -0
- package/src/lib/format.ts +44 -0
- package/src/lib/git.ts +176 -0
- package/src/lib/ideas-backlog.test.ts +54 -0
- package/src/lib/ideas-backlog.ts +109 -0
- package/src/lib/measure.ts +472 -0
- package/src/lib/model-options.ts +24 -0
- package/src/lib/programs.ts +247 -0
- package/src/lib/push-stream.ts +48 -0
- package/src/lib/run-context.ts +112 -0
- package/src/lib/run-setup.ts +34 -0
- package/src/lib/run.ts +383 -0
- package/src/lib/syntax-theme.ts +39 -0
- package/src/lib/system-prompts/experiment.ts +77 -0
- package/src/lib/system-prompts/finalize.ts +90 -0
- package/src/lib/system-prompts/index.ts +7 -0
- package/src/lib/system-prompts/setup.ts +516 -0
- package/src/lib/system-prompts/update.ts +188 -0
- package/src/lib/tool-events.ts +99 -0
- package/src/lib/validate-measurement.ts +326 -0
- package/src/lib/worktree.ts +40 -0
- package/src/screens/AuthErrorScreen.tsx +31 -0
- package/src/screens/ExecutionScreen.tsx +851 -0
- package/src/screens/FirstSetupScreen.tsx +168 -0
- package/src/screens/HomeScreen.tsx +406 -0
- package/src/screens/PreRunScreen.tsx +206 -0
- package/src/screens/SettingsScreen.tsx +189 -0
- package/src/screens/SetupScreen.tsx +226 -0
- package/src/tui.tsx +17 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
AgentEvent,
|
|
3
|
+
AgentCost,
|
|
4
|
+
AgentSessionConfig,
|
|
5
|
+
AgentSession,
|
|
6
|
+
AuthResult,
|
|
7
|
+
AgentProvider,
|
|
8
|
+
AgentProviderID,
|
|
9
|
+
AgentModelOption,
|
|
10
|
+
} from "./types.ts"
|
|
11
|
+
|
|
12
|
+
import type { AgentProvider, AgentProviderID } from "./types.ts"
|
|
13
|
+
|
|
14
|
+
const providers = new Map<AgentProviderID, AgentProvider>()
|
|
15
|
+
|
|
16
|
+
/** Set the active agent provider. Must be called before app render. */
|
|
17
|
+
export function setProvider(id: AgentProviderID, p: AgentProvider): void {
|
|
18
|
+
providers.set(id, p)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Get the active agent provider. Throws if not yet configured. */
|
|
22
|
+
export function getProvider(id: AgentProviderID = "claude"): AgentProvider {
|
|
23
|
+
const provider = providers.get(id)
|
|
24
|
+
if (!provider) {
|
|
25
|
+
throw new Error(`No ${id} agent provider configured — call setProvider() before use`)
|
|
26
|
+
}
|
|
27
|
+
return provider
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function closeProviders(): Promise<void> {
|
|
31
|
+
await Promise.all([...providers.values()].map((provider) => provider.close?.()))
|
|
32
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentProvider,
|
|
3
|
+
AgentSession,
|
|
4
|
+
AgentSessionConfig,
|
|
5
|
+
AgentEvent,
|
|
6
|
+
AuthResult,
|
|
7
|
+
} from "./types.ts"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Mock provider for contract tests.
|
|
11
|
+
* Emits a scripted sequence of AgentEvent values, independent of any real SDK.
|
|
12
|
+
*/
|
|
13
|
+
export class MockProvider implements AgentProvider {
|
|
14
|
+
readonly name = "mock"
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
private events: AgentEvent[] = [],
|
|
18
|
+
private authResult: AuthResult = { authenticated: true, account: { email: "test@example.com" } },
|
|
19
|
+
) {}
|
|
20
|
+
|
|
21
|
+
createSession(_config: AgentSessionConfig): AgentSession {
|
|
22
|
+
return new MockSession(this.events)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
runOnce(_prompt: string, config: AgentSessionConfig): AgentSession {
|
|
26
|
+
const session = this.createSession(config)
|
|
27
|
+
session.endInput()
|
|
28
|
+
return session
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async checkAuth(): Promise<AuthResult> {
|
|
32
|
+
return this.authResult
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class MockSession implements AgentSession {
|
|
37
|
+
private messages: string[] = []
|
|
38
|
+
private ended = false
|
|
39
|
+
private closed = false
|
|
40
|
+
|
|
41
|
+
constructor(private events: AgentEvent[]) {}
|
|
42
|
+
|
|
43
|
+
pushMessage(content: string): void {
|
|
44
|
+
this.messages.push(content)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
endInput(): void {
|
|
48
|
+
this.ended = true
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
close(): void {
|
|
52
|
+
this.closed = true
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async *[Symbol.asyncIterator](): AsyncIterator<AgentEvent> {
|
|
56
|
+
for (const event of this.events) {
|
|
57
|
+
if (this.closed) break
|
|
58
|
+
yield event
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import { createOpencode, type OpencodeClient } from "@opencode-ai/sdk/v2"
|
|
2
|
+
import type {
|
|
3
|
+
AgentCost,
|
|
4
|
+
AgentEvent,
|
|
5
|
+
AgentModelOption,
|
|
6
|
+
AgentProvider,
|
|
7
|
+
AgentSession,
|
|
8
|
+
AgentSessionConfig,
|
|
9
|
+
AuthResult,
|
|
10
|
+
} from "./types.ts"
|
|
11
|
+
import { createPushStream } from "../push-stream.ts"
|
|
12
|
+
|
|
13
|
+
type OpenCodeServer = Awaited<ReturnType<typeof createOpencode>>["server"]
|
|
14
|
+
type OpenCodeInstance = { client: OpencodeClient; server: OpenCodeServer }
|
|
15
|
+
|
|
16
|
+
type OpenCodeEvent = {
|
|
17
|
+
type: string
|
|
18
|
+
properties?: Record<string, unknown>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type OpenCodePart = {
|
|
22
|
+
id?: string
|
|
23
|
+
sessionID?: string
|
|
24
|
+
messageID?: string
|
|
25
|
+
type?: string
|
|
26
|
+
text?: string
|
|
27
|
+
tool?: string
|
|
28
|
+
state?: {
|
|
29
|
+
status?: string
|
|
30
|
+
input?: Record<string, unknown>
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type OpenCodeAssistantMessage = {
|
|
35
|
+
cost?: number
|
|
36
|
+
tokens?: {
|
|
37
|
+
input?: number
|
|
38
|
+
output?: number
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseModel(model: string | undefined): { providerID: string; modelID: string } | undefined {
|
|
43
|
+
if (!model) return undefined
|
|
44
|
+
const slash = model.indexOf("/")
|
|
45
|
+
if (slash <= 0 || slash === model.length - 1) {
|
|
46
|
+
throw new Error(`OpenCode model must be in provider/model form, got "${model}"`)
|
|
47
|
+
}
|
|
48
|
+
return { providerID: model.slice(0, slash), modelID: model.slice(slash + 1) }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function mapPermission(tool: string): string[] {
|
|
52
|
+
switch (tool.toLowerCase()) {
|
|
53
|
+
case "read":
|
|
54
|
+
return ["read", "list"]
|
|
55
|
+
case "write":
|
|
56
|
+
case "edit":
|
|
57
|
+
return ["edit"]
|
|
58
|
+
case "bash":
|
|
59
|
+
return ["bash"]
|
|
60
|
+
case "glob":
|
|
61
|
+
return ["glob", "list"]
|
|
62
|
+
case "grep":
|
|
63
|
+
return ["grep"]
|
|
64
|
+
default:
|
|
65
|
+
return []
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function buildPermissionRules(config: AgentSessionConfig) {
|
|
70
|
+
const tools = config.allowedTools ?? config.tools ?? []
|
|
71
|
+
const permissions = new Set<string>()
|
|
72
|
+
for (const tool of tools) {
|
|
73
|
+
for (const permission of mapPermission(tool)) {
|
|
74
|
+
permissions.add(permission)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return [
|
|
79
|
+
...[...permissions].toSorted().map((permission) => ({
|
|
80
|
+
permission,
|
|
81
|
+
pattern: "*",
|
|
82
|
+
action: "allow" as const,
|
|
83
|
+
})),
|
|
84
|
+
{ permission: "external_directory", pattern: "*", action: "deny" as const },
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getTextFromParts(parts: unknown): string {
|
|
89
|
+
if (!Array.isArray(parts)) return ""
|
|
90
|
+
return parts
|
|
91
|
+
.flatMap((part) => {
|
|
92
|
+
if (typeof part === "object" && part !== null && (part as OpenCodePart).type === "text") {
|
|
93
|
+
const text = (part as OpenCodePart).text
|
|
94
|
+
return typeof text === "string" ? [text] : []
|
|
95
|
+
}
|
|
96
|
+
return []
|
|
97
|
+
})
|
|
98
|
+
.join("")
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function extractCost(info: unknown, startedAt: number): AgentCost {
|
|
102
|
+
const message = (typeof info === "object" && info !== null ? info : {}) as OpenCodeAssistantMessage
|
|
103
|
+
return {
|
|
104
|
+
total_cost_usd: message.cost ?? 0,
|
|
105
|
+
duration_ms: Date.now() - startedAt,
|
|
106
|
+
duration_api_ms: 0,
|
|
107
|
+
num_turns: 1,
|
|
108
|
+
input_tokens: message.tokens?.input ?? 0,
|
|
109
|
+
output_tokens: message.tokens?.output ?? 0,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
class OpenCodeSession implements AgentSession {
|
|
114
|
+
private input = createPushStream<string>()
|
|
115
|
+
private events = createPushStream<AgentEvent>()
|
|
116
|
+
private abortController = new AbortController()
|
|
117
|
+
private externalSignal?: AbortSignal
|
|
118
|
+
private signalHandler?: () => void
|
|
119
|
+
private closed = false
|
|
120
|
+
private sessionID: string | null = null
|
|
121
|
+
|
|
122
|
+
constructor(
|
|
123
|
+
private provider: OpenCodeProvider,
|
|
124
|
+
private config: AgentSessionConfig,
|
|
125
|
+
) {
|
|
126
|
+
if (config.signal) {
|
|
127
|
+
if (config.signal.aborted) {
|
|
128
|
+
this.abortController.abort()
|
|
129
|
+
} else {
|
|
130
|
+
this.externalSignal = config.signal
|
|
131
|
+
this.signalHandler = () => this.close()
|
|
132
|
+
config.signal.addEventListener("abort", this.signalHandler, { once: true })
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
this.run().catch((err: unknown) => {
|
|
137
|
+
if (!this.closed) {
|
|
138
|
+
this.events.push({
|
|
139
|
+
type: "error",
|
|
140
|
+
error: err instanceof Error ? err.message : String(err),
|
|
141
|
+
retriable: false,
|
|
142
|
+
})
|
|
143
|
+
this.events.push({
|
|
144
|
+
type: "result",
|
|
145
|
+
success: false,
|
|
146
|
+
error: err instanceof Error ? err.message : String(err),
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
this.events.end()
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
pushMessage(content: string): void {
|
|
154
|
+
if (this.closed) return
|
|
155
|
+
this.input.push(content)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
endInput(): void {
|
|
159
|
+
this.input.end()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
close(): void {
|
|
163
|
+
if (this.closed) return
|
|
164
|
+
this.closed = true
|
|
165
|
+
if (this.externalSignal && this.signalHandler) {
|
|
166
|
+
this.externalSignal.removeEventListener("abort", this.signalHandler)
|
|
167
|
+
}
|
|
168
|
+
this.abortController.abort()
|
|
169
|
+
this.input.end()
|
|
170
|
+
this.events.end()
|
|
171
|
+
const sessionID = this.sessionID
|
|
172
|
+
if (sessionID) {
|
|
173
|
+
this.provider.abortSession(sessionID, this.config.cwd).catch(() => {})
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async *[Symbol.asyncIterator](): AsyncIterator<AgentEvent> {
|
|
178
|
+
yield* this.events
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private async run(): Promise<void> {
|
|
182
|
+
const { client } = await this.provider.getInstance()
|
|
183
|
+
const directory = this.config.cwd
|
|
184
|
+
const created = await client.session.create({
|
|
185
|
+
directory,
|
|
186
|
+
title: "AutoAuto",
|
|
187
|
+
permission: buildPermissionRules(this.config),
|
|
188
|
+
})
|
|
189
|
+
if (created.error) {
|
|
190
|
+
throw new Error(`Failed to create OpenCode session: ${JSON.stringify(created.error)}`)
|
|
191
|
+
}
|
|
192
|
+
if (!created.data) throw new Error("Failed to create OpenCode session")
|
|
193
|
+
|
|
194
|
+
this.sessionID = created.data.id
|
|
195
|
+
|
|
196
|
+
const eventAbort = new AbortController()
|
|
197
|
+
const subscription = await client.event.subscribe(
|
|
198
|
+
{ directory },
|
|
199
|
+
{ signal: eventAbort.signal },
|
|
200
|
+
)
|
|
201
|
+
const consumeEvents = this.consumeEvents(
|
|
202
|
+
subscription.stream as AsyncIterable<OpenCodeEvent>,
|
|
203
|
+
created.data.id,
|
|
204
|
+
eventAbort.signal,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
for await (const prompt of this.input) {
|
|
209
|
+
if (this.closed || this.abortController.signal.aborted) break
|
|
210
|
+
|
|
211
|
+
const model = parseModel(this.config.model)
|
|
212
|
+
const startedAt = Date.now()
|
|
213
|
+
const result = await client.session.prompt(
|
|
214
|
+
{
|
|
215
|
+
sessionID: created.data.id,
|
|
216
|
+
directory,
|
|
217
|
+
agent: "build",
|
|
218
|
+
system: this.config.systemPrompt,
|
|
219
|
+
model,
|
|
220
|
+
parts: [{ type: "text", text: prompt }],
|
|
221
|
+
},
|
|
222
|
+
{ signal: this.abortController.signal },
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
if (result.error) {
|
|
226
|
+
const error = JSON.stringify(result.error)
|
|
227
|
+
this.events.push({ type: "error", error, retriable: false })
|
|
228
|
+
this.events.push({ type: "result", success: false, error })
|
|
229
|
+
continue
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const text = getTextFromParts(result.data?.parts)
|
|
233
|
+
if (text.trim()) {
|
|
234
|
+
this.events.push({ type: "assistant_complete", text })
|
|
235
|
+
}
|
|
236
|
+
this.events.push({
|
|
237
|
+
type: "result",
|
|
238
|
+
success: true,
|
|
239
|
+
cost: extractCost(result.data?.info, startedAt),
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
} finally {
|
|
243
|
+
eventAbort.abort()
|
|
244
|
+
await consumeEvents.catch(() => {})
|
|
245
|
+
this.events.end()
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private async consumeEvents(
|
|
250
|
+
stream: AsyncIterable<OpenCodeEvent>,
|
|
251
|
+
sessionID: string,
|
|
252
|
+
signal: AbortSignal,
|
|
253
|
+
): Promise<void> {
|
|
254
|
+
const textByPartID = new Map<string, string>()
|
|
255
|
+
const emittedTools = new Set<string>()
|
|
256
|
+
|
|
257
|
+
for await (const event of stream) {
|
|
258
|
+
if (this.closed || signal.aborted) break
|
|
259
|
+
const properties = event.properties ?? {}
|
|
260
|
+
if (properties.sessionID !== sessionID) continue
|
|
261
|
+
|
|
262
|
+
if (event.type === "message.part.updated") {
|
|
263
|
+
const part = properties.part as OpenCodePart | undefined
|
|
264
|
+
if (!part?.id) continue
|
|
265
|
+
|
|
266
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
267
|
+
const previous = textByPartID.get(part.id) ?? ""
|
|
268
|
+
const delta = part.text.startsWith(previous)
|
|
269
|
+
? part.text.slice(previous.length)
|
|
270
|
+
: part.text
|
|
271
|
+
textByPartID.set(part.id, part.text)
|
|
272
|
+
if (delta) this.events.push({ type: "text_delta", text: delta })
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (part.type === "tool" && part.tool) {
|
|
276
|
+
const status = part.state?.status ?? "unknown"
|
|
277
|
+
const key = `${part.id}:${status}`
|
|
278
|
+
if (!emittedTools.has(key)) {
|
|
279
|
+
emittedTools.add(key)
|
|
280
|
+
const state = part.state as {
|
|
281
|
+
status?: string
|
|
282
|
+
input?: Record<string, unknown>
|
|
283
|
+
title?: string
|
|
284
|
+
} | undefined
|
|
285
|
+
const input: Record<string, unknown> = { ...state?.input }
|
|
286
|
+
// Forward provider-supplied title for richer tool status display
|
|
287
|
+
if (state?.title) input.__title = state.title
|
|
288
|
+
this.events.push({
|
|
289
|
+
type: "tool_use",
|
|
290
|
+
tool: part.tool,
|
|
291
|
+
input,
|
|
292
|
+
})
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (event.type === "session.error") {
|
|
298
|
+
this.events.push({
|
|
299
|
+
type: "error",
|
|
300
|
+
error: JSON.stringify(properties.error ?? "OpenCode session error"),
|
|
301
|
+
retriable: false,
|
|
302
|
+
})
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export class OpenCodeProvider implements AgentProvider {
|
|
309
|
+
readonly name = "opencode"
|
|
310
|
+
private instance: Promise<OpenCodeInstance> | null = null
|
|
311
|
+
private resolvedInstance: OpenCodeInstance | null = null
|
|
312
|
+
private modelCache: AgentModelOption[] | null = null
|
|
313
|
+
|
|
314
|
+
async getInstance(): Promise<OpenCodeInstance> {
|
|
315
|
+
if (!this.instance) {
|
|
316
|
+
this.instance = this.startInstance()
|
|
317
|
+
}
|
|
318
|
+
return this.instance
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
createSession(config: AgentSessionConfig): AgentSession {
|
|
322
|
+
return new OpenCodeSession(this, config)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
runOnce(prompt: string, config: AgentSessionConfig): AgentSession {
|
|
326
|
+
const session = this.createSession(config)
|
|
327
|
+
session.pushMessage(prompt)
|
|
328
|
+
session.endInput()
|
|
329
|
+
return session
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async checkAuth(): Promise<AuthResult> {
|
|
333
|
+
try {
|
|
334
|
+
const { client } = await this.getInstance()
|
|
335
|
+
const health = await client.global.health({ throwOnError: true })
|
|
336
|
+
return {
|
|
337
|
+
authenticated: true,
|
|
338
|
+
account: {
|
|
339
|
+
provider: "opencode",
|
|
340
|
+
version: health.data.version,
|
|
341
|
+
},
|
|
342
|
+
}
|
|
343
|
+
} catch (err) {
|
|
344
|
+
return {
|
|
345
|
+
authenticated: false,
|
|
346
|
+
error: err instanceof Error ? err.message : String(err),
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async listModels(cwd?: string, forceRefresh = false): Promise<AgentModelOption[]> {
|
|
352
|
+
if (this.modelCache && !forceRefresh) return this.modelCache
|
|
353
|
+
|
|
354
|
+
const { client } = await this.getInstance()
|
|
355
|
+
const response = await client.provider.list({ directory: cwd }, { throwOnError: true })
|
|
356
|
+
const connected = new Set(response.data.connected)
|
|
357
|
+
const defaults = response.data.default
|
|
358
|
+
const options: AgentModelOption[] = []
|
|
359
|
+
|
|
360
|
+
for (const provider of response.data.all) {
|
|
361
|
+
if (!connected.has(provider.id)) continue
|
|
362
|
+
const defaultModel = defaults[provider.id]
|
|
363
|
+
for (const [modelID, model] of Object.entries(provider.models)) {
|
|
364
|
+
const fullModel = `${provider.id}/${modelID}`
|
|
365
|
+
const isDefault = defaultModel === modelID
|
|
366
|
+
options.push({
|
|
367
|
+
provider: "opencode",
|
|
368
|
+
model: fullModel,
|
|
369
|
+
label: `OpenCode / ${provider.name} / ${model.name}`,
|
|
370
|
+
description: isDefault ? "Configured OpenCode default" : fullModel,
|
|
371
|
+
isDefault,
|
|
372
|
+
})
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
options.sort((a, b) => {
|
|
377
|
+
if (a.isDefault !== b.isDefault) return a.isDefault ? -1 : 1
|
|
378
|
+
return a.label.localeCompare(b.label)
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
this.modelCache = options
|
|
382
|
+
return options
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async getDefaultModel(cwd?: string): Promise<string | null> {
|
|
386
|
+
const options = await this.listModels(cwd)
|
|
387
|
+
return options.find((option) => option.isDefault)?.model ?? options[0]?.model ?? null
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async abortSession(sessionID: string, cwd?: string): Promise<void> {
|
|
391
|
+
const { client } = await this.getInstance()
|
|
392
|
+
await client.session.abort({ sessionID, directory: cwd }).catch(() => {})
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
close(): void {
|
|
396
|
+
this.resolvedInstance?.server.close()
|
|
397
|
+
this.resolvedInstance = null
|
|
398
|
+
this.instance = null
|
|
399
|
+
this.modelCache = null
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private async startInstance(): Promise<OpenCodeInstance> {
|
|
403
|
+
const instance = await createOpencode({
|
|
404
|
+
port: await getFreePort(),
|
|
405
|
+
timeout: 10_000,
|
|
406
|
+
config: { autoupdate: false },
|
|
407
|
+
})
|
|
408
|
+
this.resolvedInstance = instance
|
|
409
|
+
return instance
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async function getFreePort(): Promise<number> {
|
|
414
|
+
const server = Bun.serve({
|
|
415
|
+
port: 0,
|
|
416
|
+
fetch() {
|
|
417
|
+
return new Response("ok")
|
|
418
|
+
},
|
|
419
|
+
})
|
|
420
|
+
const port = server.port
|
|
421
|
+
if (port == null) throw new Error("Failed to allocate local OpenCode port")
|
|
422
|
+
await server.stop()
|
|
423
|
+
return port
|
|
424
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** Normalized event types emitted by all agent providers. */
|
|
2
|
+
export type AgentEvent =
|
|
3
|
+
| { type: "text_delta"; text: string }
|
|
4
|
+
| { type: "tool_use"; tool: string; input?: Record<string, unknown> }
|
|
5
|
+
| { type: "assistant_complete"; text: string }
|
|
6
|
+
| { type: "error"; error: string; retriable: boolean }
|
|
7
|
+
| { type: "result"; success: boolean; error?: string; cost?: AgentCost }
|
|
8
|
+
|
|
9
|
+
/** Cost and usage data from a completed agent session. */
|
|
10
|
+
export interface AgentCost {
|
|
11
|
+
total_cost_usd: number
|
|
12
|
+
duration_ms: number
|
|
13
|
+
duration_api_ms: number
|
|
14
|
+
num_turns: number
|
|
15
|
+
input_tokens: number
|
|
16
|
+
output_tokens: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type AgentProviderID = "claude" | "opencode" | "codex"
|
|
20
|
+
|
|
21
|
+
export interface AgentModelOption {
|
|
22
|
+
provider: AgentProviderID
|
|
23
|
+
model: string
|
|
24
|
+
label: string
|
|
25
|
+
description?: string
|
|
26
|
+
isDefault?: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Configuration for creating an agent session. */
|
|
30
|
+
export interface AgentSessionConfig {
|
|
31
|
+
systemPrompt?: string
|
|
32
|
+
tools?: string[]
|
|
33
|
+
allowedTools?: string[]
|
|
34
|
+
maxTurns?: number
|
|
35
|
+
cwd?: string
|
|
36
|
+
model?: string
|
|
37
|
+
effort?: string
|
|
38
|
+
signal?: AbortSignal
|
|
39
|
+
/** Escape hatch for provider-specific options (e.g. temperature, reasoning_effort). */
|
|
40
|
+
providerOptions?: Record<string, unknown>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A running agent session that yields events and accepts user messages. */
|
|
44
|
+
export interface AgentSession extends AsyncIterable<AgentEvent> {
|
|
45
|
+
/** Push a user message into the conversation. */
|
|
46
|
+
pushMessage(content: string): void
|
|
47
|
+
/** Signal that no more input will be sent (one-shot mode). */
|
|
48
|
+
endInput(): void
|
|
49
|
+
/** Close the session and release resources. Idempotent. */
|
|
50
|
+
close(): void
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Result of an authentication check. */
|
|
54
|
+
export type AuthResult =
|
|
55
|
+
| { authenticated: true; account: Record<string, unknown> & { email?: string } }
|
|
56
|
+
| { authenticated: false; error: string }
|
|
57
|
+
|
|
58
|
+
/** Interface that all agent SDK providers must implement. */
|
|
59
|
+
export interface AgentProvider {
|
|
60
|
+
readonly name: AgentProviderID | string
|
|
61
|
+
/** Create an interactive multi-turn session. */
|
|
62
|
+
createSession(config: AgentSessionConfig): AgentSession
|
|
63
|
+
/** Convenience: create a one-shot session with a single prompt. */
|
|
64
|
+
runOnce(prompt: string, config: AgentSessionConfig): AgentSession
|
|
65
|
+
/** Verify authentication with the provider. */
|
|
66
|
+
checkAuth(): Promise<AuthResult>
|
|
67
|
+
/** List models available through this provider. */
|
|
68
|
+
listModels?(cwd?: string, forceRefresh?: boolean): Promise<AgentModelOption[]>
|
|
69
|
+
/** Return the provider's configured default model, if available. */
|
|
70
|
+
getDefaultModel?(cwd?: string): Promise<string | null>
|
|
71
|
+
/** Release provider-owned resources. */
|
|
72
|
+
close?(): void | Promise<void>
|
|
73
|
+
}
|
package/src/lib/auth.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { getProvider, type AgentProviderID } from "./agent/index.ts"
|
|
2
|
+
import type { AuthResult } from "./agent/types.ts"
|
|
3
|
+
|
|
4
|
+
export type { AuthResult }
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Check if the user is authenticated with an agent provider.
|
|
8
|
+
*/
|
|
9
|
+
export async function checkAuth(provider: AgentProviderID = "claude"): Promise<AuthResult> {
|
|
10
|
+
return getProvider(provider).checkAuth()
|
|
11
|
+
}
|