@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.
Files changed (36) hide show
  1. package/AGENTS.md +13 -0
  2. package/LICENSE +21 -0
  3. package/__tests__/runtime-common.spec.ts +95 -0
  4. package/__tests__/runtime-config.spec.ts +124 -0
  5. package/__tests__/runtime-permissions.spec.ts +181 -0
  6. package/__tests__/runtime-test-helpers.ts +142 -0
  7. package/__tests__/session-runtime-config.spec.ts +156 -0
  8. package/__tests__/session-runtime-direct.spec.ts +141 -0
  9. package/__tests__/session-runtime-stream.spec.ts +181 -0
  10. package/package.json +59 -0
  11. package/src/AGENTS.md +38 -0
  12. package/src/adapter-config.ts +21 -0
  13. package/src/icon.ts +17 -0
  14. package/src/index.ts +11 -0
  15. package/src/models.ts +24 -0
  16. package/src/paths.ts +30 -0
  17. package/src/runtime/common/agent.ts +11 -0
  18. package/src/runtime/common/inline-config.ts +45 -0
  19. package/src/runtime/common/mcp.ts +47 -0
  20. package/src/runtime/common/model.ts +115 -0
  21. package/src/runtime/common/object-utils.ts +35 -0
  22. package/src/runtime/common/permission-node.ts +119 -0
  23. package/src/runtime/common/permissions.ts +73 -0
  24. package/src/runtime/common/prompt.ts +56 -0
  25. package/src/runtime/common/session-records.ts +61 -0
  26. package/src/runtime/common/tools.ts +129 -0
  27. package/src/runtime/common.ts +17 -0
  28. package/src/runtime/init.ts +55 -0
  29. package/src/runtime/session/child-env.ts +100 -0
  30. package/src/runtime/session/direct.ts +109 -0
  31. package/src/runtime/session/process.ts +55 -0
  32. package/src/runtime/session/shared.ts +72 -0
  33. package/src/runtime/session/skill-config.ts +84 -0
  34. package/src/runtime/session/stream.ts +198 -0
  35. package/src/runtime/session.ts +13 -0
  36. 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 {}