kimaki 0.4.22 → 0.4.23

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.
@@ -0,0 +1,180 @@
1
+ import { spawn, type ChildProcess } from 'node:child_process'
2
+ import net from 'node:net'
3
+ import {
4
+ createOpencodeClient,
5
+ type OpencodeClient,
6
+ type Config,
7
+ } from '@opencode-ai/sdk'
8
+ import { createLogger } from './logger.js'
9
+
10
+ const opencodeLogger = createLogger('OPENCODE')
11
+
12
+ const opencodeServers = new Map<
13
+ string,
14
+ {
15
+ process: ChildProcess
16
+ client: OpencodeClient
17
+ port: number
18
+ }
19
+ >()
20
+
21
+ const serverRetryCount = new Map<string, number>()
22
+
23
+ async function getOpenPort(): Promise<number> {
24
+ return new Promise((resolve, reject) => {
25
+ const server = net.createServer()
26
+ server.listen(0, () => {
27
+ const address = server.address()
28
+ if (address && typeof address === 'object') {
29
+ const port = address.port
30
+ server.close(() => {
31
+ resolve(port)
32
+ })
33
+ } else {
34
+ reject(new Error('Failed to get port'))
35
+ }
36
+ })
37
+ server.on('error', reject)
38
+ })
39
+ }
40
+
41
+ async function waitForServer(port: number, maxAttempts = 30): Promise<boolean> {
42
+ for (let i = 0; i < maxAttempts; i++) {
43
+ try {
44
+ const endpoints = [
45
+ `http://localhost:${port}/api/health`,
46
+ `http://localhost:${port}/`,
47
+ `http://localhost:${port}/api`,
48
+ ]
49
+
50
+ for (const endpoint of endpoints) {
51
+ try {
52
+ const response = await fetch(endpoint)
53
+ if (response.status < 500) {
54
+ opencodeLogger.log(`Server ready on port `)
55
+ return true
56
+ }
57
+ } catch (e) {}
58
+ }
59
+ } catch (e) {}
60
+ await new Promise((resolve) => setTimeout(resolve, 1000))
61
+ }
62
+ throw new Error(
63
+ `Server did not start on port ${port} after ${maxAttempts} seconds`,
64
+ )
65
+ }
66
+
67
+ export async function initializeOpencodeForDirectory(directory: string) {
68
+ const existing = opencodeServers.get(directory)
69
+ if (existing && !existing.process.killed) {
70
+ opencodeLogger.log(
71
+ `Reusing existing server on port ${existing.port} for directory: ${directory}`,
72
+ )
73
+ return () => {
74
+ const entry = opencodeServers.get(directory)
75
+ if (!entry?.client) {
76
+ throw new Error(
77
+ `OpenCode server for directory "${directory}" is in an error state (no client available)`,
78
+ )
79
+ }
80
+ return entry.client
81
+ }
82
+ }
83
+
84
+ const port = await getOpenPort()
85
+
86
+ const opencodeCommand = process.env.OPENCODE_PATH || 'opencode'
87
+
88
+ const serverProcess = spawn(
89
+ opencodeCommand,
90
+ ['serve', '--port', port.toString()],
91
+ {
92
+ stdio: 'pipe',
93
+ detached: false,
94
+ cwd: directory,
95
+ env: {
96
+ ...process.env,
97
+ OPENCODE_CONFIG_CONTENT: JSON.stringify({
98
+ $schema: 'https://opencode.ai/config.json',
99
+ lsp: false,
100
+ formatter: false,
101
+ permission: {
102
+ edit: 'allow',
103
+ bash: 'allow',
104
+ webfetch: 'allow',
105
+ },
106
+ } satisfies Config),
107
+ OPENCODE_PORT: port.toString(),
108
+ },
109
+ },
110
+ )
111
+
112
+ serverProcess.stdout?.on('data', (data) => {
113
+ opencodeLogger.log(`opencode ${directory}: ${data.toString().trim()}`)
114
+ })
115
+
116
+ serverProcess.stderr?.on('data', (data) => {
117
+ opencodeLogger.error(`opencode ${directory}: ${data.toString().trim()}`)
118
+ })
119
+
120
+ serverProcess.on('error', (error) => {
121
+ opencodeLogger.error(`Failed to start server on port :`, port, error)
122
+ })
123
+
124
+ serverProcess.on('exit', (code) => {
125
+ opencodeLogger.log(
126
+ `Opencode server on ${directory} exited with code:`,
127
+ code,
128
+ )
129
+ opencodeServers.delete(directory)
130
+ if (code !== 0) {
131
+ const retryCount = serverRetryCount.get(directory) || 0
132
+ if (retryCount < 5) {
133
+ serverRetryCount.set(directory, retryCount + 1)
134
+ opencodeLogger.log(
135
+ `Restarting server for directory: ${directory} (attempt ${retryCount + 1}/5)`,
136
+ )
137
+ initializeOpencodeForDirectory(directory).catch((e) => {
138
+ opencodeLogger.error(`Failed to restart opencode server:`, e)
139
+ })
140
+ } else {
141
+ opencodeLogger.error(
142
+ `Server for ${directory} crashed too many times (5), not restarting`,
143
+ )
144
+ }
145
+ } else {
146
+ serverRetryCount.delete(directory)
147
+ }
148
+ })
149
+
150
+ await waitForServer(port)
151
+
152
+ const client = createOpencodeClient({
153
+ baseUrl: `http://localhost:${port}`,
154
+ fetch: (request: Request) =>
155
+ fetch(request, {
156
+ // @ts-ignore
157
+ timeout: false,
158
+ }),
159
+ })
160
+
161
+ opencodeServers.set(directory, {
162
+ process: serverProcess,
163
+ client,
164
+ port,
165
+ })
166
+
167
+ return () => {
168
+ const entry = opencodeServers.get(directory)
169
+ if (!entry?.client) {
170
+ throw new Error(
171
+ `OpenCode server for directory "${directory}" is in an error state (no client available)`,
172
+ )
173
+ }
174
+ return entry.client
175
+ }
176
+ }
177
+
178
+ export function getOpencodeServers() {
179
+ return opencodeServers
180
+ }