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
|
const DEFAULT_INTERRUPT_STEP_TIMEOUT_MS = 3_000;
|
|
6
12
|
function getInterruptStepTimeoutMsFromEnv() {
|
|
7
13
|
const raw = process.env['KIMAKI_INTERRUPT_STEP_TIMEOUT_MS'];
|
|
@@ -14,16 +20,17 @@ function getInterruptStepTimeoutMsFromEnv() {
|
|
|
14
20
|
}
|
|
15
21
|
return parsed;
|
|
16
22
|
}
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
// ── Encapsulated interrupt state ─────────────────────────────────
|
|
24
|
+
// All 4 mutable variables (pendingByMessageId, latestAssistantMessageID,
|
|
25
|
+
// recoveringSessions, waiters) are trapped inside this closure. The plugin
|
|
26
|
+
// hooks only see the returned API methods — they cannot break invariants
|
|
27
|
+
// like forgetting to clear a timer or leaving a stale recovery lock.
|
|
28
|
+
function createInterruptState() {
|
|
22
29
|
const pendingByMessageId = new Map();
|
|
23
30
|
const latestAssistantMessageIDBySession = new Map();
|
|
24
31
|
const recoveringSessions = new Set();
|
|
25
32
|
const waiters = new Set();
|
|
26
|
-
function
|
|
33
|
+
function clearPending(messageID) {
|
|
27
34
|
const pending = pendingByMessageId.get(messageID);
|
|
28
35
|
if (!pending) {
|
|
29
36
|
return;
|
|
@@ -31,6 +38,14 @@ const interruptOpencodeSessionOnUserMessage = async (ctx) => {
|
|
|
31
38
|
clearTimeout(pending.timer);
|
|
32
39
|
pendingByMessageId.delete(messageID);
|
|
33
40
|
}
|
|
41
|
+
function dispatchEvent(event) {
|
|
42
|
+
Array.from(waiters).forEach((waiter) => {
|
|
43
|
+
if (!waiter.match(event)) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
waiter.finish();
|
|
47
|
+
});
|
|
48
|
+
}
|
|
34
49
|
function waitForEvent(input) {
|
|
35
50
|
return new Promise((resolve) => {
|
|
36
51
|
const finish = (matched) => {
|
|
@@ -50,32 +65,7 @@ const interruptOpencodeSessionOnUserMessage = async (ctx) => {
|
|
|
50
65
|
waiters.add(waiter);
|
|
51
66
|
});
|
|
52
67
|
}
|
|
53
|
-
function
|
|
54
|
-
const existing = pendingByMessageId.get(messageID);
|
|
55
|
-
if (existing) {
|
|
56
|
-
clearTimeout(existing.timer);
|
|
57
|
-
}
|
|
58
|
-
const timer = setTimeout(() => {
|
|
59
|
-
void interruptPendingMessage({ messageID });
|
|
60
|
-
}, delayMs);
|
|
61
|
-
pendingByMessageId.set(messageID, {
|
|
62
|
-
sessionID,
|
|
63
|
-
started: false,
|
|
64
|
-
timer,
|
|
65
|
-
abortAfterStepMessageID: latestAssistantMessageIDBySession.get(sessionID),
|
|
66
|
-
agent: undefined,
|
|
67
|
-
model: undefined,
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
function markStarted({ messageID }) {
|
|
71
|
-
const pending = pendingByMessageId.get(messageID);
|
|
72
|
-
if (!pending) {
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
pending.started = true;
|
|
76
|
-
clearPendingByMessageId({ messageID });
|
|
77
|
-
}
|
|
78
|
-
function getNextPendingMessage({ sessionID }) {
|
|
68
|
+
function getNextPendingForSession(sessionID) {
|
|
79
69
|
for (const [messageID, pending] of pendingByMessageId.entries()) {
|
|
80
70
|
if (pending.sessionID !== sessionID) {
|
|
81
71
|
continue;
|
|
@@ -87,24 +77,98 @@ const interruptOpencodeSessionOnUserMessage = async (ctx) => {
|
|
|
87
77
|
}
|
|
88
78
|
return undefined;
|
|
89
79
|
}
|
|
90
|
-
|
|
91
|
-
|
|
80
|
+
return {
|
|
81
|
+
dispatchEvent,
|
|
82
|
+
waitForEvent,
|
|
83
|
+
getNextPendingForSession,
|
|
84
|
+
hasPending(messageID) {
|
|
85
|
+
return pendingByMessageId.has(messageID);
|
|
86
|
+
},
|
|
87
|
+
getPending(messageID) {
|
|
88
|
+
return pendingByMessageId.get(messageID);
|
|
89
|
+
},
|
|
90
|
+
// Schedule a timeout to interrupt a pending message. Cleans up any
|
|
91
|
+
// existing timer for the same messageID before setting a new one.
|
|
92
|
+
schedulePending({ messageID, sessionID, delayMs, onTimeout, }) {
|
|
93
|
+
const existing = pendingByMessageId.get(messageID);
|
|
94
|
+
if (existing) {
|
|
95
|
+
clearTimeout(existing.timer);
|
|
96
|
+
}
|
|
97
|
+
const timer = setTimeout(onTimeout, delayMs);
|
|
98
|
+
pendingByMessageId.set(messageID, {
|
|
99
|
+
sessionID,
|
|
100
|
+
started: false,
|
|
101
|
+
timer,
|
|
102
|
+
abortAfterStepMessageID: latestAssistantMessageIDBySession.get(sessionID),
|
|
103
|
+
agent: undefined,
|
|
104
|
+
model: undefined,
|
|
105
|
+
});
|
|
106
|
+
},
|
|
107
|
+
markStarted(messageID) {
|
|
108
|
+
const pending = pendingByMessageId.get(messageID);
|
|
109
|
+
if (!pending) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
pending.started = true;
|
|
113
|
+
clearPending(messageID);
|
|
114
|
+
},
|
|
115
|
+
clearPending,
|
|
116
|
+
isRecovering(sessionID) {
|
|
117
|
+
return recoveringSessions.has(sessionID);
|
|
118
|
+
},
|
|
119
|
+
setRecovering(sessionID) {
|
|
120
|
+
recoveringSessions.add(sessionID);
|
|
121
|
+
},
|
|
122
|
+
clearRecovering(sessionID) {
|
|
123
|
+
recoveringSessions.delete(sessionID);
|
|
124
|
+
},
|
|
125
|
+
setLatestAssistantMessage(sessionID, messageID) {
|
|
126
|
+
latestAssistantMessageIDBySession.set(sessionID, messageID);
|
|
127
|
+
},
|
|
128
|
+
clearLatestAssistantMessage(sessionID) {
|
|
129
|
+
latestAssistantMessageIDBySession.delete(sessionID);
|
|
130
|
+
},
|
|
131
|
+
// Clean up all state for a deleted session — timers, recovery locks, etc.
|
|
132
|
+
cleanupSession(sessionID) {
|
|
133
|
+
latestAssistantMessageIDBySession.delete(sessionID);
|
|
134
|
+
Array.from(pendingByMessageId.entries()).forEach(([messageID, pending]) => {
|
|
135
|
+
if (pending.sessionID !== sessionID) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
clearPending(messageID);
|
|
139
|
+
});
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
// ── Plugin ───────────────────────────────────────────────────────
|
|
144
|
+
const interruptOpencodeSessionOnUserMessage = async (ctx) => {
|
|
145
|
+
const interruptStepTimeoutMs = getInterruptStepTimeoutMsFromEnv();
|
|
146
|
+
const state = createInterruptState();
|
|
147
|
+
async function interruptPendingMessage(messageID) {
|
|
148
|
+
const pending = state.getPending(messageID);
|
|
92
149
|
if (!pending) {
|
|
93
|
-
|
|
150
|
+
state.clearPending(messageID);
|
|
94
151
|
return;
|
|
95
152
|
}
|
|
96
153
|
if (pending.started) {
|
|
97
|
-
|
|
154
|
+
state.clearPending(messageID);
|
|
98
155
|
return;
|
|
99
156
|
}
|
|
100
157
|
const sessionID = pending.sessionID;
|
|
101
|
-
if (
|
|
102
|
-
|
|
158
|
+
if (state.isRecovering(sessionID)) {
|
|
159
|
+
state.schedulePending({
|
|
160
|
+
messageID,
|
|
161
|
+
sessionID,
|
|
162
|
+
delayMs: 200,
|
|
163
|
+
onTimeout: () => {
|
|
164
|
+
void interruptPendingMessage(messageID);
|
|
165
|
+
},
|
|
166
|
+
});
|
|
103
167
|
return;
|
|
104
168
|
}
|
|
105
|
-
|
|
169
|
+
state.setRecovering(sessionID);
|
|
106
170
|
try {
|
|
107
|
-
const abortedAssistantWait = waitForEvent({
|
|
171
|
+
const abortedAssistantWait = state.waitForEvent({
|
|
108
172
|
match: (event) => {
|
|
109
173
|
return (event.type === 'message.updated'
|
|
110
174
|
&& event.properties.info.role === 'assistant'
|
|
@@ -113,7 +177,7 @@ const interruptOpencodeSessionOnUserMessage = async (ctx) => {
|
|
|
113
177
|
},
|
|
114
178
|
timeoutMs: 5_000,
|
|
115
179
|
});
|
|
116
|
-
const idleWait = waitForEvent({
|
|
180
|
+
const idleWait = state.waitForEvent({
|
|
117
181
|
match: (event) => {
|
|
118
182
|
return event.type === 'session.idle' && event.properties.sessionID === sessionID;
|
|
119
183
|
},
|
|
@@ -124,9 +188,9 @@ const interruptOpencodeSessionOnUserMessage = async (ctx) => {
|
|
|
124
188
|
});
|
|
125
189
|
await abortedAssistantWait;
|
|
126
190
|
await idleWait;
|
|
127
|
-
const currentPending =
|
|
191
|
+
const currentPending = state.getPending(messageID);
|
|
128
192
|
if (!currentPending || currentPending.started) {
|
|
129
|
-
|
|
193
|
+
state.clearPending(messageID);
|
|
130
194
|
return;
|
|
131
195
|
}
|
|
132
196
|
// Keep the queued user message execution context across abort+resume.
|
|
@@ -143,33 +207,33 @@ const interruptOpencodeSessionOnUserMessage = async (ctx) => {
|
|
|
143
207
|
path: { id: sessionID },
|
|
144
208
|
body: resumeBody,
|
|
145
209
|
});
|
|
146
|
-
|
|
147
|
-
const nextPending =
|
|
210
|
+
state.clearPending(messageID);
|
|
211
|
+
const nextPending = state.getNextPendingForSession(sessionID);
|
|
148
212
|
if (!nextPending) {
|
|
149
213
|
return;
|
|
150
214
|
}
|
|
151
|
-
|
|
215
|
+
state.schedulePending({
|
|
216
|
+
messageID: nextPending.messageID,
|
|
217
|
+
sessionID,
|
|
218
|
+
delayMs: 50,
|
|
219
|
+
onTimeout: () => {
|
|
220
|
+
void interruptPendingMessage(nextPending.messageID);
|
|
221
|
+
},
|
|
222
|
+
});
|
|
152
223
|
}
|
|
153
224
|
finally {
|
|
154
|
-
|
|
225
|
+
state.clearRecovering(sessionID);
|
|
155
226
|
}
|
|
156
227
|
}
|
|
157
228
|
return {
|
|
158
229
|
async event({ event }) {
|
|
159
|
-
|
|
160
|
-
if (!waiter.match(event)) {
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
waiter.finish();
|
|
164
|
-
});
|
|
230
|
+
state.dispatchEvent(event);
|
|
165
231
|
if (event.type === 'message.part.updated' && event.properties.part.type === 'step-finish') {
|
|
166
|
-
const nextPending =
|
|
167
|
-
sessionID: event.properties.part.sessionID,
|
|
168
|
-
});
|
|
232
|
+
const nextPending = state.getNextPendingForSession(event.properties.part.sessionID);
|
|
169
233
|
if (!nextPending) {
|
|
170
234
|
return;
|
|
171
235
|
}
|
|
172
|
-
if (
|
|
236
|
+
if (state.isRecovering(nextPending.pending.sessionID)) {
|
|
173
237
|
return;
|
|
174
238
|
}
|
|
175
239
|
if (!nextPending.pending.abortAfterStepMessageID) {
|
|
@@ -178,16 +242,14 @@ const interruptOpencodeSessionOnUserMessage = async (ctx) => {
|
|
|
178
242
|
if (event.properties.part.messageID !== nextPending.pending.abortAfterStepMessageID) {
|
|
179
243
|
return;
|
|
180
244
|
}
|
|
181
|
-
void interruptPendingMessage(
|
|
245
|
+
void interruptPendingMessage(nextPending.messageID);
|
|
182
246
|
return;
|
|
183
247
|
}
|
|
184
248
|
if (event.type === 'message.updated' && event.properties.info.role === 'assistant') {
|
|
185
249
|
if (!event.properties.info.error) {
|
|
186
|
-
|
|
250
|
+
state.setLatestAssistantMessage(event.properties.info.sessionID, event.properties.info.id);
|
|
187
251
|
}
|
|
188
|
-
const nextPending =
|
|
189
|
-
sessionID: event.properties.info.sessionID,
|
|
190
|
-
});
|
|
252
|
+
const nextPending = state.getNextPendingForSession(event.properties.info.sessionID);
|
|
191
253
|
if (nextPending
|
|
192
254
|
&& !nextPending.pending.started
|
|
193
255
|
&& !event.properties.info.error
|
|
@@ -195,22 +257,15 @@ const interruptOpencodeSessionOnUserMessage = async (ctx) => {
|
|
|
195
257
|
nextPending.pending.abortAfterStepMessageID = event.properties.info.id;
|
|
196
258
|
}
|
|
197
259
|
const parentID = event.properties.info.parentID;
|
|
198
|
-
markStarted(
|
|
260
|
+
state.markStarted(parentID);
|
|
199
261
|
return;
|
|
200
262
|
}
|
|
201
263
|
if (event.type === 'session.idle') {
|
|
202
|
-
|
|
264
|
+
state.clearLatestAssistantMessage(event.properties.sessionID);
|
|
203
265
|
return;
|
|
204
266
|
}
|
|
205
267
|
if (event.type === 'session.deleted') {
|
|
206
|
-
|
|
207
|
-
latestAssistantMessageIDBySession.delete(sessionID);
|
|
208
|
-
Array.from(pendingByMessageId.entries()).forEach(([messageID, pending]) => {
|
|
209
|
-
if (pending.sessionID !== sessionID) {
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
clearPendingByMessageId({ messageID });
|
|
213
|
-
});
|
|
268
|
+
state.cleanupSession(event.properties.info.id);
|
|
214
269
|
}
|
|
215
270
|
},
|
|
216
271
|
async 'chat.message'(input, output) {
|
|
@@ -227,15 +282,18 @@ const interruptOpencodeSessionOnUserMessage = async (ctx) => {
|
|
|
227
282
|
if (!messageID) {
|
|
228
283
|
return;
|
|
229
284
|
}
|
|
230
|
-
if (
|
|
285
|
+
if (state.hasPending(messageID)) {
|
|
231
286
|
return;
|
|
232
287
|
}
|
|
233
|
-
|
|
288
|
+
state.schedulePending({
|
|
234
289
|
messageID,
|
|
235
290
|
sessionID,
|
|
236
291
|
delayMs: interruptStepTimeoutMs,
|
|
292
|
+
onTimeout: () => {
|
|
293
|
+
void interruptPendingMessage(messageID);
|
|
294
|
+
},
|
|
237
295
|
});
|
|
238
|
-
const pending =
|
|
296
|
+
const pending = state.getPending(messageID);
|
|
239
297
|
if (!pending) {
|
|
240
298
|
return;
|
|
241
299
|
}
|