kimaki 0.4.90 → 0.4.91
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/agent-model.e2e.test.js +80 -2
- package/dist/anthropic-auth-plugin.js +246 -195
- package/dist/anthropic-auth-plugin.test.js +125 -0
- package/dist/anthropic-auth-state.js +231 -0
- package/dist/bin.js +6 -3
- package/dist/cli-parsing.test.js +23 -0
- package/dist/cli-send-thread.e2e.test.js +2 -2
- package/dist/cli.js +72 -46
- package/dist/commands/merge-worktree.js +6 -3
- package/dist/commands/new-worktree.js +18 -7
- package/dist/commands/worktrees.js +71 -7
- package/dist/context-awareness-plugin.js +52 -50
- package/dist/context-awareness-plugin.test.js +68 -1
- package/dist/discord-bot.js +126 -54
- package/dist/discord-utils.test.js +19 -0
- package/dist/errors.js +0 -5
- package/dist/exec-async.js +26 -0
- package/dist/external-opencode-sync.js +33 -72
- package/dist/forum-sync/config.js +2 -2
- package/dist/forum-sync/markdown.js +4 -8
- package/dist/hrana-server.js +11 -3
- package/dist/image-optimizer-plugin.js +153 -0
- package/dist/ipc-tools-plugin.js +11 -4
- package/dist/kimaki-opencode-plugin.js +1 -0
- package/dist/logger.js +0 -1
- package/dist/markdown.js +2 -2
- package/dist/message-preprocessing.js +100 -16
- package/dist/onboarding-tutorial.js +1 -1
- package/dist/opencode-command-detection.js +70 -0
- package/dist/opencode-command-detection.test.js +210 -0
- package/dist/opencode-interrupt-plugin.js +64 -8
- package/dist/opencode-interrupt-plugin.test.js +23 -39
- package/dist/opencode.js +16 -20
- package/dist/pkce.js +23 -0
- package/dist/plugin-logger.js +59 -0
- package/dist/queue-advanced-permissions-typing.e2e.test.js +1 -1
- package/dist/queue-advanced-question.e2e.test.js +127 -42
- package/dist/sentry.js +7 -114
- package/dist/session-handler/event-stream-state.js +1 -1
- package/dist/session-handler/thread-runtime-state.js +9 -0
- package/dist/session-handler/thread-session-runtime.js +197 -45
- package/dist/session-title-rename.test.js +80 -0
- package/dist/store.js +1 -2
- package/dist/system-message.js +105 -49
- package/dist/system-message.test.js +598 -15
- package/dist/task-runner.js +7 -4
- package/dist/task-schedule.js +2 -0
- package/dist/thread-message-queue.e2e.test.js +18 -11
- package/dist/unnest-code-blocks.js +11 -1
- package/dist/unnest-code-blocks.test.js +32 -0
- package/dist/voice-handler.js +15 -5
- package/dist/voice.js +53 -23
- package/dist/voice.test.js +2 -0
- package/dist/worktrees.js +111 -120
- package/package.json +15 -19
- package/skills/lintcn/SKILL.md +6 -1
- package/skills/new-skill/SKILL.md +211 -0
- package/skills/npm-package/SKILL.md +3 -2
- package/skills/spiceflow/SKILL.md +1 -1
- package/skills/usecomputer/SKILL.md +174 -249
- package/src/agent-model.e2e.test.ts +95 -2
- package/src/anthropic-auth-plugin.test.ts +159 -0
- package/src/anthropic-auth-plugin.ts +474 -403
- package/src/anthropic-auth-state.ts +282 -0
- package/src/bin.ts +6 -3
- package/src/cli-parsing.test.ts +32 -0
- package/src/cli-send-thread.e2e.test.ts +2 -2
- package/src/cli.ts +93 -62
- package/src/commands/merge-worktree.ts +8 -3
- package/src/commands/new-worktree.ts +22 -10
- package/src/commands/worktrees.ts +86 -5
- package/src/context-awareness-plugin.test.ts +77 -1
- package/src/context-awareness-plugin.ts +85 -64
- package/src/discord-bot.ts +135 -56
- package/src/discord-utils.test.ts +21 -0
- package/src/errors.ts +0 -6
- package/src/exec-async.ts +35 -0
- package/src/external-opencode-sync.ts +39 -85
- package/src/forum-sync/config.ts +2 -2
- package/src/forum-sync/markdown.ts +5 -9
- package/src/hrana-server.ts +15 -3
- package/src/image-optimizer-plugin.ts +194 -0
- package/src/ipc-tools-plugin.ts +16 -8
- package/src/kimaki-opencode-plugin.ts +1 -0
- package/src/logger.ts +0 -1
- package/src/markdown.ts +2 -2
- package/src/message-preprocessing.ts +117 -16
- package/src/onboarding-tutorial.ts +1 -1
- package/src/opencode-command-detection.test.ts +268 -0
- package/src/opencode-command-detection.ts +79 -0
- package/src/opencode-interrupt-plugin.test.ts +93 -50
- package/src/opencode-interrupt-plugin.ts +86 -9
- package/src/opencode.ts +16 -22
- package/src/plugin-logger.ts +68 -0
- package/src/queue-advanced-permissions-typing.e2e.test.ts +1 -1
- package/src/queue-advanced-question.e2e.test.ts +243 -158
- package/src/sentry.ts +7 -120
- package/src/session-handler/event-stream-state.ts +1 -1
- package/src/session-handler/thread-runtime-state.ts +17 -0
- package/src/session-handler/thread-session-runtime.ts +232 -46
- package/src/session-title-rename.test.ts +112 -0
- package/src/store.ts +3 -8
- package/src/system-message.test.ts +612 -0
- package/src/system-message.ts +136 -63
- package/src/task-runner.ts +7 -4
- package/src/task-schedule.ts +3 -0
- package/src/thread-message-queue.e2e.test.ts +22 -11
- package/src/undici.d.ts +12 -0
- package/src/unnest-code-blocks.test.ts +34 -0
- package/src/unnest-code-blocks.ts +18 -1
- package/src/voice-handler.ts +18 -4
- package/src/voice.test.ts +2 -0
- package/src/voice.ts +68 -23
- package/src/worktrees.ts +152 -156
|
@@ -10,15 +10,24 @@
|
|
|
10
10
|
// forgetting to clear a timer.
|
|
11
11
|
|
|
12
12
|
import type { Plugin } from '@opencode-ai/plugin'
|
|
13
|
+
import type {
|
|
14
|
+
Part,
|
|
15
|
+
TextPartInput,
|
|
16
|
+
FilePartInput,
|
|
17
|
+
AgentPartInput,
|
|
18
|
+
SubtaskPartInput,
|
|
19
|
+
} from '@opencode-ai/sdk'
|
|
13
20
|
|
|
14
21
|
type PluginHooks = Awaited<ReturnType<Plugin>>
|
|
15
22
|
type InterruptEvent = Parameters<NonNullable<PluginHooks['event']>>[0]['event']
|
|
23
|
+
type PromptPartInput = TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput
|
|
16
24
|
|
|
17
25
|
type PendingMessage = {
|
|
18
26
|
sessionID: string
|
|
19
27
|
started: boolean
|
|
20
28
|
timer: ReturnType<typeof setTimeout>
|
|
21
29
|
abortAfterStepMessageID: string | undefined
|
|
30
|
+
parts: PromptPartInput[]
|
|
22
31
|
agent: string | undefined
|
|
23
32
|
model:
|
|
24
33
|
| {
|
|
@@ -28,6 +37,62 @@ type PendingMessage = {
|
|
|
28
37
|
| undefined
|
|
29
38
|
}
|
|
30
39
|
|
|
40
|
+
type InterruptChatOutput =
|
|
41
|
+
NonNullable<PluginHooks['chat.message']> extends (
|
|
42
|
+
input: unknown,
|
|
43
|
+
output: infer T,
|
|
44
|
+
) => Promise<void>
|
|
45
|
+
? T
|
|
46
|
+
: never
|
|
47
|
+
|
|
48
|
+
function toPromptParts(parts: Part[]): PromptPartInput[] {
|
|
49
|
+
return parts.reduce<PromptPartInput[]>((acc, part) => {
|
|
50
|
+
if (part.type === 'text') {
|
|
51
|
+
acc.push({
|
|
52
|
+
id: part.id,
|
|
53
|
+
type: 'text',
|
|
54
|
+
text: part.text,
|
|
55
|
+
synthetic: part.synthetic,
|
|
56
|
+
ignored: part.ignored,
|
|
57
|
+
time: part.time,
|
|
58
|
+
metadata: part.metadata,
|
|
59
|
+
})
|
|
60
|
+
return acc
|
|
61
|
+
}
|
|
62
|
+
if (part.type === 'file') {
|
|
63
|
+
acc.push({
|
|
64
|
+
id: part.id,
|
|
65
|
+
type: 'file',
|
|
66
|
+
mime: part.mime,
|
|
67
|
+
filename: part.filename,
|
|
68
|
+
url: part.url,
|
|
69
|
+
source: part.source,
|
|
70
|
+
})
|
|
71
|
+
return acc
|
|
72
|
+
}
|
|
73
|
+
if (part.type === 'agent') {
|
|
74
|
+
acc.push({
|
|
75
|
+
id: part.id,
|
|
76
|
+
type: 'agent',
|
|
77
|
+
name: part.name,
|
|
78
|
+
source: part.source,
|
|
79
|
+
})
|
|
80
|
+
return acc
|
|
81
|
+
}
|
|
82
|
+
if (part.type === 'subtask') {
|
|
83
|
+
acc.push({
|
|
84
|
+
id: part.id,
|
|
85
|
+
type: 'subtask',
|
|
86
|
+
prompt: part.prompt,
|
|
87
|
+
description: part.description,
|
|
88
|
+
agent: part.agent,
|
|
89
|
+
})
|
|
90
|
+
return acc
|
|
91
|
+
}
|
|
92
|
+
return acc
|
|
93
|
+
}, [])
|
|
94
|
+
}
|
|
95
|
+
|
|
31
96
|
type EventWaiter = {
|
|
32
97
|
match: (event: InterruptEvent) => boolean
|
|
33
98
|
finish: () => void
|
|
@@ -134,11 +199,13 @@ function createInterruptState() {
|
|
|
134
199
|
schedulePending({
|
|
135
200
|
messageID,
|
|
136
201
|
sessionID,
|
|
202
|
+
parts,
|
|
137
203
|
delayMs,
|
|
138
204
|
onTimeout,
|
|
139
205
|
}: {
|
|
140
206
|
messageID: string
|
|
141
207
|
sessionID: string
|
|
208
|
+
parts: PromptPartInput[]
|
|
142
209
|
delayMs: number
|
|
143
210
|
onTimeout: () => void
|
|
144
211
|
}): void {
|
|
@@ -152,6 +219,7 @@ function createInterruptState() {
|
|
|
152
219
|
started: false,
|
|
153
220
|
timer,
|
|
154
221
|
abortAfterStepMessageID: latestAssistantMessageIDBySession.get(sessionID),
|
|
222
|
+
parts,
|
|
155
223
|
agent: undefined,
|
|
156
224
|
model: undefined,
|
|
157
225
|
})
|
|
@@ -223,6 +291,7 @@ const interruptOpencodeSessionOnUserMessage: Plugin = async (ctx) => {
|
|
|
223
291
|
state.schedulePending({
|
|
224
292
|
messageID,
|
|
225
293
|
sessionID,
|
|
294
|
+
parts: pending.parts,
|
|
226
295
|
delayMs: 200,
|
|
227
296
|
onTimeout: () => {
|
|
228
297
|
void interruptPendingMessage(messageID)
|
|
@@ -263,24 +332,30 @@ const interruptOpencodeSessionOnUserMessage: Plugin = async (ctx) => {
|
|
|
263
332
|
return
|
|
264
333
|
}
|
|
265
334
|
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
|
|
270
|
-
|
|
335
|
+
// Resubmit the original queued user message after abort.
|
|
336
|
+
// session.abort() clears OpenCode's internal prompt queue, so resuming
|
|
337
|
+
// with an empty parts array can silently drop the user's message.
|
|
338
|
+
// Keep the original messageID + parts and preserve agent/model context so
|
|
339
|
+
// session overrides (issue #77) survive the abort + replay path.
|
|
340
|
+
const replayBody: {
|
|
341
|
+
messageID: string
|
|
342
|
+
parts: PromptPartInput[]
|
|
271
343
|
agent?: string
|
|
272
344
|
model?: { providerID: string; modelID: string }
|
|
273
|
-
} = {
|
|
345
|
+
} = {
|
|
346
|
+
messageID,
|
|
347
|
+
parts: currentPending.parts,
|
|
348
|
+
}
|
|
274
349
|
if (currentPending.agent) {
|
|
275
|
-
|
|
350
|
+
replayBody.agent = currentPending.agent
|
|
276
351
|
}
|
|
277
352
|
if (currentPending.model) {
|
|
278
|
-
|
|
353
|
+
replayBody.model = currentPending.model
|
|
279
354
|
}
|
|
280
355
|
|
|
281
356
|
await ctx.client.session.promptAsync({
|
|
282
357
|
path: { id: sessionID },
|
|
283
|
-
body:
|
|
358
|
+
body: replayBody,
|
|
284
359
|
})
|
|
285
360
|
state.clearPending(messageID)
|
|
286
361
|
|
|
@@ -291,6 +366,7 @@ const interruptOpencodeSessionOnUserMessage: Plugin = async (ctx) => {
|
|
|
291
366
|
state.schedulePending({
|
|
292
367
|
messageID: nextPending.messageID,
|
|
293
368
|
sessionID,
|
|
369
|
+
parts: nextPending.pending.parts,
|
|
294
370
|
delayMs: 50,
|
|
295
371
|
onTimeout: () => {
|
|
296
372
|
void interruptPendingMessage(nextPending.messageID)
|
|
@@ -382,6 +458,7 @@ const interruptOpencodeSessionOnUserMessage: Plugin = async (ctx) => {
|
|
|
382
458
|
state.schedulePending({
|
|
383
459
|
messageID,
|
|
384
460
|
sessionID,
|
|
461
|
+
parts: toPromptParts(output.parts),
|
|
385
462
|
delayMs: interruptStepTimeoutMs,
|
|
386
463
|
onTimeout: () => {
|
|
387
464
|
void interruptPendingMessage(messageID)
|
package/src/opencode.ts
CHANGED
|
@@ -462,10 +462,14 @@ async function startSingleServer(): Promise<ServerStartError | SingleServer> {
|
|
|
462
462
|
|
|
463
463
|
const port = await getOpenPort()
|
|
464
464
|
|
|
465
|
-
const serveArgs = [
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
465
|
+
const serveArgs = [
|
|
466
|
+
'serve',
|
|
467
|
+
'--port',
|
|
468
|
+
port.toString(),
|
|
469
|
+
'--print-logs',
|
|
470
|
+
'--log-level',
|
|
471
|
+
'WARN',
|
|
472
|
+
]
|
|
469
473
|
|
|
470
474
|
const {
|
|
471
475
|
command: spawnCommand,
|
|
@@ -621,7 +625,6 @@ async function startSingleServer(): Promise<ServerStartError | SingleServer> {
|
|
|
621
625
|
startingServerProcess = serverProcess
|
|
622
626
|
|
|
623
627
|
// Buffer logs until we know if server started successfully.
|
|
624
|
-
// Once ready, switch to forwarding if --verbose-opencode-server is set.
|
|
625
628
|
const logBuffer: string[] = []
|
|
626
629
|
const startupStderrTail: string[] = []
|
|
627
630
|
let serverReady = false
|
|
@@ -638,10 +641,8 @@ async function startSingleServer(): Promise<ServerStartError | SingleServer> {
|
|
|
638
641
|
logBuffer.push(...lines.map((line) => `[stdout] ${line}`))
|
|
639
642
|
return
|
|
640
643
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
opencodeLogger.log(`[server:${port}] ${line}`)
|
|
644
|
-
}
|
|
644
|
+
for (const line of lines) {
|
|
645
|
+
opencodeLogger.log(line)
|
|
645
646
|
}
|
|
646
647
|
} catch (error) {
|
|
647
648
|
logBuffer.push(`Failed to process stdout startup logs: ${error}`)
|
|
@@ -657,10 +658,8 @@ async function startSingleServer(): Promise<ServerStartError | SingleServer> {
|
|
|
657
658
|
pushStartupStderrTail({ stderrTail: startupStderrTail, chunk })
|
|
658
659
|
return
|
|
659
660
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
opencodeLogger.error(`[server:${port}] ${line}`)
|
|
663
|
-
}
|
|
661
|
+
for (const line of lines) {
|
|
662
|
+
opencodeLogger.error(line)
|
|
664
663
|
}
|
|
665
664
|
} catch (error) {
|
|
666
665
|
logBuffer.push(`Failed to process stderr startup logs: ${error}`)
|
|
@@ -738,12 +737,10 @@ async function startSingleServer(): Promise<ServerStartError | SingleServer> {
|
|
|
738
737
|
serverReady = true
|
|
739
738
|
opencodeLogger.log(`Server ready on port ${port}`)
|
|
740
739
|
|
|
741
|
-
//
|
|
742
|
-
//
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
opencodeLogger.log(`[server:${port}:startup] ${line}`)
|
|
746
|
-
}
|
|
740
|
+
// Always dump startup logs so plugin loading errors and other startup output
|
|
741
|
+
// are visible in kimaki.log.
|
|
742
|
+
for (const line of logBuffer) {
|
|
743
|
+
opencodeLogger.log(line)
|
|
747
744
|
}
|
|
748
745
|
|
|
749
746
|
const server: SingleServer = {
|
|
@@ -824,9 +821,6 @@ export async function initializeOpencodeForDirectory(
|
|
|
824
821
|
|
|
825
822
|
if (!initializedDirectories.has(directory)) {
|
|
826
823
|
initializedDirectories.add(directory)
|
|
827
|
-
opencodeLogger.log(
|
|
828
|
-
`Using shared server on port ${server.port} for directory: ${directory}`,
|
|
829
|
-
)
|
|
830
824
|
}
|
|
831
825
|
|
|
832
826
|
return () => {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import util from 'node:util'
|
|
4
|
+
import { sanitizeSensitiveText, sanitizeUnknownValue } from './privacy-sanitizer.js'
|
|
5
|
+
|
|
6
|
+
let pluginLogFilePath: string | null = null
|
|
7
|
+
|
|
8
|
+
export function setPluginLogFilePath(dataDir: string): void {
|
|
9
|
+
pluginLogFilePath = path.join(dataDir, 'kimaki.log')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function formatArg(arg: unknown): string {
|
|
13
|
+
if (typeof arg === 'string') {
|
|
14
|
+
return sanitizeSensitiveText(arg, { redactPaths: false })
|
|
15
|
+
}
|
|
16
|
+
const safeArg = sanitizeUnknownValue(arg, { redactPaths: false })
|
|
17
|
+
return util.inspect(safeArg, { colors: false, depth: 4 })
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function formatPluginErrorWithStack(error: unknown): string {
|
|
21
|
+
if (error instanceof Error) {
|
|
22
|
+
return sanitizeSensitiveText(
|
|
23
|
+
error.stack ?? `${error.name}: ${error.message}`,
|
|
24
|
+
{ redactPaths: false },
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
if (typeof error === 'string') {
|
|
28
|
+
return sanitizeSensitiveText(error, { redactPaths: false })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const safeError = sanitizeUnknownValue(error, { redactPaths: false })
|
|
32
|
+
return sanitizeSensitiveText(util.inspect(safeError, { colors: false, depth: 4 }), {
|
|
33
|
+
redactPaths: false,
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function writeToFile(level: string, prefix: string, args: unknown[]) {
|
|
38
|
+
if (!pluginLogFilePath) {
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
const timestamp = new Date().toISOString()
|
|
42
|
+
const message = `[${timestamp}] [${level}] [${prefix}] ${args.map(formatArg).join(' ')}\n`
|
|
43
|
+
try {
|
|
44
|
+
fs.appendFileSync(pluginLogFilePath, message)
|
|
45
|
+
} catch {
|
|
46
|
+
// Plugin logging must never break the OpenCode plugin process.
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function createPluginLogger(prefix: string) {
|
|
51
|
+
return {
|
|
52
|
+
log: (...args: unknown[]) => {
|
|
53
|
+
writeToFile('LOG', prefix, args)
|
|
54
|
+
},
|
|
55
|
+
info: (...args: unknown[]) => {
|
|
56
|
+
writeToFile('INFO', prefix, args)
|
|
57
|
+
},
|
|
58
|
+
warn: (...args: unknown[]) => {
|
|
59
|
+
writeToFile('WARN', prefix, args)
|
|
60
|
+
},
|
|
61
|
+
error: (...args: unknown[]) => {
|
|
62
|
+
writeToFile('ERROR', prefix, args)
|
|
63
|
+
},
|
|
64
|
+
debug: (...args: unknown[]) => {
|
|
65
|
+
writeToFile('DEBUG', prefix, args)
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -121,12 +121,12 @@ describe('queue advanced: typing around permissions', () => {
|
|
|
121
121
|
"--- from: user (queue-permission-tester)
|
|
122
122
|
PERMISSION_TYPING_MARKER
|
|
123
123
|
--- from: assistant (TestBot)
|
|
124
|
+
⬥ requesting external read permission
|
|
124
125
|
⚠️ **Permission Required**
|
|
125
126
|
**Type:** \`external_directory\`
|
|
126
127
|
Agent is accessing files outside the project. [Learn more](https://opencode.ai/docs/permissions/#external-directories)
|
|
127
128
|
**Pattern:** \`/Users/morse/*\`
|
|
128
129
|
✅ Permission **accepted**
|
|
129
|
-
⬥ requesting external read permission
|
|
130
130
|
[user clicks button]
|
|
131
131
|
⬥ permission-flow-done
|
|
132
132
|
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|