@vibe-forge/adapter-opencode 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/AGENTS.md +13 -0
- package/LICENSE +21 -0
- package/__tests__/runtime-common.spec.ts +95 -0
- package/__tests__/runtime-config.spec.ts +124 -0
- package/__tests__/runtime-permissions.spec.ts +181 -0
- package/__tests__/runtime-test-helpers.ts +142 -0
- package/__tests__/session-runtime-config.spec.ts +156 -0
- package/__tests__/session-runtime-direct.spec.ts +141 -0
- package/__tests__/session-runtime-stream.spec.ts +181 -0
- package/package.json +59 -0
- package/src/AGENTS.md +38 -0
- package/src/adapter-config.ts +21 -0
- package/src/icon.ts +17 -0
- package/src/index.ts +11 -0
- package/src/models.ts +24 -0
- package/src/paths.ts +30 -0
- package/src/runtime/common/agent.ts +11 -0
- package/src/runtime/common/inline-config.ts +45 -0
- package/src/runtime/common/mcp.ts +47 -0
- package/src/runtime/common/model.ts +115 -0
- package/src/runtime/common/object-utils.ts +35 -0
- package/src/runtime/common/permission-node.ts +119 -0
- package/src/runtime/common/permissions.ts +73 -0
- package/src/runtime/common/prompt.ts +56 -0
- package/src/runtime/common/session-records.ts +61 -0
- package/src/runtime/common/tools.ts +129 -0
- package/src/runtime/common.ts +17 -0
- package/src/runtime/init.ts +55 -0
- package/src/runtime/session/child-env.ts +100 -0
- package/src/runtime/session/direct.ts +109 -0
- package/src/runtime/session/process.ts +55 -0
- package/src/runtime/session/shared.ts +72 -0
- package/src/runtime/session/skill-config.ts +84 -0
- package/src/runtime/session/stream.ts +198 -0
- package/src/runtime/session.ts +13 -0
- package/src/schema.ts +1 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import process from 'node:process'
|
|
2
|
+
|
|
3
|
+
import type { AdapterCtx, AdapterEvent, AdapterOutputEvent, AdapterQueryOptions, AdapterSession } from '@vibe-forge/core/adapter'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_OPENCODE_TOOLS,
|
|
7
|
+
buildOpenCodeRunArgs,
|
|
8
|
+
buildOpenCodeSessionTitle,
|
|
9
|
+
normalizeOpenCodePrompt,
|
|
10
|
+
resolveOpenCodeAgent
|
|
11
|
+
} from '../common'
|
|
12
|
+
import { resolveOpenCodeBinaryPath } from '../../paths'
|
|
13
|
+
import { buildChildEnv, ensureSystemPromptFile } from './child-env'
|
|
14
|
+
import { findOpenCodeSessionId, runOpenCodeCommand } from './process'
|
|
15
|
+
import { createAssistantMessage, getErrorMessage, resolveAdapterConfig, stripAnsi, toAdapterErrorData } from './shared'
|
|
16
|
+
|
|
17
|
+
export const createStreamOpenCodeSession = async (
|
|
18
|
+
ctx: AdapterCtx,
|
|
19
|
+
options: AdapterQueryOptions
|
|
20
|
+
): Promise<AdapterSession> => {
|
|
21
|
+
const adapterConfig = resolveAdapterConfig(ctx)
|
|
22
|
+
const agent = resolveOpenCodeAgent({
|
|
23
|
+
agent: adapterConfig.agent,
|
|
24
|
+
planAgent: adapterConfig.planAgent,
|
|
25
|
+
permissionMode: options.permissionMode
|
|
26
|
+
})
|
|
27
|
+
const binaryPath = resolveOpenCodeBinaryPath(ctx.env)
|
|
28
|
+
const title = buildOpenCodeSessionTitle(options.sessionId, adapterConfig.titlePrefix)
|
|
29
|
+
const systemPromptFile = await ensureSystemPromptFile(ctx, options)
|
|
30
|
+
const cachedSession = options.type === 'resume' ? await ctx.cache.get('adapter.opencode.session') : undefined
|
|
31
|
+
|
|
32
|
+
if (options.type === 'create') await ctx.cache.set('adapter.opencode.session', { title })
|
|
33
|
+
|
|
34
|
+
options.onEvent({
|
|
35
|
+
type: 'init',
|
|
36
|
+
data: {
|
|
37
|
+
uuid: options.sessionId,
|
|
38
|
+
model: options.model ?? 'default',
|
|
39
|
+
version: 'unknown',
|
|
40
|
+
tools: DEFAULT_OPENCODE_TOOLS,
|
|
41
|
+
slashCommands: [],
|
|
42
|
+
cwd: ctx.cwd,
|
|
43
|
+
agents: agent ? [agent] : [],
|
|
44
|
+
title
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
let destroyed = false
|
|
49
|
+
let currentPid: number | undefined
|
|
50
|
+
let currentKill: (() => void) | undefined
|
|
51
|
+
let opencodeSessionId = cachedSession?.opencodeSessionId
|
|
52
|
+
let didEmitFatalError = false
|
|
53
|
+
|
|
54
|
+
const emitEvent = (event: AdapterOutputEvent) => {
|
|
55
|
+
if (event.type === 'error' && event.data.fatal !== false) {
|
|
56
|
+
didEmitFatalError = true
|
|
57
|
+
}
|
|
58
|
+
options.onEvent(event)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const emitUnexpectedExit = (error: unknown) => {
|
|
62
|
+
if (destroyed) return
|
|
63
|
+
destroyed = true
|
|
64
|
+
currentPid = undefined
|
|
65
|
+
currentKill = undefined
|
|
66
|
+
ctx.logger.error('OpenCode session turn failed unexpectedly', { err: error })
|
|
67
|
+
emitEvent({ type: 'error', data: toAdapterErrorData(error) })
|
|
68
|
+
emitEvent({ type: 'exit', data: { exitCode: 1, stderr: getErrorMessage(error) } })
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const runTurn = async (content: Extract<AdapterEvent, { type: 'message' }>, allowRetry: boolean): Promise<void> => {
|
|
72
|
+
if (destroyed) return
|
|
73
|
+
const normalized = normalizeOpenCodePrompt(content.content)
|
|
74
|
+
const { cliModel, env } = await buildChildEnv({ ctx, options, adapterConfig, systemPromptFile })
|
|
75
|
+
|
|
76
|
+
if (opencodeSessionId == null && options.type === 'resume') {
|
|
77
|
+
opencodeSessionId = await findOpenCodeSessionId({
|
|
78
|
+
binaryPath,
|
|
79
|
+
cwd: ctx.cwd,
|
|
80
|
+
env,
|
|
81
|
+
title,
|
|
82
|
+
maxCount: adapterConfig.sessionListMaxCount ?? 50,
|
|
83
|
+
logger: ctx.logger
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const result = await runOpenCodeCommand({
|
|
88
|
+
binaryPath,
|
|
89
|
+
args: buildOpenCodeRunArgs({
|
|
90
|
+
prompt: normalized.prompt,
|
|
91
|
+
files: normalized.files,
|
|
92
|
+
model: cliModel,
|
|
93
|
+
agent,
|
|
94
|
+
share: adapterConfig.share,
|
|
95
|
+
title,
|
|
96
|
+
opencodeSessionId,
|
|
97
|
+
extraOptions: options.extraOptions
|
|
98
|
+
}),
|
|
99
|
+
cwd: ctx.cwd,
|
|
100
|
+
env,
|
|
101
|
+
onStart: (pid) => {
|
|
102
|
+
currentPid = pid
|
|
103
|
+
currentKill = () => {
|
|
104
|
+
if (pid != null) {
|
|
105
|
+
try {
|
|
106
|
+
process.kill(pid, 'SIGINT')
|
|
107
|
+
} catch {
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
currentPid = undefined
|
|
115
|
+
currentKill = undefined
|
|
116
|
+
if (destroyed) return
|
|
117
|
+
|
|
118
|
+
const output = stripAnsi(result.stdout).trim()
|
|
119
|
+
const error = stripAnsi(result.stderr).trim()
|
|
120
|
+
if (result.exitCode !== 0) {
|
|
121
|
+
const missingSession = /session.+not found|no session found/i.test(`${output}\n${error}`)
|
|
122
|
+
if (missingSession && opencodeSessionId != null && allowRetry) {
|
|
123
|
+
opencodeSessionId = undefined
|
|
124
|
+
await ctx.cache.set('adapter.opencode.session', { title })
|
|
125
|
+
await runTurn(content, false)
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
if (!didEmitFatalError) {
|
|
129
|
+
emitEvent({
|
|
130
|
+
type: 'error',
|
|
131
|
+
data: toAdapterErrorData(result.stderr || result.stdout || `Process exited with code ${result.exitCode}`, {
|
|
132
|
+
details: {
|
|
133
|
+
exitCode: result.exitCode,
|
|
134
|
+
stdout: result.stdout,
|
|
135
|
+
stderr: result.stderr
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
emitEvent({ type: 'exit', data: { exitCode: result.exitCode, stderr: result.stderr || result.stdout } })
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const resolvedSessionId = await findOpenCodeSessionId({
|
|
145
|
+
binaryPath,
|
|
146
|
+
cwd: ctx.cwd,
|
|
147
|
+
env,
|
|
148
|
+
title,
|
|
149
|
+
maxCount: adapterConfig.sessionListMaxCount ?? 50,
|
|
150
|
+
logger: ctx.logger
|
|
151
|
+
})
|
|
152
|
+
if (resolvedSessionId) {
|
|
153
|
+
opencodeSessionId = resolvedSessionId
|
|
154
|
+
await ctx.cache.set('adapter.opencode.session', { opencodeSessionId, title })
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const assistantMessage = createAssistantMessage(
|
|
158
|
+
output === '' ? '[OpenCode completed without text output]' : output,
|
|
159
|
+
cliModel
|
|
160
|
+
)
|
|
161
|
+
emitEvent({ type: 'message', data: assistantMessage })
|
|
162
|
+
emitEvent({ type: 'stop', data: assistantMessage })
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let queue = Promise.resolve()
|
|
166
|
+
const enqueueMessage = (event: Extract<AdapterEvent, { type: 'message' }>) => {
|
|
167
|
+
queue = queue.catch(() => undefined).then(async () => {
|
|
168
|
+
try {
|
|
169
|
+
await runTurn(event, true)
|
|
170
|
+
} catch (error) {
|
|
171
|
+
emitUnexpectedExit(error)
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (options.description != null && options.description.trim() !== '') {
|
|
177
|
+
enqueueMessage({ type: 'message', content: [{ type: 'text', text: options.description }] })
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
kill: () => {
|
|
182
|
+
destroyed = true
|
|
183
|
+
currentKill?.()
|
|
184
|
+
},
|
|
185
|
+
emit: (event) => {
|
|
186
|
+
if (destroyed) return
|
|
187
|
+
if (event.type === 'message') enqueueMessage(event)
|
|
188
|
+
if (event.type === 'interrupt') currentKill?.()
|
|
189
|
+
if (event.type === 'stop') {
|
|
190
|
+
destroyed = true
|
|
191
|
+
currentKill?.()
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
get pid() {
|
|
195
|
+
return currentPid
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AdapterCtx, AdapterQueryOptions, AdapterSession } from '@vibe-forge/core/adapter'
|
|
2
|
+
|
|
3
|
+
import { createDirectOpenCodeSession } from './session/direct'
|
|
4
|
+
import { createStreamOpenCodeSession } from './session/stream'
|
|
5
|
+
|
|
6
|
+
export const createOpenCodeSession = async (
|
|
7
|
+
ctx: AdapterCtx,
|
|
8
|
+
options: AdapterQueryOptions
|
|
9
|
+
): Promise<AdapterSession> => (
|
|
10
|
+
options.mode === 'direct'
|
|
11
|
+
? createDirectOpenCodeSession(ctx, options)
|
|
12
|
+
: createStreamOpenCodeSession(ctx, options)
|
|
13
|
+
)
|
package/src/schema.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|