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.
- package/dist/anthropic-auth-plugin.js +628 -0
- package/dist/channel-management.js +2 -2
- package/dist/cli.js +316 -129
- package/dist/commands/action-buttons.js +1 -1
- package/dist/commands/login.js +634 -277
- package/dist/commands/model.js +91 -6
- package/dist/commands/paginated-select.js +57 -0
- package/dist/commands/resume.js +2 -2
- package/dist/commands/tasks.js +205 -0
- package/dist/commands/undo-redo.js +80 -18
- package/dist/context-awareness-plugin.js +347 -0
- package/dist/database.js +103 -7
- package/dist/db.js +39 -1
- package/dist/discord-bot.js +42 -19
- package/dist/discord-urls.js +11 -0
- package/dist/discord-ws-proxy.js +350 -0
- package/dist/discord-ws-proxy.test.js +500 -0
- package/dist/errors.js +1 -1
- package/dist/gateway-session.js +163 -0
- package/dist/hrana-server.js +114 -4
- package/dist/interaction-handler.js +30 -7
- package/dist/ipc-tools-plugin.js +186 -0
- package/dist/message-preprocessing.js +56 -11
- package/dist/onboarding-welcome.js +1 -1
- package/dist/opencode-interrupt-plugin.js +133 -75
- package/dist/opencode-plugin.js +12 -389
- package/dist/opencode.js +59 -5
- package/dist/parse-permission-rules.test.js +117 -0
- package/dist/queue-drain-after-interactive-ui.e2e.test.js +119 -0
- package/dist/session-handler/thread-session-runtime.js +68 -29
- package/dist/startup-time.e2e.test.js +295 -0
- package/dist/store.js +1 -0
- package/dist/system-message.js +3 -1
- package/dist/task-runner.js +7 -3
- package/dist/task-schedule.js +12 -0
- package/dist/thread-message-queue.e2e.test.js +13 -1
- package/dist/undo-redo.e2e.test.js +166 -0
- package/dist/utils.js +4 -1
- package/dist/voice-attachment.js +34 -0
- package/dist/voice-handler.js +11 -9
- package/dist/voice-message.e2e.test.js +78 -0
- package/dist/voice.test.js +31 -0
- package/package.json +12 -7
- package/skills/egaki/SKILL.md +80 -15
- package/skills/errore/SKILL.md +13 -0
- package/skills/lintcn/SKILL.md +749 -0
- package/skills/npm-package/SKILL.md +17 -3
- package/skills/spiceflow/SKILL.md +14 -0
- package/skills/zele/SKILL.md +9 -0
- package/src/anthropic-auth-plugin.ts +732 -0
- package/src/channel-management.ts +2 -2
- package/src/cli.ts +354 -132
- package/src/commands/action-buttons.ts +1 -0
- package/src/commands/login.ts +836 -337
- package/src/commands/model.ts +102 -7
- package/src/commands/paginated-select.ts +81 -0
- package/src/commands/resume.ts +6 -1
- package/src/commands/tasks.ts +293 -0
- package/src/commands/undo-redo.ts +87 -20
- package/src/context-awareness-plugin.ts +469 -0
- package/src/database.ts +138 -7
- package/src/db.ts +40 -1
- package/src/discord-bot.ts +46 -19
- package/src/discord-urls.ts +12 -0
- package/src/errors.ts +1 -1
- package/src/hrana-server.ts +124 -3
- package/src/interaction-handler.ts +41 -9
- package/src/ipc-tools-plugin.ts +228 -0
- package/src/message-preprocessing.ts +82 -11
- package/src/onboarding-welcome.ts +1 -1
- package/src/opencode-interrupt-plugin.ts +164 -91
- package/src/opencode-plugin.ts +13 -483
- package/src/opencode.ts +60 -5
- package/src/parse-permission-rules.test.ts +127 -0
- package/src/queue-drain-after-interactive-ui.e2e.test.ts +151 -0
- package/src/session-handler/thread-runtime-state.ts +4 -1
- package/src/session-handler/thread-session-runtime.ts +82 -20
- package/src/startup-time.e2e.test.ts +372 -0
- package/src/store.ts +8 -0
- package/src/system-message.ts +10 -1
- package/src/task-runner.ts +9 -22
- package/src/task-schedule.ts +15 -0
- package/src/thread-message-queue.e2e.test.ts +14 -1
- package/src/undo-redo.e2e.test.ts +207 -0
- package/src/utils.ts +7 -0
- package/src/voice-attachment.ts +51 -0
- package/src/voice-handler.ts +15 -7
- package/src/voice-message.e2e.test.ts +95 -0
- package/src/voice.test.ts +36 -0
- 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
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
140
|
-
|
|
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
|
-
|
|
213
|
+
state.clearPending(messageID)
|
|
143
214
|
return
|
|
144
215
|
}
|
|
145
216
|
if (pending.started) {
|
|
146
|
-
|
|
217
|
+
state.clearPending(messageID)
|
|
147
218
|
return
|
|
148
219
|
}
|
|
149
220
|
|
|
150
221
|
const sessionID = pending.sessionID
|
|
151
|
-
if (
|
|
152
|
-
|
|
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
|
-
|
|
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 =
|
|
260
|
+
const currentPending = state.getPending(messageID)
|
|
183
261
|
if (!currentPending || currentPending.started) {
|
|
184
|
-
|
|
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
|
-
|
|
285
|
+
state.clearPending(messageID)
|
|
211
286
|
|
|
212
|
-
const nextPending =
|
|
287
|
+
const nextPending = state.getNextPendingForSession(sessionID)
|
|
213
288
|
if (!nextPending) {
|
|
214
289
|
return
|
|
215
290
|
}
|
|
216
|
-
|
|
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
|
-
|
|
300
|
+
state.clearRecovering(sessionID)
|
|
219
301
|
}
|
|
220
302
|
}
|
|
221
303
|
|
|
222
304
|
return {
|
|
223
305
|
async event({ event }) {
|
|
224
|
-
|
|
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 =
|
|
233
|
-
|
|
234
|
-
|
|
309
|
+
const nextPending = state.getNextPendingForSession(
|
|
310
|
+
event.properties.part.sessionID,
|
|
311
|
+
)
|
|
235
312
|
if (!nextPending) {
|
|
236
313
|
return
|
|
237
314
|
}
|
|
238
|
-
if (
|
|
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(
|
|
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
|
-
|
|
330
|
+
state.setLatestAssistantMessage(
|
|
254
331
|
event.properties.info.sessionID,
|
|
255
332
|
event.properties.info.id,
|
|
256
333
|
)
|
|
257
334
|
}
|
|
258
335
|
|
|
259
|
-
const nextPending =
|
|
260
|
-
|
|
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(
|
|
349
|
+
state.markStarted(parentID)
|
|
273
350
|
return
|
|
274
351
|
}
|
|
275
352
|
|
|
276
353
|
if (event.type === 'session.idle') {
|
|
277
|
-
|
|
354
|
+
state.clearLatestAssistantMessage(event.properties.sessionID)
|
|
278
355
|
return
|
|
279
356
|
}
|
|
280
357
|
|
|
281
358
|
if (event.type === 'session.deleted') {
|
|
282
|
-
|
|
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 (
|
|
379
|
+
if (state.hasPending(messageID)) {
|
|
310
380
|
return
|
|
311
381
|
}
|
|
312
|
-
|
|
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 =
|
|
390
|
+
const pending = state.getPending(messageID)
|
|
318
391
|
if (!pending) {
|
|
319
392
|
return
|
|
320
393
|
}
|