kimaki 0.4.76 → 0.4.78
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/dist/adapter-rest-boundary.test.js +34 -0
- package/dist/agent-model.e2e.test.js +2 -20
- package/dist/cli.js +50 -13
- package/dist/commands/channel-ref.js +16 -0
- package/dist/commands/diff.js +20 -85
- package/dist/commands/merge-worktree.js +5 -17
- package/dist/commands/new-worktree.js +5 -9
- package/dist/commands/permissions.js +77 -11
- package/dist/commands/resume.js +5 -9
- package/dist/commands/screenshare.js +295 -0
- package/dist/commands/session.js +6 -17
- package/dist/critique-utils.js +95 -0
- package/dist/diff-patch-plugin.js +314 -0
- package/dist/discord-bot.js +19 -14
- package/dist/discord-js-import-boundary.test.js +62 -0
- package/dist/discord-utils.js +44 -0
- package/dist/event-stream-real-capture.e2e.test.js +2 -20
- package/dist/gateway-proxy.e2e.test.js +2 -5
- package/dist/generated/cloudflare/browser.js +17 -0
- package/dist/generated/cloudflare/client.js +34 -0
- package/dist/generated/cloudflare/commonInputTypes.js +10 -0
- package/dist/generated/cloudflare/enums.js +48 -0
- package/dist/generated/cloudflare/internal/class.js +47 -0
- package/dist/generated/cloudflare/internal/prismaNamespace.js +252 -0
- package/dist/generated/cloudflare/internal/prismaNamespaceBrowser.js +222 -0
- package/dist/generated/cloudflare/internal/query_compiler_fast_bg.js +135 -0
- package/dist/generated/cloudflare/models/bot_api_keys.js +1 -0
- package/dist/generated/cloudflare/models/bot_tokens.js +1 -0
- package/dist/generated/cloudflare/models/channel_agents.js +1 -0
- package/dist/generated/cloudflare/models/channel_directories.js +1 -0
- package/dist/generated/cloudflare/models/channel_mention_mode.js +1 -0
- package/dist/generated/cloudflare/models/channel_models.js +1 -0
- package/dist/generated/cloudflare/models/channel_verbosity.js +1 -0
- package/dist/generated/cloudflare/models/channel_worktrees.js +1 -0
- package/dist/generated/cloudflare/models/forum_sync_configs.js +1 -0
- package/dist/generated/cloudflare/models/global_models.js +1 -0
- package/dist/generated/cloudflare/models/ipc_requests.js +1 -0
- package/dist/generated/cloudflare/models/part_messages.js +1 -0
- package/dist/generated/cloudflare/models/scheduled_tasks.js +1 -0
- package/dist/generated/cloudflare/models/session_agents.js +1 -0
- package/dist/generated/cloudflare/models/session_events.js +1 -0
- package/dist/generated/cloudflare/models/session_models.js +1 -0
- package/dist/generated/cloudflare/models/session_start_sources.js +1 -0
- package/dist/generated/cloudflare/models/thread_sessions.js +1 -0
- package/dist/generated/cloudflare/models/thread_worktrees.js +1 -0
- package/dist/generated/cloudflare/models.js +1 -0
- package/dist/generated/node/browser.js +17 -0
- package/dist/generated/node/client.js +37 -0
- package/dist/generated/node/commonInputTypes.js +10 -0
- package/dist/generated/node/enums.js +48 -0
- package/dist/generated/node/internal/class.js +49 -0
- package/dist/generated/node/internal/prismaNamespace.js +252 -0
- package/dist/generated/node/internal/prismaNamespaceBrowser.js +222 -0
- package/dist/generated/node/models/bot_api_keys.js +1 -0
- package/dist/generated/node/models/bot_tokens.js +1 -0
- package/dist/generated/node/models/channel_agents.js +1 -0
- package/dist/generated/node/models/channel_directories.js +1 -0
- package/dist/generated/node/models/channel_mention_mode.js +1 -0
- package/dist/generated/node/models/channel_models.js +1 -0
- package/dist/generated/node/models/channel_verbosity.js +1 -0
- package/dist/generated/node/models/channel_worktrees.js +1 -0
- package/dist/generated/node/models/forum_sync_configs.js +1 -0
- package/dist/generated/node/models/global_models.js +1 -0
- package/dist/generated/node/models/ipc_requests.js +1 -0
- package/dist/generated/node/models/part_messages.js +1 -0
- package/dist/generated/node/models/scheduled_tasks.js +1 -0
- package/dist/generated/node/models/session_agents.js +1 -0
- package/dist/generated/node/models/session_events.js +1 -0
- package/dist/generated/node/models/session_models.js +1 -0
- package/dist/generated/node/models/session_start_sources.js +1 -0
- package/dist/generated/node/models/thread_sessions.js +1 -0
- package/dist/generated/node/models/thread_worktrees.js +1 -0
- package/dist/generated/node/models.js +1 -0
- package/dist/interaction-handler.js +10 -0
- package/dist/kimaki-digital-twin.e2e.test.js +2 -20
- package/dist/message-flags-boundary.test.js +54 -0
- package/dist/message-formatting.js +3 -62
- package/dist/onboarding-tutorial-plugin.js +1 -1
- package/dist/opencode-command.js +129 -0
- package/dist/opencode-command.test.js +48 -0
- package/dist/opencode-interrupt-plugin.js +19 -1
- package/dist/opencode-interrupt-plugin.test.js +0 -5
- package/dist/opencode-plugin-loading.e2e.test.js +9 -20
- package/dist/opencode-plugin.js +4 -4
- package/dist/opencode.js +150 -27
- package/dist/patch-text-parser.js +97 -0
- package/dist/platform/components-v2.js +20 -0
- package/dist/platform/discord-adapter.js +1440 -0
- package/dist/platform/discord-routes.js +31 -0
- package/dist/platform/message-flags.js +8 -0
- package/dist/platform/platform-value.js +41 -0
- package/dist/platform/slack-adapter.js +872 -0
- package/dist/platform/slack-markdown.js +169 -0
- package/dist/platform/types.js +4 -0
- package/dist/queue-advanced-e2e-setup.js +265 -0
- package/dist/queue-advanced-footer.e2e.test.js +173 -0
- package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
- package/dist/queue-advanced-permissions-typing.e2e.test.js +73 -1
- package/dist/runtime-lifecycle.e2e.test.js +2 -20
- package/dist/session-handler/event-stream-state.js +5 -0
- package/dist/session-handler/event-stream-state.test.js +6 -2
- package/dist/session-handler/thread-session-runtime.js +32 -2
- package/dist/system-message.js +26 -23
- package/dist/test-utils.js +16 -0
- package/dist/thread-message-queue.e2e.test.js +2 -20
- package/dist/utils.js +3 -1
- package/dist/voice-message.e2e.test.js +2 -20
- package/dist/voice.js +122 -9
- package/dist/voice.test.js +17 -2
- package/dist/websockify.js +69 -0
- package/dist/worktree-lifecycle.e2e.test.js +308 -0
- package/package.json +4 -2
- package/skills/critique/SKILL.md +17 -0
- package/skills/egaki/SKILL.md +35 -0
- package/skills/event-sourcing-state/SKILL.md +252 -0
- package/skills/goke/SKILL.md +1 -0
- package/skills/npm-package/SKILL.md +21 -2
- package/skills/playwriter/SKILL.md +1 -1
- package/skills/x-articles/SKILL.md +554 -0
- package/src/agent-model.e2e.test.ts +4 -19
- package/src/cli.ts +60 -13
- package/src/commands/diff.ts +25 -99
- package/src/commands/merge-worktree.ts +5 -21
- package/src/commands/new-worktree.ts +5 -11
- package/src/commands/permissions.ts +100 -15
- package/src/commands/resume.ts +5 -12
- package/src/commands/screenshare.ts +354 -0
- package/src/commands/session.ts +6 -23
- package/src/critique-utils.ts +139 -0
- package/src/discord-bot.ts +20 -15
- package/src/discord-utils.ts +53 -0
- package/src/event-stream-real-capture.e2e.test.ts +4 -20
- package/src/gateway-proxy.e2e.test.ts +2 -5
- package/src/interaction-handler.ts +15 -0
- package/src/kimaki-digital-twin.e2e.test.ts +2 -21
- package/src/message-formatting.ts +3 -68
- package/src/onboarding-tutorial-plugin.ts +1 -1
- package/src/opencode-command.test.ts +70 -0
- package/src/opencode-command.ts +188 -0
- package/src/opencode-interrupt-plugin.test.ts +0 -5
- package/src/opencode-interrupt-plugin.ts +34 -1
- package/src/opencode-plugin-loading.e2e.test.ts +25 -35
- package/src/opencode-plugin.ts +5 -4
- package/src/opencode.ts +199 -32
- package/src/patch-text-parser.ts +107 -0
- package/src/queue-advanced-e2e-setup.ts +273 -0
- package/src/queue-advanced-footer.e2e.test.ts +211 -0
- package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
- package/src/queue-advanced-permissions-typing.e2e.test.ts +92 -0
- package/src/runtime-lifecycle.e2e.test.ts +4 -19
- package/src/session-handler/event-stream-state.test.ts +6 -2
- package/src/session-handler/event-stream-state.ts +5 -0
- package/src/session-handler/thread-session-runtime.ts +45 -2
- package/src/system-message.ts +26 -23
- package/src/test-utils.ts +17 -0
- package/src/thread-message-queue.e2e.test.ts +2 -20
- package/src/utils.ts +3 -1
- package/src/voice-message.e2e.test.ts +3 -20
- package/src/voice.test.ts +26 -2
- package/src/voice.ts +147 -9
- package/src/websockify.ts +101 -0
- package/src/worktree-lifecycle.e2e.test.ts +391 -0
package/src/opencode-plugin.ts
CHANGED
|
@@ -344,7 +344,7 @@ const kimakiPlugin: Plugin = async ({ directory }) => {
|
|
|
344
344
|
if (memoryContent) {
|
|
345
345
|
const condensed = condenseMemoryMd(memoryContent)
|
|
346
346
|
output.parts.push({
|
|
347
|
-
id: crypto.randomUUID()
|
|
347
|
+
id: `prt_${crypto.randomUUID()}`,
|
|
348
348
|
sessionID,
|
|
349
349
|
messageID,
|
|
350
350
|
type: 'text' as const,
|
|
@@ -386,7 +386,7 @@ const kimakiPlugin: Plugin = async ({ directory }) => {
|
|
|
386
386
|
})
|
|
387
387
|
|
|
388
388
|
output.parts.push({
|
|
389
|
-
id: crypto.randomUUID()
|
|
389
|
+
id: `prt_${crypto.randomUUID()}`,
|
|
390
390
|
sessionID,
|
|
391
391
|
messageID,
|
|
392
392
|
type: 'text' as const,
|
|
@@ -398,7 +398,7 @@ const kimakiPlugin: Plugin = async ({ directory }) => {
|
|
|
398
398
|
// When the user comes back after a long break, remind the model
|
|
399
399
|
// to save any important context from the previous conversation.
|
|
400
400
|
output.parts.push({
|
|
401
|
-
id: crypto.randomUUID()
|
|
401
|
+
id: `prt_${crypto.randomUUID()}`,
|
|
402
402
|
sessionID,
|
|
403
403
|
messageID,
|
|
404
404
|
type: 'text' as const,
|
|
@@ -425,7 +425,7 @@ const kimakiPlugin: Plugin = async ({ directory }) => {
|
|
|
425
425
|
|
|
426
426
|
sessionGitStates.set(sessionID, gitState)
|
|
427
427
|
output.parts.push({
|
|
428
|
-
id: crypto.randomUUID()
|
|
428
|
+
id: `prt_${crypto.randomUUID()}`,
|
|
429
429
|
sessionID,
|
|
430
430
|
messageID,
|
|
431
431
|
type: 'text' as const,
|
|
@@ -481,3 +481,4 @@ const kimakiPlugin: Plugin = async ({ directory }) => {
|
|
|
481
481
|
export { kimakiPlugin }
|
|
482
482
|
export { interruptOpencodeSessionOnUserMessage } from './opencode-interrupt-plugin.js'
|
|
483
483
|
export { onboardingTutorialPlugin } from './onboarding-tutorial-plugin.js'
|
|
484
|
+
|
package/src/opencode.ts
CHANGED
|
@@ -57,6 +57,13 @@ import {
|
|
|
57
57
|
FetchError,
|
|
58
58
|
type OpenCodeErrors,
|
|
59
59
|
} from './errors.js'
|
|
60
|
+
import {
|
|
61
|
+
ensureKimakiCommandShim,
|
|
62
|
+
getPathEnvKey,
|
|
63
|
+
getSpawnCommandAndArgs,
|
|
64
|
+
prependPathEntry,
|
|
65
|
+
selectResolvedCommand,
|
|
66
|
+
} from './opencode-command.js'
|
|
60
67
|
|
|
61
68
|
const opencodeLogger = createLogger(LogPrefix.OPENCODE)
|
|
62
69
|
|
|
@@ -181,6 +188,8 @@ type ServerLifecycleEvent =
|
|
|
181
188
|
let singleServer: SingleServer | null = null
|
|
182
189
|
let serverRetryCount = 0
|
|
183
190
|
const serverLifecycleListeners = new Set<(event: ServerLifecycleEvent) => void>()
|
|
191
|
+
let processCleanupHandlersRegistered = false
|
|
192
|
+
let startingServerProcess: ChildProcess | null = null
|
|
184
193
|
|
|
185
194
|
// Cached SDK clients per directory. Each client has a fixed
|
|
186
195
|
// x-opencode-directory header pointing to its project directory.
|
|
@@ -201,6 +210,107 @@ export function subscribeOpencodeServerLifecycle(
|
|
|
201
210
|
}
|
|
202
211
|
}
|
|
203
212
|
|
|
213
|
+
function killSingleServerProcessNow({
|
|
214
|
+
reason,
|
|
215
|
+
}: {
|
|
216
|
+
reason: string
|
|
217
|
+
}): void {
|
|
218
|
+
if (!singleServer) {
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const serverProcess = singleServer.process
|
|
223
|
+
const pid = serverProcess.pid
|
|
224
|
+
if (!pid || serverProcess.killed) {
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const killResult = errore.try({
|
|
229
|
+
try: () => {
|
|
230
|
+
serverProcess.kill('SIGTERM')
|
|
231
|
+
},
|
|
232
|
+
catch: (error) => {
|
|
233
|
+
return new Error('Failed to send SIGTERM to opencode server', {
|
|
234
|
+
cause: error,
|
|
235
|
+
})
|
|
236
|
+
},
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
if (killResult instanceof Error) {
|
|
240
|
+
opencodeLogger.warn(
|
|
241
|
+
`[cleanup:${reason}] ${killResult.message} (pid: ${pid}, port: ${singleServer.port})`,
|
|
242
|
+
)
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
opencodeLogger.log(
|
|
247
|
+
`[cleanup:${reason}] Sent SIGTERM to opencode server (pid: ${pid}, port: ${singleServer.port})`,
|
|
248
|
+
)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function killStartingServerProcessNow({
|
|
252
|
+
reason,
|
|
253
|
+
}: {
|
|
254
|
+
reason: string
|
|
255
|
+
}): void {
|
|
256
|
+
const serverProcess = startingServerProcess
|
|
257
|
+
if (!serverProcess) {
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const pid = serverProcess.pid
|
|
262
|
+
if (!pid || serverProcess.killed) {
|
|
263
|
+
return
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const killResult = errore.try({
|
|
267
|
+
try: () => {
|
|
268
|
+
serverProcess.kill('SIGTERM')
|
|
269
|
+
},
|
|
270
|
+
catch: (error) => {
|
|
271
|
+
return new Error('Failed to send SIGTERM to starting opencode server', {
|
|
272
|
+
cause: error,
|
|
273
|
+
})
|
|
274
|
+
},
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
if (killResult instanceof Error) {
|
|
278
|
+
opencodeLogger.warn(
|
|
279
|
+
`[cleanup:${reason}] ${killResult.message} (pid: ${pid})`,
|
|
280
|
+
)
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
opencodeLogger.log(
|
|
285
|
+
`[cleanup:${reason}] Sent SIGTERM to starting opencode server (pid: ${pid})`,
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function ensureProcessCleanupHandlersRegistered(): void {
|
|
290
|
+
if (processCleanupHandlersRegistered) {
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
processCleanupHandlersRegistered = true
|
|
294
|
+
|
|
295
|
+
opencodeLogger.log('Registering process cleanup handlers for opencode server')
|
|
296
|
+
|
|
297
|
+
process.on('exit', () => {
|
|
298
|
+
killSingleServerProcessNow({ reason: 'process-exit' })
|
|
299
|
+
killStartingServerProcessNow({ reason: 'process-exit' })
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
// Fallback for short-lived CLI subcommands that call process.exit without
|
|
303
|
+
// running discord-bot.ts shutdown handlers.
|
|
304
|
+
process.on('SIGINT', () => {
|
|
305
|
+
killSingleServerProcessNow({ reason: 'sigint' })
|
|
306
|
+
killStartingServerProcessNow({ reason: 'sigint' })
|
|
307
|
+
})
|
|
308
|
+
process.on('SIGTERM', () => {
|
|
309
|
+
killSingleServerProcessNow({ reason: 'sigterm' })
|
|
310
|
+
killStartingServerProcessNow({ reason: 'sigterm' })
|
|
311
|
+
})
|
|
312
|
+
}
|
|
313
|
+
|
|
204
314
|
// ── Resolve opencode binary ──────────────────────────────────────
|
|
205
315
|
// Resolve the full path to the opencode binary so we can spawn without
|
|
206
316
|
// shell: true. Using shell: true creates an intermediate sh process — when
|
|
@@ -216,18 +326,32 @@ export function resolveOpencodeCommand(): string {
|
|
|
216
326
|
|
|
217
327
|
const envPath = process.env.OPENCODE_PATH
|
|
218
328
|
if (envPath) {
|
|
219
|
-
|
|
220
|
-
|
|
329
|
+
const resolvedFromEnv = selectResolvedCommand({
|
|
330
|
+
output: envPath,
|
|
331
|
+
isWindows: process.platform === 'win32',
|
|
332
|
+
})
|
|
333
|
+
if (resolvedFromEnv) {
|
|
334
|
+
resolvedOpencodeCommand = resolvedFromEnv
|
|
335
|
+
return resolvedFromEnv
|
|
336
|
+
}
|
|
221
337
|
}
|
|
222
338
|
|
|
223
339
|
const isWindows = process.platform === 'win32'
|
|
224
340
|
const whichCmd = isWindows ? 'where' : 'which'
|
|
225
|
-
const result = errore.
|
|
341
|
+
const result = errore.try({
|
|
226
342
|
try: () => {
|
|
227
|
-
|
|
343
|
+
const commandOutput = execFileSync(whichCmd, ['opencode'], {
|
|
228
344
|
encoding: 'utf8',
|
|
229
345
|
timeout: 5000,
|
|
230
|
-
})
|
|
346
|
+
})
|
|
347
|
+
const resolved = selectResolvedCommand({
|
|
348
|
+
output: commandOutput,
|
|
349
|
+
isWindows,
|
|
350
|
+
})
|
|
351
|
+
if (resolved) {
|
|
352
|
+
return resolved
|
|
353
|
+
}
|
|
354
|
+
throw new Error('opencode not found in PATH')
|
|
231
355
|
},
|
|
232
356
|
catch: () => new Error('opencode not found in PATH'),
|
|
233
357
|
})
|
|
@@ -243,25 +367,6 @@ export function resolveOpencodeCommand(): string {
|
|
|
243
367
|
opencodeLogger.log(`Resolved opencode binary: ${result}`)
|
|
244
368
|
return result
|
|
245
369
|
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Build the spawn command and args, handling Windows .cmd shims.
|
|
249
|
-
* On Windows, .cmd/.bat files can't be spawned directly without a shell —
|
|
250
|
-
* we wrap them with cmd.exe /d /s /c instead of using shell: true
|
|
251
|
-
* (which creates an intermediate sh process that eats SIGTERM).
|
|
252
|
-
*/
|
|
253
|
-
function getSpawnCommandAndArgs(baseArgs: string[]): { command: string; args: string[] } {
|
|
254
|
-
const resolved = resolveOpencodeCommand()
|
|
255
|
-
if (process.platform !== 'win32') {
|
|
256
|
-
return { command: resolved, args: baseArgs }
|
|
257
|
-
}
|
|
258
|
-
const lower = resolved.toLowerCase()
|
|
259
|
-
if (lower.endsWith('.cmd') || lower.endsWith('.bat')) {
|
|
260
|
-
return { command: 'cmd.exe', args: ['/d', '/s', '/c', `"${resolved}"`, ...baseArgs] }
|
|
261
|
-
}
|
|
262
|
-
return { command: resolved, args: baseArgs }
|
|
263
|
-
}
|
|
264
|
-
|
|
265
370
|
async function getOpenPort(): Promise<number> {
|
|
266
371
|
return new Promise((resolve, reject) => {
|
|
267
372
|
const server = net.createServer()
|
|
@@ -346,6 +451,8 @@ async function ensureSingleServer(): Promise<ServerStartError | SingleServer> {
|
|
|
346
451
|
}
|
|
347
452
|
|
|
348
453
|
async function startSingleServer(): Promise<ServerStartError | SingleServer> {
|
|
454
|
+
ensureProcessCleanupHandlersRegistered()
|
|
455
|
+
|
|
349
456
|
const port = await getOpenPort()
|
|
350
457
|
|
|
351
458
|
const serveArgs = ['serve', '--port', port.toString()]
|
|
@@ -353,7 +460,14 @@ async function startSingleServer(): Promise<ServerStartError | SingleServer> {
|
|
|
353
460
|
serveArgs.push('--print-logs', '--log-level', 'DEBUG')
|
|
354
461
|
}
|
|
355
462
|
|
|
356
|
-
const {
|
|
463
|
+
const {
|
|
464
|
+
command: spawnCommand,
|
|
465
|
+
args: spawnArgs,
|
|
466
|
+
windowsVerbatimArguments,
|
|
467
|
+
} = getSpawnCommandAndArgs({
|
|
468
|
+
resolvedCommand: resolveOpencodeCommand(),
|
|
469
|
+
baseArgs: serveArgs,
|
|
470
|
+
})
|
|
357
471
|
|
|
358
472
|
// Server config uses permissive defaults. Per-directory external_directory
|
|
359
473
|
// permissions are set at session creation time via session.create({ permission }).
|
|
@@ -380,6 +494,22 @@ async function startSingleServer(): Promise<ServerStartError | SingleServer> {
|
|
|
380
494
|
[kimakiDataDir]: 'allow',
|
|
381
495
|
[`${kimakiDataDir}/*`]: 'allow',
|
|
382
496
|
}
|
|
497
|
+
const kimakiShimDirectory = ensureKimakiCommandShim({
|
|
498
|
+
dataDir: getDataDir(),
|
|
499
|
+
execPath: process.execPath,
|
|
500
|
+
execArgv: process.execArgv,
|
|
501
|
+
entryScript: process.argv[1] || fileURLToPath(new URL('../bin.js', import.meta.url)),
|
|
502
|
+
})
|
|
503
|
+
const pathEnvKey = getPathEnvKey(process.env)
|
|
504
|
+
const pathEnv = kimakiShimDirectory instanceof Error
|
|
505
|
+
? process.env[pathEnvKey]
|
|
506
|
+
: prependPathEntry({
|
|
507
|
+
entry: kimakiShimDirectory,
|
|
508
|
+
existingPath: process.env[pathEnvKey],
|
|
509
|
+
})
|
|
510
|
+
if (kimakiShimDirectory instanceof Error) {
|
|
511
|
+
opencodeLogger.warn(kimakiShimDirectory.message)
|
|
512
|
+
}
|
|
383
513
|
|
|
384
514
|
const serverProcess = spawn(
|
|
385
515
|
spawnCommand,
|
|
@@ -387,6 +517,7 @@ async function startSingleServer(): Promise<ServerStartError | SingleServer> {
|
|
|
387
517
|
{
|
|
388
518
|
stdio: 'pipe',
|
|
389
519
|
detached: false,
|
|
520
|
+
windowsVerbatimArguments,
|
|
390
521
|
// No project-specific cwd — the server handles all directories via
|
|
391
522
|
// x-opencode-directory header. Use home dir as a neutral working dir.
|
|
392
523
|
cwd: os.homedir(),
|
|
@@ -437,10 +568,13 @@ async function startSingleServer(): Promise<ServerStartError | SingleServer> {
|
|
|
437
568
|
...(process.env.KIMAKI_SENTRY_DSN && {
|
|
438
569
|
KIMAKI_SENTRY_DSN: process.env.KIMAKI_SENTRY_DSN,
|
|
439
570
|
}),
|
|
571
|
+
...(pathEnv && { [pathEnvKey]: pathEnv }),
|
|
440
572
|
},
|
|
441
573
|
},
|
|
442
574
|
)
|
|
443
575
|
|
|
576
|
+
startingServerProcess = serverProcess
|
|
577
|
+
|
|
444
578
|
// Buffer logs until we know if server started successfully.
|
|
445
579
|
// Once ready, switch to forwarding if --verbose-opencode-server is set.
|
|
446
580
|
const logBuffer: string[] = []
|
|
@@ -493,6 +627,10 @@ async function startSingleServer(): Promise<ServerStartError | SingleServer> {
|
|
|
493
627
|
})
|
|
494
628
|
|
|
495
629
|
serverProcess.on('exit', (code, signal) => {
|
|
630
|
+
if (startingServerProcess === serverProcess) {
|
|
631
|
+
startingServerProcess = null
|
|
632
|
+
}
|
|
633
|
+
|
|
496
634
|
opencodeLogger.log(
|
|
497
635
|
`Opencode server exited with code: ${code}, signal: ${signal}`,
|
|
498
636
|
)
|
|
@@ -538,6 +676,11 @@ async function startSingleServer(): Promise<ServerStartError | SingleServer> {
|
|
|
538
676
|
startupStderrTail,
|
|
539
677
|
})
|
|
540
678
|
if (waitResult instanceof Error) {
|
|
679
|
+
killStartingServerProcessNow({ reason: 'startup-failed' })
|
|
680
|
+
if (startingServerProcess === serverProcess) {
|
|
681
|
+
startingServerProcess = null
|
|
682
|
+
}
|
|
683
|
+
|
|
541
684
|
// Dump buffered logs on failure
|
|
542
685
|
opencodeLogger.error(`Server failed to start:`)
|
|
543
686
|
for (const line of logBuffer) {
|
|
@@ -561,6 +704,9 @@ async function startSingleServer(): Promise<ServerStartError | SingleServer> {
|
|
|
561
704
|
port,
|
|
562
705
|
baseUrl: `http://127.0.0.1:${port}`,
|
|
563
706
|
}
|
|
707
|
+
if (startingServerProcess === serverProcess) {
|
|
708
|
+
startingServerProcess = null
|
|
709
|
+
}
|
|
564
710
|
singleServer = server
|
|
565
711
|
notifyServerLifecycle({ type: 'started', port })
|
|
566
712
|
return server
|
|
@@ -700,6 +846,24 @@ export function buildSessionPermissions({
|
|
|
700
846
|
{ permission: 'external_directory', pattern: `${kimakiDataDir}/*`, action: 'allow' },
|
|
701
847
|
)
|
|
702
848
|
|
|
849
|
+
// Allow opencode tool output artifacts under XDG data so agents can inspect
|
|
850
|
+
// prior tool outputs without interactive permission prompts.
|
|
851
|
+
const opencodeToolOutputDir = path
|
|
852
|
+
.join(os.homedir(), '.local', 'share', 'opencode', 'tool-output')
|
|
853
|
+
.replaceAll('\\', '/')
|
|
854
|
+
rules.push(
|
|
855
|
+
{
|
|
856
|
+
permission: 'external_directory',
|
|
857
|
+
pattern: opencodeToolOutputDir,
|
|
858
|
+
action: 'allow',
|
|
859
|
+
},
|
|
860
|
+
{
|
|
861
|
+
permission: 'external_directory',
|
|
862
|
+
pattern: `${opencodeToolOutputDir}/*`,
|
|
863
|
+
action: 'allow',
|
|
864
|
+
},
|
|
865
|
+
)
|
|
866
|
+
|
|
703
867
|
// For worktrees: allow access to the original repository directory
|
|
704
868
|
if (originalRepo) {
|
|
705
869
|
rules.push(
|
|
@@ -737,19 +901,19 @@ export async function stopOpencodeServer(): Promise<boolean> {
|
|
|
737
901
|
return false
|
|
738
902
|
}
|
|
739
903
|
|
|
904
|
+
const server = singleServer
|
|
740
905
|
opencodeLogger.log(
|
|
741
|
-
`Stopping opencode server (pid: ${
|
|
906
|
+
`Stopping opencode server (pid: ${server.process.pid}, port: ${server.port})`,
|
|
742
907
|
)
|
|
743
|
-
if (!
|
|
908
|
+
if (!server.process.killed) {
|
|
744
909
|
const killResult = errore.try({
|
|
745
910
|
try: () => {
|
|
746
|
-
|
|
911
|
+
server.process.kill('SIGTERM')
|
|
747
912
|
},
|
|
748
913
|
catch: (error) => {
|
|
749
|
-
return new Error(
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
)
|
|
914
|
+
return new Error('Failed to send SIGTERM to opencode server', {
|
|
915
|
+
cause: error,
|
|
916
|
+
})
|
|
753
917
|
},
|
|
754
918
|
})
|
|
755
919
|
if (killResult instanceof Error) {
|
|
@@ -757,6 +921,9 @@ export async function stopOpencodeServer(): Promise<boolean> {
|
|
|
757
921
|
}
|
|
758
922
|
}
|
|
759
923
|
|
|
924
|
+
killStartingServerProcessNow({ reason: 'stop-opencode-server' })
|
|
925
|
+
startingServerProcess = null
|
|
926
|
+
|
|
760
927
|
singleServer = null
|
|
761
928
|
clientCache.clear()
|
|
762
929
|
serverRetryCount = 0
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Shared apply_patch text parsing utilities.
|
|
2
|
+
// Used by diff-patch-plugin.ts (file path extraction for snapshots) and
|
|
3
|
+
// message-formatting.ts (per-file addition/deletion counts for Discord display).
|
|
4
|
+
//
|
|
5
|
+
// The apply_patch tool uses three path header formats:
|
|
6
|
+
// *** Add File: path — new file
|
|
7
|
+
// *** Update File: path — existing file edit
|
|
8
|
+
// *** Delete File: path — file removal
|
|
9
|
+
// *** Move to: path — rename destination
|
|
10
|
+
// --- a/path / +++ b/path — unified diff headers (fallback)
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Extract all file paths referenced in a patchText string.
|
|
14
|
+
* Handles custom apply_patch headers, move targets, and unified diff headers.
|
|
15
|
+
* Returns deduplicated paths.
|
|
16
|
+
*/
|
|
17
|
+
export function extractPatchFilePaths(patchText: string): string[] {
|
|
18
|
+
const custom = [
|
|
19
|
+
...patchText.matchAll(
|
|
20
|
+
/^\*\*\* (?:Add|Update|Delete) File:\s+(.+)$/gm,
|
|
21
|
+
),
|
|
22
|
+
].map((m) => {
|
|
23
|
+
return (m[1] ?? '').trim()
|
|
24
|
+
})
|
|
25
|
+
const moved = [
|
|
26
|
+
...patchText.matchAll(/^\*\*\* Move to:\s+(.+)$/gm),
|
|
27
|
+
].map((m) => {
|
|
28
|
+
return (m[1] ?? '').trim()
|
|
29
|
+
})
|
|
30
|
+
const unified = [
|
|
31
|
+
...patchText.matchAll(/^(?:---|\+\+\+) [ab]\/(.+)$/gm),
|
|
32
|
+
].map((m) => {
|
|
33
|
+
return (m[1] ?? '').trim()
|
|
34
|
+
})
|
|
35
|
+
const all = [...custom, ...moved, ...unified].filter(Boolean)
|
|
36
|
+
return all.filter((v, i, a) => {
|
|
37
|
+
return a.indexOf(v) === i
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Parse a patchText string and count additions/deletions per file.
|
|
43
|
+
* Patch format uses `*** Add File:`, `*** Update File:`, `*** Delete File:` headers,
|
|
44
|
+
* with diff lines prefixed by `+` (addition) or `-` (deletion) inside `@@` hunks.
|
|
45
|
+
*/
|
|
46
|
+
export function parsePatchFileCounts(
|
|
47
|
+
patchText: string,
|
|
48
|
+
): Map<string, { additions: number; deletions: number }> {
|
|
49
|
+
const counts = new Map<string, { additions: number; deletions: number }>()
|
|
50
|
+
const lines = patchText.split('\n')
|
|
51
|
+
let currentFile = ''
|
|
52
|
+
let currentType = ''
|
|
53
|
+
let inHunk = false
|
|
54
|
+
|
|
55
|
+
for (const line of lines) {
|
|
56
|
+
const addMatch = line.match(/^\*\*\* Add File:\s*(.+)/)
|
|
57
|
+
const updateMatch = line.match(/^\*\*\* Update File:\s*(.+)/)
|
|
58
|
+
const deleteMatch = line.match(/^\*\*\* Delete File:\s*(.+)/)
|
|
59
|
+
|
|
60
|
+
if (addMatch || updateMatch || deleteMatch) {
|
|
61
|
+
const match = addMatch || updateMatch || deleteMatch
|
|
62
|
+
currentFile = (match?.[1] ?? '').trim()
|
|
63
|
+
currentType = addMatch ? 'add' : updateMatch ? 'update' : 'delete'
|
|
64
|
+
counts.set(currentFile, { additions: 0, deletions: 0 })
|
|
65
|
+
inHunk = false
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (line.startsWith('@@')) {
|
|
70
|
+
inHunk = true
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (line.startsWith('*** ')) {
|
|
75
|
+
inHunk = false
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!currentFile) {
|
|
80
|
+
continue
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const entry = counts.get(currentFile)
|
|
84
|
+
if (!entry) {
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (currentType === 'add') {
|
|
89
|
+
// all content lines in Add File are additions
|
|
90
|
+
if (line.length > 0 && !line.startsWith('*** ')) {
|
|
91
|
+
entry.additions++
|
|
92
|
+
}
|
|
93
|
+
} else if (currentType === 'delete') {
|
|
94
|
+
// all content lines in Delete File are deletions
|
|
95
|
+
if (line.length > 0 && !line.startsWith('*** ')) {
|
|
96
|
+
entry.deletions++
|
|
97
|
+
}
|
|
98
|
+
} else if (inHunk) {
|
|
99
|
+
if (line.startsWith('+')) {
|
|
100
|
+
entry.additions++
|
|
101
|
+
} else if (line.startsWith('-')) {
|
|
102
|
+
entry.deletions++
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return counts
|
|
107
|
+
}
|