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.
Files changed (94) hide show
  1. package/bin.js +70 -0
  2. package/dist/ai-tool-to-genai.js +210 -0
  3. package/dist/ai-tool-to-genai.test.js +267 -0
  4. package/dist/channel-management.js +97 -0
  5. package/dist/cli.js +709 -0
  6. package/dist/commands/abort.js +78 -0
  7. package/dist/commands/add-project.js +98 -0
  8. package/dist/commands/agent.js +152 -0
  9. package/dist/commands/ask-question.js +183 -0
  10. package/dist/commands/create-new-project.js +78 -0
  11. package/dist/commands/fork.js +186 -0
  12. package/dist/commands/model.js +313 -0
  13. package/dist/commands/permissions.js +126 -0
  14. package/dist/commands/queue.js +129 -0
  15. package/dist/commands/resume.js +145 -0
  16. package/dist/commands/session.js +142 -0
  17. package/dist/commands/share.js +80 -0
  18. package/dist/commands/types.js +2 -0
  19. package/dist/commands/undo-redo.js +161 -0
  20. package/dist/commands/user-command.js +145 -0
  21. package/dist/database.js +184 -0
  22. package/dist/discord-bot.js +384 -0
  23. package/dist/discord-utils.js +217 -0
  24. package/dist/escape-backticks.test.js +410 -0
  25. package/dist/format-tables.js +96 -0
  26. package/dist/format-tables.test.js +418 -0
  27. package/dist/genai-worker-wrapper.js +109 -0
  28. package/dist/genai-worker.js +297 -0
  29. package/dist/genai.js +232 -0
  30. package/dist/interaction-handler.js +144 -0
  31. package/dist/logger.js +51 -0
  32. package/dist/markdown.js +310 -0
  33. package/dist/markdown.test.js +262 -0
  34. package/dist/message-formatting.js +273 -0
  35. package/dist/message-formatting.test.js +73 -0
  36. package/dist/openai-realtime.js +228 -0
  37. package/dist/opencode.js +216 -0
  38. package/dist/session-handler.js +580 -0
  39. package/dist/system-message.js +61 -0
  40. package/dist/tools.js +356 -0
  41. package/dist/utils.js +85 -0
  42. package/dist/voice-handler.js +541 -0
  43. package/dist/voice.js +314 -0
  44. package/dist/worker-types.js +4 -0
  45. package/dist/xml.js +92 -0
  46. package/dist/xml.test.js +32 -0
  47. package/package.json +60 -0
  48. package/src/__snapshots__/compact-session-context-no-system.md +35 -0
  49. package/src/__snapshots__/compact-session-context.md +47 -0
  50. package/src/ai-tool-to-genai.test.ts +296 -0
  51. package/src/ai-tool-to-genai.ts +255 -0
  52. package/src/channel-management.ts +161 -0
  53. package/src/cli.ts +1010 -0
  54. package/src/commands/abort.ts +94 -0
  55. package/src/commands/add-project.ts +139 -0
  56. package/src/commands/agent.ts +201 -0
  57. package/src/commands/ask-question.ts +276 -0
  58. package/src/commands/create-new-project.ts +111 -0
  59. package/src/commands/fork.ts +257 -0
  60. package/src/commands/model.ts +402 -0
  61. package/src/commands/permissions.ts +146 -0
  62. package/src/commands/queue.ts +181 -0
  63. package/src/commands/resume.ts +230 -0
  64. package/src/commands/session.ts +184 -0
  65. package/src/commands/share.ts +96 -0
  66. package/src/commands/types.ts +25 -0
  67. package/src/commands/undo-redo.ts +213 -0
  68. package/src/commands/user-command.ts +178 -0
  69. package/src/database.ts +220 -0
  70. package/src/discord-bot.ts +513 -0
  71. package/src/discord-utils.ts +282 -0
  72. package/src/escape-backticks.test.ts +447 -0
  73. package/src/format-tables.test.ts +440 -0
  74. package/src/format-tables.ts +110 -0
  75. package/src/genai-worker-wrapper.ts +160 -0
  76. package/src/genai-worker.ts +366 -0
  77. package/src/genai.ts +321 -0
  78. package/src/interaction-handler.ts +187 -0
  79. package/src/logger.ts +57 -0
  80. package/src/markdown.test.ts +358 -0
  81. package/src/markdown.ts +365 -0
  82. package/src/message-formatting.test.ts +81 -0
  83. package/src/message-formatting.ts +340 -0
  84. package/src/openai-realtime.ts +363 -0
  85. package/src/opencode.ts +277 -0
  86. package/src/session-handler.ts +758 -0
  87. package/src/system-message.ts +62 -0
  88. package/src/tools.ts +428 -0
  89. package/src/utils.ts +118 -0
  90. package/src/voice-handler.ts +760 -0
  91. package/src/voice.ts +432 -0
  92. package/src/worker-types.ts +66 -0
  93. package/src/xml.test.ts +37 -0
  94. package/src/xml.ts +121 -0
@@ -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
+ }