kimaki 0.4.78 → 0.4.80

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 (90) hide show
  1. package/dist/anthropic-auth-plugin.js +628 -0
  2. package/dist/channel-management.js +2 -2
  3. package/dist/cli.js +316 -129
  4. package/dist/commands/action-buttons.js +1 -1
  5. package/dist/commands/login.js +634 -277
  6. package/dist/commands/model.js +91 -6
  7. package/dist/commands/paginated-select.js +57 -0
  8. package/dist/commands/resume.js +2 -2
  9. package/dist/commands/tasks.js +205 -0
  10. package/dist/commands/undo-redo.js +80 -18
  11. package/dist/context-awareness-plugin.js +347 -0
  12. package/dist/database.js +103 -7
  13. package/dist/db.js +39 -1
  14. package/dist/discord-bot.js +42 -19
  15. package/dist/discord-urls.js +11 -0
  16. package/dist/discord-ws-proxy.js +350 -0
  17. package/dist/discord-ws-proxy.test.js +500 -0
  18. package/dist/errors.js +1 -1
  19. package/dist/gateway-session.js +163 -0
  20. package/dist/hrana-server.js +114 -4
  21. package/dist/interaction-handler.js +30 -7
  22. package/dist/ipc-tools-plugin.js +186 -0
  23. package/dist/message-preprocessing.js +56 -11
  24. package/dist/onboarding-welcome.js +1 -1
  25. package/dist/opencode-interrupt-plugin.js +133 -75
  26. package/dist/opencode-plugin.js +12 -389
  27. package/dist/opencode.js +59 -5
  28. package/dist/parse-permission-rules.test.js +117 -0
  29. package/dist/queue-drain-after-interactive-ui.e2e.test.js +119 -0
  30. package/dist/session-handler/thread-session-runtime.js +68 -29
  31. package/dist/startup-time.e2e.test.js +295 -0
  32. package/dist/store.js +1 -0
  33. package/dist/system-message.js +3 -1
  34. package/dist/task-runner.js +7 -3
  35. package/dist/task-schedule.js +12 -0
  36. package/dist/thread-message-queue.e2e.test.js +13 -1
  37. package/dist/undo-redo.e2e.test.js +166 -0
  38. package/dist/utils.js +4 -1
  39. package/dist/voice-attachment.js +34 -0
  40. package/dist/voice-handler.js +11 -9
  41. package/dist/voice-message.e2e.test.js +78 -0
  42. package/dist/voice.test.js +31 -0
  43. package/package.json +12 -7
  44. package/skills/egaki/SKILL.md +80 -15
  45. package/skills/errore/SKILL.md +13 -0
  46. package/skills/lintcn/SKILL.md +749 -0
  47. package/skills/npm-package/SKILL.md +17 -3
  48. package/skills/spiceflow/SKILL.md +14 -0
  49. package/skills/zele/SKILL.md +9 -0
  50. package/src/anthropic-auth-plugin.ts +732 -0
  51. package/src/channel-management.ts +2 -2
  52. package/src/cli.ts +354 -132
  53. package/src/commands/action-buttons.ts +1 -0
  54. package/src/commands/login.ts +836 -337
  55. package/src/commands/model.ts +102 -7
  56. package/src/commands/paginated-select.ts +81 -0
  57. package/src/commands/resume.ts +6 -1
  58. package/src/commands/tasks.ts +293 -0
  59. package/src/commands/undo-redo.ts +87 -20
  60. package/src/context-awareness-plugin.ts +469 -0
  61. package/src/database.ts +138 -7
  62. package/src/db.ts +40 -1
  63. package/src/discord-bot.ts +46 -19
  64. package/src/discord-urls.ts +12 -0
  65. package/src/errors.ts +1 -1
  66. package/src/hrana-server.ts +124 -3
  67. package/src/interaction-handler.ts +41 -9
  68. package/src/ipc-tools-plugin.ts +228 -0
  69. package/src/message-preprocessing.ts +82 -11
  70. package/src/onboarding-welcome.ts +1 -1
  71. package/src/opencode-interrupt-plugin.ts +164 -91
  72. package/src/opencode-plugin.ts +13 -483
  73. package/src/opencode.ts +60 -5
  74. package/src/parse-permission-rules.test.ts +127 -0
  75. package/src/queue-drain-after-interactive-ui.e2e.test.ts +151 -0
  76. package/src/session-handler/thread-runtime-state.ts +4 -1
  77. package/src/session-handler/thread-session-runtime.ts +82 -20
  78. package/src/startup-time.e2e.test.ts +372 -0
  79. package/src/store.ts +8 -0
  80. package/src/system-message.ts +10 -1
  81. package/src/task-runner.ts +9 -22
  82. package/src/task-schedule.ts +15 -0
  83. package/src/thread-message-queue.e2e.test.ts +14 -1
  84. package/src/undo-redo.e2e.test.ts +207 -0
  85. package/src/utils.ts +7 -0
  86. package/src/voice-attachment.ts +51 -0
  87. package/src/voice-handler.ts +15 -7
  88. package/src/voice-message.e2e.test.ts +95 -0
  89. package/src/voice.test.ts +36 -0
  90. package/src/onboarding-tutorial-plugin.ts +0 -93
