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.
Files changed (114) hide show
  1. package/dist/agent-model.e2e.test.js +80 -2
  2. package/dist/anthropic-auth-plugin.js +246 -195
  3. package/dist/anthropic-auth-plugin.test.js +125 -0
  4. package/dist/anthropic-auth-state.js +231 -0
  5. package/dist/bin.js +6 -3
  6. package/dist/cli-parsing.test.js +23 -0
  7. package/dist/cli-send-thread.e2e.test.js +2 -2
  8. package/dist/cli.js +72 -46
  9. package/dist/commands/merge-worktree.js +6 -3
  10. package/dist/commands/new-worktree.js +18 -7
  11. package/dist/commands/worktrees.js +71 -7
  12. package/dist/context-awareness-plugin.js +52 -50
  13. package/dist/context-awareness-plugin.test.js +68 -1
  14. package/dist/discord-bot.js +126 -54
  15. package/dist/discord-utils.test.js +19 -0
  16. package/dist/errors.js +0 -5
  17. package/dist/exec-async.js +26 -0
  18. package/dist/external-opencode-sync.js +33 -72
  19. package/dist/forum-sync/config.js +2 -2
  20. package/dist/forum-sync/markdown.js +4 -8
  21. package/dist/hrana-server.js +11 -3
  22. package/dist/image-optimizer-plugin.js +153 -0
  23. package/dist/ipc-tools-plugin.js +11 -4
  24. package/dist/kimaki-opencode-plugin.js +1 -0
  25. package/dist/logger.js +0 -1
  26. package/dist/markdown.js +2 -2
  27. package/dist/message-preprocessing.js +100 -16
  28. package/dist/onboarding-tutorial.js +1 -1
  29. package/dist/opencode-command-detection.js +70 -0
  30. package/dist/opencode-command-detection.test.js +210 -0
  31. package/dist/opencode-interrupt-plugin.js +64 -8
  32. package/dist/opencode-interrupt-plugin.test.js +23 -39
  33. package/dist/opencode.js +16 -20
  34. package/dist/pkce.js +23 -0
  35. package/dist/plugin-logger.js +59 -0
  36. package/dist/queue-advanced-permissions-typing.e2e.test.js +1 -1
  37. package/dist/queue-advanced-question.e2e.test.js +127 -42
  38. package/dist/sentry.js +7 -114
  39. package/dist/session-handler/event-stream-state.js +1 -1
  40. package/dist/session-handler/thread-runtime-state.js +9 -0
  41. package/dist/session-handler/thread-session-runtime.js +197 -45
  42. package/dist/session-title-rename.test.js +80 -0
  43. package/dist/store.js +1 -2
  44. package/dist/system-message.js +105 -49
  45. package/dist/system-message.test.js +598 -15
  46. package/dist/task-runner.js +7 -4
  47. package/dist/task-schedule.js +2 -0
  48. package/dist/thread-message-queue.e2e.test.js +18 -11
  49. package/dist/unnest-code-blocks.js +11 -1
  50. package/dist/unnest-code-blocks.test.js +32 -0
  51. package/dist/voice-handler.js +15 -5
  52. package/dist/voice.js +53 -23
  53. package/dist/voice.test.js +2 -0
  54. package/dist/worktrees.js +111 -120
  55. package/package.json +15 -19
  56. package/skills/lintcn/SKILL.md +6 -1
  57. package/skills/new-skill/SKILL.md +211 -0
  58. package/skills/npm-package/SKILL.md +3 -2
  59. package/skills/spiceflow/SKILL.md +1 -1
  60. package/skills/usecomputer/SKILL.md +174 -249
  61. package/src/agent-model.e2e.test.ts +95 -2
  62. package/src/anthropic-auth-plugin.test.ts +159 -0
  63. package/src/anthropic-auth-plugin.ts +474 -403
  64. package/src/anthropic-auth-state.ts +282 -0
  65. package/src/bin.ts +6 -3
  66. package/src/cli-parsing.test.ts +32 -0
  67. package/src/cli-send-thread.e2e.test.ts +2 -2
  68. package/src/cli.ts +93 -62
  69. package/src/commands/merge-worktree.ts +8 -3
  70. package/src/commands/new-worktree.ts +22 -10
  71. package/src/commands/worktrees.ts +86 -5
  72. package/src/context-awareness-plugin.test.ts +77 -1
  73. package/src/context-awareness-plugin.ts +85 -64
  74. package/src/discord-bot.ts +135 -56
  75. package/src/discord-utils.test.ts +21 -0
  76. package/src/errors.ts +0 -6
  77. package/src/exec-async.ts +35 -0
  78. package/src/external-opencode-sync.ts +39 -85
  79. package/src/forum-sync/config.ts +2 -2
  80. package/src/forum-sync/markdown.ts +5 -9
  81. package/src/hrana-server.ts +15 -3
  82. package/src/image-optimizer-plugin.ts +194 -0
  83. package/src/ipc-tools-plugin.ts +16 -8
  84. package/src/kimaki-opencode-plugin.ts +1 -0
  85. package/src/logger.ts +0 -1
  86. package/src/markdown.ts +2 -2
  87. package/src/message-preprocessing.ts +117 -16
  88. package/src/onboarding-tutorial.ts +1 -1
  89. package/src/opencode-command-detection.test.ts +268 -0
  90. package/src/opencode-command-detection.ts +79 -0
  91. package/src/opencode-interrupt-plugin.test.ts +93 -50
  92. package/src/opencode-interrupt-plugin.ts +86 -9
  93. package/src/opencode.ts +16 -22
  94. package/src/plugin-logger.ts +68 -0
  95. package/src/queue-advanced-permissions-typing.e2e.test.ts +1 -1
  96. package/src/queue-advanced-question.e2e.test.ts +243 -158
  97. package/src/sentry.ts +7 -120
  98. package/src/session-handler/event-stream-state.ts +1 -1
  99. package/src/session-handler/thread-runtime-state.ts +17 -0
  100. package/src/session-handler/thread-session-runtime.ts +232 -46
  101. package/src/session-title-rename.test.ts +112 -0
  102. package/src/store.ts +3 -8
  103. package/src/system-message.test.ts +612 -0
  104. package/src/system-message.ts +136 -63
  105. package/src/task-runner.ts +7 -4
  106. package/src/task-schedule.ts +3 -0
  107. package/src/thread-message-queue.e2e.test.ts +22 -11
  108. package/src/undici.d.ts +12 -0
  109. package/src/unnest-code-blocks.test.ts +34 -0
  110. package/src/unnest-code-blocks.ts +18 -1
  111. package/src/voice-handler.ts +18 -4
  112. package/src/voice.test.ts +2 -0
  113. package/src/voice.ts +68 -23
  114. 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
