shuvmaki 0.4.26
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/bin.js +70 -0
- package/dist/ai-tool-to-genai.js +210 -0
- package/dist/ai-tool-to-genai.test.js +267 -0
- package/dist/channel-management.js +97 -0
- package/dist/cli.js +709 -0
- package/dist/commands/abort.js +78 -0
- package/dist/commands/add-project.js +98 -0
- package/dist/commands/agent.js +152 -0
- package/dist/commands/ask-question.js +183 -0
- package/dist/commands/create-new-project.js +78 -0
- package/dist/commands/fork.js +186 -0
- package/dist/commands/model.js +313 -0
- package/dist/commands/permissions.js +126 -0
- package/dist/commands/queue.js +129 -0
- package/dist/commands/resume.js +145 -0
- package/dist/commands/session.js +142 -0
- package/dist/commands/share.js +80 -0
- package/dist/commands/types.js +2 -0
- package/dist/commands/undo-redo.js +161 -0
- package/dist/commands/user-command.js +145 -0
- package/dist/database.js +184 -0
- package/dist/discord-bot.js +384 -0
- package/dist/discord-utils.js +217 -0
- package/dist/escape-backticks.test.js +410 -0
- package/dist/format-tables.js +96 -0
- package/dist/format-tables.test.js +418 -0
- package/dist/genai-worker-wrapper.js +109 -0
- package/dist/genai-worker.js +297 -0
- package/dist/genai.js +232 -0
- package/dist/interaction-handler.js +144 -0
- package/dist/logger.js +51 -0
- package/dist/markdown.js +310 -0
- package/dist/markdown.test.js +262 -0
- package/dist/message-formatting.js +273 -0
- package/dist/message-formatting.test.js +73 -0
- package/dist/openai-realtime.js +228 -0
- package/dist/opencode.js +216 -0
- package/dist/session-handler.js +580 -0
- package/dist/system-message.js +61 -0
- package/dist/tools.js +356 -0
- package/dist/utils.js +85 -0
- package/dist/voice-handler.js +541 -0
- package/dist/voice.js +314 -0
- package/dist/worker-types.js +4 -0
- package/dist/xml.js +92 -0
- package/dist/xml.test.js +32 -0
- package/package.json +60 -0
- package/src/__snapshots__/compact-session-context-no-system.md +35 -0
- package/src/__snapshots__/compact-session-context.md +47 -0
- package/src/ai-tool-to-genai.test.ts +296 -0
- package/src/ai-tool-to-genai.ts +255 -0
- package/src/channel-management.ts +161 -0
- package/src/cli.ts +1010 -0
- package/src/commands/abort.ts +94 -0
- package/src/commands/add-project.ts +139 -0
- package/src/commands/agent.ts +201 -0
- package/src/commands/ask-question.ts +276 -0
- package/src/commands/create-new-project.ts +111 -0
- package/src/commands/fork.ts +257 -0
- package/src/commands/model.ts +402 -0
- package/src/commands/permissions.ts +146 -0
- package/src/commands/queue.ts +181 -0
- package/src/commands/resume.ts +230 -0
- package/src/commands/session.ts +184 -0
- package/src/commands/share.ts +96 -0
- package/src/commands/types.ts +25 -0
- package/src/commands/undo-redo.ts +213 -0
- package/src/commands/user-command.ts +178 -0
- package/src/database.ts +220 -0
- package/src/discord-bot.ts +513 -0
- package/src/discord-utils.ts +282 -0
- package/src/escape-backticks.test.ts +447 -0
- package/src/format-tables.test.ts +440 -0
- package/src/format-tables.ts +110 -0
- package/src/genai-worker-wrapper.ts +160 -0
- package/src/genai-worker.ts +366 -0
- package/src/genai.ts +321 -0
- package/src/interaction-handler.ts +187 -0
- package/src/logger.ts +57 -0
- package/src/markdown.test.ts +358 -0
- package/src/markdown.ts +365 -0
- package/src/message-formatting.test.ts +81 -0
- package/src/message-formatting.ts +340 -0
- package/src/openai-realtime.ts +363 -0
- package/src/opencode.ts +277 -0
- package/src/session-handler.ts +758 -0
- package/src/system-message.ts +62 -0
- package/src/tools.ts +428 -0
- package/src/utils.ts +118 -0
- package/src/voice-handler.ts +760 -0
- package/src/voice.ts +432 -0
- package/src/worker-types.ts +66 -0
- package/src/xml.test.ts +37 -0
- package/src/xml.ts +121 -0
package/src/opencode.ts
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
// OpenCode server process manager.
|
|
2
|
+
// Spawns and maintains OpenCode API servers per project directory,
|
|
3
|
+
// handles automatic restarts on failure, and provides typed SDK clients.
|
|
4
|
+
|
|
5
|
+
import { spawn, type ChildProcess } from 'node:child_process'
|
|
6
|
+
import fs from 'node:fs'
|
|
7
|
+
import net from 'node:net'
|
|
8
|
+
import {
|
|
9
|
+
createOpencodeClient,
|
|
10
|
+
type OpencodeClient,
|
|
11
|
+
type Config,
|
|
12
|
+
} from '@opencode-ai/sdk'
|
|
13
|
+
import {
|
|
14
|
+
createOpencodeClient as createOpencodeClientV2,
|
|
15
|
+
type OpencodeClient as OpencodeClientV2,
|
|
16
|
+
} from '@opencode-ai/sdk/v2'
|
|
17
|
+
import { createLogger } from './logger.js'
|
|
18
|
+
|
|
19
|
+
const opencodeLogger = createLogger('OPENCODE')
|
|
20
|
+
|
|
21
|
+
const opencodeServers = new Map<
|
|
22
|
+
string,
|
|
23
|
+
{
|
|
24
|
+
process: ChildProcess
|
|
25
|
+
client: OpencodeClient
|
|
26
|
+
clientV2: OpencodeClientV2
|
|
27
|
+
port: number
|
|
28
|
+
}
|
|
29
|
+
>()
|
|
30
|
+
|
|
31
|
+
const serverRetryCount = new Map<string, number>()
|
|
32
|
+
|
|
33
|
+
async function getOpenPort(): Promise<number> {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const server = net.createServer()
|
|
36
|
+
server.listen(0, () => {
|
|
37
|
+
const address = server.address()
|
|
38
|
+
if (address && typeof address === 'object') {
|
|
39
|
+
const port = address.port
|
|
40
|
+
server.close(() => {
|
|
41
|
+
resolve(port)
|
|
42
|
+
})
|
|
43
|
+
} else {
|
|
44
|
+
reject(new Error('Failed to get port'))
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
server.on('error', reject)
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function waitForServer(port: number, maxAttempts = 30): Promise<boolean> {
|
|
52
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
53
|
+
try {
|
|
54
|
+
const endpoints = [
|
|
55
|
+
`http://127.0.0.1:${port}/api/health`,
|
|
56
|
+
`http://127.0.0.1:${port}/`,
|
|
57
|
+
`http://127.0.0.1:${port}/api`,
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
for (const endpoint of endpoints) {
|
|
61
|
+
try {
|
|
62
|
+
const response = await fetch(endpoint)
|
|
63
|
+
if (response.status < 500) {
|
|
64
|
+
return true
|
|
65
|
+
}
|
|
66
|
+
const body = await response.text()
|
|
67
|
+
// Fatal errors that won't resolve with retrying
|
|
68
|
+
if (body.includes('BunInstallFailedError')) {
|
|
69
|
+
throw new Error(`Server failed to start: ${body.slice(0, 200)}`)
|
|
70
|
+
}
|
|
71
|
+
} catch (e) {
|
|
72
|
+
// Re-throw fatal errors
|
|
73
|
+
if ((e as Error).message?.includes('Server failed to start')) {
|
|
74
|
+
throw e
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} catch (e) {
|
|
79
|
+
// Re-throw fatal errors that won't resolve with retrying
|
|
80
|
+
if ((e as Error).message?.includes('Server failed to start')) {
|
|
81
|
+
throw e
|
|
82
|
+
}
|
|
83
|
+
opencodeLogger.debug(
|
|
84
|
+
`Server polling attempt failed: ${(e as Error).message}`,
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
88
|
+
}
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Server did not start on port ${port} after ${maxAttempts} seconds`,
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function initializeOpencodeForDirectory(directory: string) {
|
|
95
|
+
const existing = opencodeServers.get(directory)
|
|
96
|
+
if (existing && !existing.process.killed) {
|
|
97
|
+
opencodeLogger.log(
|
|
98
|
+
`Reusing existing server on port ${existing.port} for directory: ${directory}`,
|
|
99
|
+
)
|
|
100
|
+
return () => {
|
|
101
|
+
const entry = opencodeServers.get(directory)
|
|
102
|
+
if (!entry?.client) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`OpenCode server for directory "${directory}" is in an error state (no client available)`,
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
return entry.client
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Verify directory exists and is accessible before spawning
|
|
112
|
+
try {
|
|
113
|
+
fs.accessSync(directory, fs.constants.R_OK | fs.constants.X_OK)
|
|
114
|
+
} catch {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`Directory does not exist or is not accessible: ${directory}`,
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const port = await getOpenPort()
|
|
121
|
+
|
|
122
|
+
// Look for shuvcode first (preferred fork), then opencode
|
|
123
|
+
const opencodeCommand = (() => {
|
|
124
|
+
if (process.env.OPENCODE_PATH) {
|
|
125
|
+
return process.env.OPENCODE_PATH
|
|
126
|
+
}
|
|
127
|
+
const possiblePaths = [
|
|
128
|
+
`${process.env.HOME}/.bun/bin/shuvcode`,
|
|
129
|
+
`${process.env.HOME}/.local/bin/shuvcode`,
|
|
130
|
+
`${process.env.HOME}/.bun/bin/opencode`,
|
|
131
|
+
`${process.env.HOME}/.local/bin/opencode`,
|
|
132
|
+
`${process.env.HOME}/.opencode/bin/opencode`,
|
|
133
|
+
'/usr/local/bin/shuvcode',
|
|
134
|
+
'/usr/local/bin/opencode',
|
|
135
|
+
]
|
|
136
|
+
for (const p of possiblePaths) {
|
|
137
|
+
try {
|
|
138
|
+
fs.accessSync(p, fs.constants.X_OK)
|
|
139
|
+
return p
|
|
140
|
+
} catch {
|
|
141
|
+
// continue
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Fallback to PATH lookup
|
|
145
|
+
return 'shuvcode'
|
|
146
|
+
})()
|
|
147
|
+
|
|
148
|
+
const serverProcess = spawn(
|
|
149
|
+
opencodeCommand,
|
|
150
|
+
['serve', '--port', port.toString()],
|
|
151
|
+
{
|
|
152
|
+
stdio: 'pipe',
|
|
153
|
+
detached: false,
|
|
154
|
+
cwd: directory,
|
|
155
|
+
env: {
|
|
156
|
+
...process.env,
|
|
157
|
+
OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
|
158
|
+
$schema: 'https://opencode.ai/config.json',
|
|
159
|
+
lsp: false,
|
|
160
|
+
formatter: false,
|
|
161
|
+
permission: {
|
|
162
|
+
edit: 'allow',
|
|
163
|
+
bash: 'allow',
|
|
164
|
+
webfetch: 'allow',
|
|
165
|
+
},
|
|
166
|
+
} satisfies Config),
|
|
167
|
+
OPENCODE_PORT: port.toString(),
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
// Buffer logs until we know if server started successfully
|
|
173
|
+
const logBuffer: string[] = []
|
|
174
|
+
logBuffer.push(
|
|
175
|
+
`Spawned opencode serve --port ${port} in ${directory} (pid: ${serverProcess.pid})`,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
serverProcess.stdout?.on('data', (data) => {
|
|
179
|
+
logBuffer.push(`[stdout] ${data.toString().trim()}`)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
serverProcess.stderr?.on('data', (data) => {
|
|
183
|
+
logBuffer.push(`[stderr] ${data.toString().trim()}`)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
serverProcess.on('error', (error) => {
|
|
187
|
+
logBuffer.push(`Failed to start server on port ${port}: ${error}`)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
serverProcess.on('exit', (code) => {
|
|
191
|
+
opencodeLogger.log(
|
|
192
|
+
`Opencode server on ${directory} exited with code:`,
|
|
193
|
+
code,
|
|
194
|
+
)
|
|
195
|
+
opencodeServers.delete(directory)
|
|
196
|
+
if (code !== 0) {
|
|
197
|
+
const retryCount = serverRetryCount.get(directory) || 0
|
|
198
|
+
if (retryCount < 5) {
|
|
199
|
+
serverRetryCount.set(directory, retryCount + 1)
|
|
200
|
+
opencodeLogger.log(
|
|
201
|
+
`Restarting server for directory: ${directory} (attempt ${retryCount + 1}/5)`,
|
|
202
|
+
)
|
|
203
|
+
initializeOpencodeForDirectory(directory).catch((e) => {
|
|
204
|
+
opencodeLogger.error(`Failed to restart opencode server:`, e)
|
|
205
|
+
})
|
|
206
|
+
} else {
|
|
207
|
+
opencodeLogger.error(
|
|
208
|
+
`Server for ${directory} crashed too many times (5), not restarting`,
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
serverRetryCount.delete(directory)
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
await waitForServer(port)
|
|
218
|
+
opencodeLogger.log(`Server ready on port ${port}`)
|
|
219
|
+
} catch (e) {
|
|
220
|
+
// Dump buffered logs on failure
|
|
221
|
+
opencodeLogger.error(`Server failed to start for ${directory}:`)
|
|
222
|
+
for (const line of logBuffer) {
|
|
223
|
+
opencodeLogger.error(` ${line}`)
|
|
224
|
+
}
|
|
225
|
+
throw e
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const baseUrl = `http://127.0.0.1:${port}`
|
|
229
|
+
const fetchWithTimeout = (request: Request) =>
|
|
230
|
+
fetch(request, {
|
|
231
|
+
// @ts-ignore
|
|
232
|
+
timeout: false,
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
const client = createOpencodeClient({
|
|
236
|
+
baseUrl,
|
|
237
|
+
fetch: fetchWithTimeout,
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
const clientV2 = createOpencodeClientV2({
|
|
241
|
+
baseUrl,
|
|
242
|
+
fetch: fetchWithTimeout as typeof fetch,
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
opencodeServers.set(directory, {
|
|
246
|
+
process: serverProcess,
|
|
247
|
+
client,
|
|
248
|
+
clientV2,
|
|
249
|
+
port,
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
return () => {
|
|
253
|
+
const entry = opencodeServers.get(directory)
|
|
254
|
+
if (!entry?.client) {
|
|
255
|
+
throw new Error(
|
|
256
|
+
`OpenCode server for directory "${directory}" is in an error state (no client available)`,
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
return entry.client
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function getOpencodeServers() {
|
|
264
|
+
return opencodeServers
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function getOpencodeServerPort(directory: string): number | null {
|
|
268
|
+
const entry = opencodeServers.get(directory)
|
|
269
|
+
return entry?.port ?? null
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function getOpencodeClientV2(
|
|
273
|
+
directory: string,
|
|
274
|
+
): OpencodeClientV2 | null {
|
|
275
|
+
const entry = opencodeServers.get(directory)
|
|
276
|
+
return entry?.clientV2 ?? null
|
|
277
|
+
}
|