@@ -2,6 +2,12 @@
2
2
  // step boundary, with a hard timeout as fallback.
3
3
  // Tracks only whether each user message has started processing by
4
4
  // correlating assistant message parentID events.
5
+ //
6
+ // State design: all mutable state (pending messages, recovery locks, event
7
+ // waiters, latest assistant IDs) is encapsulated in a closure-based factory
8
+ // (createInterruptState). The plugin hooks only interact with the returned
9
+ // API — they cannot directly touch Maps/Sets or break invariants like
10
+ // forgetting to clear a timer.
5
11
 
6
12
  import type { Plugin } from '@opencode-ai/plugin'
7
13
 
@@ -42,17 +48,19 @@ function getInterruptStepTimeoutMsFromEnv(): number {
42
48
  return parsed
43
49
  }
44
50
 
45
- // Interrupt a session when a queued user message has not started yet.
46
- // "Started" is detected when an assistant message.updated has parentID equal to
47
- // the queued user message ID.
48
- const interruptOpencodeSessionOnUserMessage: Plugin = async (ctx) => {
49
- const interruptStepTimeoutMs = getInterruptStepTimeoutMsFromEnv()
51
+ // ── Encapsulated interrupt state ─────────────────────────────────
52
+ // All 4 mutable variables (pendingByMessageId, latestAssistantMessageID,
53
+ // recoveringSessions, waiters) are trapped inside this closure. The plugin
54
+ // hooks only see the returned API methods — they cannot break invariants
55
+ // like forgetting to clear a timer or leaving a stale recovery lock.
56
+
57
+ function createInterruptState() {
50
58
  const pendingByMessageId = new Map<string, PendingMessage>()
51
59
  const latestAssistantMessageIDBySession = new Map<string, string>()
52
60
  const recoveringSessions = new Set<string>()
53
61
  const waiters = new Set<EventWaiter>()
54
62
 
55
- function clearPendingByMessageId({ messageID }: { messageID: string }): void {
63
+ function clearPending(messageID: string): void {
56
64
  const pending = pendingByMessageId.get(messageID)
57
65
  if (!pending) {
58
66
  return
@@ -61,6 +69,15 @@ const interruptOpencodeSessionOnUserMessage: Plugin = async (ctx) => {
61
69
  pendingByMessageId.delete(messageID)
62
70
  }
63
71
 
72
+ function dispatchEvent(event: InterruptEvent): void {
73
+ Array.from(waiters).forEach((waiter) => {
74
+ if (!waiter.match(event)) {
75
+ return
76
+ }
77
+ waiter.finish()
78
+ })
79
+ }
80
+
64
81
  function waitForEvent(input: {
65
82
  match: (event: InterruptEvent) => boolean
66
83
  timeoutMs: number
@@ -84,44 +101,7 @@ const interruptOpencodeSessionOnUserMessage: Plugin = async (ctx) => {
84
101
  })
85
102
  }
86
103
 
87
- function scheduleTimeout({
88
- messageID,
89
- sessionID,
90
- delayMs,
91
- }: {
92
- messageID: string
93
- sessionID: string
94
- delayMs: number
95
- }): void {
96
- const existing = pendingByMessageId.get(messageID)
97
- if (existing) {
98
- clearTimeout(existing.timer)
99
- }
100
-
101
- const timer = setTimeout(() => {
102
- void interruptPendingMessage({ messageID })
103
- }, delayMs)
104
-
105
- pendingByMessageId.set(messageID, {
106
- sessionID,
107
- started: false,
108
- timer,
109
- abortAfterStepMessageID: latestAssistantMessageIDBySession.get(sessionID),
110
- agent: undefined,
111
- model: undefined,
112
- })
113
- }
114
-
115
- function markStarted({ messageID }: { messageID: string }): void {
116
- const pending = pendingByMessageId.get(messageID)
117
- if (!pending) {
118
- return
119
- }
120
- pending.started = true
121
- clearPendingByMessageId({ messageID })
122
- }
123
-
124
- function getNextPendingMessage({ sessionID }: { sessionID: string }):
104
+ function getNextPendingForSession(sessionID: string):
125
105
  | { messageID: string; pending: PendingMessage }
126
106
  | undefined {
127
107
  for (const [messageID, pending] of pendingByMessageId.entries()) {
@@ -136,26 +116,124 @@ const interruptOpencodeSessionOnUserMessage: Plugin = async (ctx) => {
136
116
  return undefined
137
117
  }
138
118
 
139
- async function interruptPendingMessage({ messageID }: { messageID: string }): Promise<void> {
140
- const pending = pendingByMessageId.get(messageID)
119
+ return {
120
+ dispatchEvent,
121
+ waitForEvent,
122
+ getNextPendingForSession,
123
+
124
+ hasPending(messageID: string): boolean {
125
+ return pendingByMessageId.has(messageID)
126
+ },
127
+
128
+ getPending(messageID: string): PendingMessage | undefined {
129
+ return pendingByMessageId.get(messageID)
130
+ },
131
+
132
+ // Schedule a timeout to interrupt a pending message. Cleans up any
133
+ // existing timer for the same messageID before setting a new one.
134
+ schedulePending({
135
+ messageID,
136
+ sessionID,
137
+ delayMs,
138
+ onTimeout,
139
+ }: {
140
+ messageID: string
141
+ sessionID: string
142
+ delayMs: number
143
+ onTimeout: () => void
144
+ }): void {
145
+ const existing = pendingByMessageId.get(messageID)
146
+ if (existing) {
147
+ clearTimeout(existing.timer)
148
+ }
149
+ const timer = setTimeout(onTimeout, delayMs)
150
+ pendingByMessageId.set(messageID, {
151
+ sessionID,
152
+ started: false,
153
+ timer,
154
+ abortAfterStepMessageID: latestAssistantMessageIDBySession.get(sessionID),
155
+ agent: undefined,
156
+ model: undefined,
157
+ })
158
+ },
159
+
160
+ markStarted(messageID: string): void {
161
+ const pending = pendingByMessageId.get(messageID)
162
+ if (!pending) {
163
+ return
164
+ }
165
+ pending.started = true
166
+ clearPending(messageID)
167
+ },
168
+
169
+ clearPending,
170
+
171
+ isRecovering(sessionID: string): boolean {
172
+ return recoveringSessions.has(sessionID)
173
+ },
174
+
175
+ setRecovering(sessionID: string): void {
176
+ recoveringSessions.add(sessionID)
177
+ },
178
+
179
+ clearRecovering(sessionID: string): void {
180
+ recoveringSessions.delete(sessionID)
181
+ },
182
+
183
+ setLatestAssistantMessage(sessionID: string, messageID: string): void {
184
+ latestAssistantMessageIDBySession.set(sessionID, messageID)
185
+ },
186
+
187
+ clearLatestAssistantMessage(sessionID: string): void {
188
+ latestAssistantMessageIDBySession.delete(sessionID)
189
+ },
190
+
191
+ // Clean up all state for a deleted session — timers, recovery locks, etc.
192
+ cleanupSession(sessionID: string): void {
193
+ latestAssistantMessageIDBySession.delete(sessionID)
194
+ Array.from(pendingByMessageId.entries()).forEach(([messageID, pending]) => {
195
+ if (pending.sessionID !== sessionID) {
196
+ return
197
+ }
198
+ clearPending(messageID)
199
+ })
200
+ },
201
+ }
202
+ }
203
+
204
+ // ── Plugin ───────────────────────────────────────────────────────
205
+
206
+ const interruptOpencodeSessionOnUserMessage: Plugin = async (ctx) => {
207
+ const interruptStepTimeoutMs = getInterruptStepTimeoutMsFromEnv()
208
+ const state = createInterruptState()
209
+
210
+ async function interruptPendingMessage(messageID: string): Promise<void> {
211
+ const pending = state.getPending(messageID)
141
212
  if (!pending) {
142
- clearPendingByMessageId({ messageID })
213
+ state.clearPending(messageID)
143
214
  return
144
215
  }
145
216
  if (pending.started) {
146
- clearPendingByMessageId({ messageID })
217
+ state.clearPending(messageID)
147
218
  return
148
219
  }
149
220
 
150
221
  const sessionID = pending.sessionID
151
- if (recoveringSessions.has(sessionID)) {
152
- scheduleTimeout({ messageID, sessionID, delayMs: 200 })
222
+ if (state.isRecovering(sessionID)) {
223
+ state.schedulePending({
224
+ messageID,
225
+ sessionID,
226
+ delayMs: 200,
227
+ onTimeout: () => {
228
+ void interruptPendingMessage(messageID)
229
+ },
230
+ })
153
231
  return
154
232
  }
155
233
 
156
- recoveringSessions.add(sessionID)
234
+ state.setRecovering(sessionID)
157
235
  try {
158
- const abortedAssistantWait = waitForEvent({
236
+ const abortedAssistantWait = state.waitForEvent({
159
237
  match: (event) => {
160
238
  return (
161
239
  event.type === 'message.updated'
@@ -166,7 +244,7 @@ const interruptOpencodeSessionOnUserMessage: Plugin = async (ctx) => {
166
244
  },
167
245
  timeoutMs: 5_000,
168
246
  })
169
- const idleWait = waitForEvent({
247
+ const idleWait = state.waitForEvent({
170
248
  match: (event) => {
171
249
  return event.type === 'session.idle' && event.properties.sessionID === sessionID
172
250
  },
@@ -179,9 +257,9 @@ const interruptOpencodeSessionOnUserMessage: Plugin = async (ctx) => {
179
257
  await abortedAssistantWait
180
258
  await idleWait
181
259
 
182
- const currentPending = pendingByMessageId.get(messageID)
260
+ const currentPending = state.getPending(messageID)
183
261
  if (!currentPending || currentPending.started) {
184
- clearPendingByMessageId({ messageID })
262
+ state.clearPending(messageID)
185
263
  return
186
264
  }
187
265
 
@@ -191,10 +269,7 @@ const interruptOpencodeSessionOnUserMessage: Plugin = async (ctx) => {
191
269
  const resumeBody: {
192
270
  parts: []
193
271
  agent?: string
194
- model?: {
195
- providerID: string
196
- modelID: string
197
- }
272
+ model?: { providerID: string; modelID: string }
198
273
  } = { parts: [] }
199
274
  if (currentPending.agent) {
200
275
  resumeBody.agent = currentPending.agent
@@ -207,35 +282,37 @@ const interruptOpencodeSessionOnUserMessage: Plugin = async (ctx) => {
207
282
  path: { id: sessionID },
208
283
  body: resumeBody,
209
284
  })
210
- clearPendingByMessageId({ messageID })
285
+ state.clearPending(messageID)
211
286
 
212
- const nextPending = getNextPendingMessage({ sessionID })
287
+ const nextPending = state.getNextPendingForSession(sessionID)
213
288
  if (!nextPending) {
214
289
  return
215
290
  }
216
- scheduleTimeout({ messageID: nextPending.messageID, sessionID, delayMs: 50 })
291
+ state.schedulePending({
292
+ messageID: nextPending.messageID,
293
+ sessionID,
294
+ delayMs: 50,
295
+ onTimeout: () => {
296
+ void interruptPendingMessage(nextPending.messageID)
297
+ },
298
+ })
217
299
  } finally {
218
- recoveringSessions.delete(sessionID)
300
+ state.clearRecovering(sessionID)
219
301
  }
220
302
  }
221
303
 
222
304
  return {
223
305
  async event({ event }) {
224
- Array.from(waiters).forEach((waiter) => {
225
- if (!waiter.match(event)) {
226
- return
227
- }
228
- waiter.finish()
229
- })
306
+ state.dispatchEvent(event)
230
307
 
231
308
  if (event.type === 'message.part.updated' && event.properties.part.type === 'step-finish') {
232
- const nextPending = getNextPendingMessage({
233
- sessionID: event.properties.part.sessionID,
234
- })
309
+ const nextPending = state.getNextPendingForSession(
310
+ event.properties.part.sessionID,
311
+ )
235
312
  if (!nextPending) {
236
313
  return
237
314
  }
238
- if (recoveringSessions.has(nextPending.pending.sessionID)) {
315
+ if (state.isRecovering(nextPending.pending.sessionID)) {
239
316
  return
240
317
  }
241
318
  if (!nextPending.pending.abortAfterStepMessageID) {
@@ -244,21 +321,21 @@ const interruptOpencodeSessionOnUserMessage: Plugin = async (ctx) => {
244
321
  if (event.properties.part.messageID !== nextPending.pending.abortAfterStepMessageID) {
245
322
  return
246
323
  }
247
- void interruptPendingMessage({ messageID: nextPending.messageID })
324
+ void interruptPendingMessage(nextPending.messageID)
248
325
  return
249
326
  }
250
327
 
251
328
  if (event.type === 'message.updated' && event.properties.info.role === 'assistant') {
252
329
  if (!event.properties.info.error) {
253
- latestAssistantMessageIDBySession.set(
330
+ state.setLatestAssistantMessage(
254
331
  event.properties.info.sessionID,
255
332
  event.properties.info.id,
256
333
  )
257
334
  }
258
335
 
259
- const nextPending = getNextPendingMessage({
260
- sessionID: event.properties.info.sessionID,
261
- })
336
+ const nextPending = state.getNextPendingForSession(
337
+ event.properties.info.sessionID,
338
+ )
262
339
  if (
263
340
  nextPending
264
341
  && !nextPending.pending.started
@@ -269,24 +346,17 @@ const interruptOpencodeSessionOnUserMessage: Plugin = async (ctx) => {
269
346
  }
270
347
 
271
348
  const parentID = event.properties.info.parentID
272
- markStarted({ messageID: parentID })
349
+ state.markStarted(parentID)
273
350
  return
274
351
  }
275
352
 
276
353
  if (event.type === 'session.idle') {
277
- latestAssistantMessageIDBySession.delete(event.properties.sessionID)
354
+ state.clearLatestAssistantMessage(event.properties.sessionID)
278
355
  return
279
356
  }
280
357
 
281
358
  if (event.type === 'session.deleted') {
282
- const sessionID = event.properties.info.id
283
- latestAssistantMessageIDBySession.delete(sessionID)
284
- Array.from(pendingByMessageId.entries()).forEach(([messageID, pending]) => {
285
- if (pending.sessionID !== sessionID) {
286
- return
287
- }
288
- clearPendingByMessageId({ messageID })
289
- })
359
+ state.cleanupSession(event.properties.info.id)
290
360
  }
291
361
  },
292
362
 
@@ -306,15 +376,18 @@ const interruptOpencodeSessionOnUserMessage: Plugin = async (ctx) => {
306
376
  if (!messageID) {
307
377
  return
308
378
  }
309
- if (pendingByMessageId.has(messageID)) {
379
+ if (state.hasPending(messageID)) {
310
380
  return
311
381
  }
312
- scheduleTimeout({
382
+ state.schedulePending({
313
383
  messageID,
314
384
  sessionID,
315
385
  delayMs: interruptStepTimeoutMs,
386
+ onTimeout: () => {
387
+ void interruptPendingMessage(messageID)
388
+ },
316
389
  })
317
- const pending = pendingByMessageId.get(messageID)
390
+ const pending = state.getPending(messageID)
318
391
  if (!pending) {
319
392
  return
320
393
  }