- // Keep the queued user message execution context across abort+resume.
267
- // Without this, OpenCode re-resolves model defaults and can ignore
268
- // /model session overrides (issue #77).
269
- const resumeBody: {
270
- parts: []
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
- } = { parts: [] }
345
+ } = {
346
+ messageID,
347
+ parts: currentPending.parts,
348
+ }
274
349
  if (currentPending.agent) {
275
- resumeBody.agent = currentPending.agent
350
+ replayBody.agent = currentPending.agent
276
351
  }
277
352
  if (currentPending.model) {
278
- resumeBody.model = currentPending.model
353
+ replayBody.model = currentPending.model
279
354
  }
280
355
 
281
356
  await ctx.client.session.promptAsync({
282
357
  path: { id: sessionID },
283
- body: resumeBody,
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 = ['serve', '--port', port.toString()]
466
- if (store.getState().verboseOpencodeServer) {
467
- serveArgs.push('--print-logs', '--log-level', 'DEBUG')
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
- if (store.getState().verboseOpencodeServer) {
642
- for (const line of lines) {
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
- if (store.getState().verboseOpencodeServer) {
661
- for (const line of lines) {
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
- // When verbose mode is enabled, also dump startup logs so plugin loading
742
- // errors and other startup output are visible in kimaki.log.
743
- if (store.getState().verboseOpencodeServer) {
744
- for (const line of logBuffer) {
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*"