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
  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
- // Interrupt a session when a queued user message has not started yet.
18
- // "Started" is detected when an assistant message.updated has parentID equal to
19
- // the queued user message ID.
20
- const interruptOpencodeSessionOnUserMessage = async (ctx) => {
21
- const interruptStepTimeoutMs = getInterruptStepTimeoutMsFromEnv();
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 clearPendingByMessageId({ messageID }) {
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 scheduleTimeout({ messageID, sessionID, delayMs, }) {
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
- async function interruptPendingMessage({ messageID }) {
91
- const pending = pendingByMessageId.get(messageID);
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
- clearPendingByMessageId({ messageID });
150
+ state.clearPending(messageID);
94
151
  return;
95
152
  }
96
153
  if (pending.started) {
97
- clearPendingByMessageId({ messageID });
154
+ state.clearPending(messageID);
98
155
  return;
99
156
  }
100
157
  const sessionID = pending.sessionID;
101
- if (recoveringSessions.has(sessionID)) {
102
- scheduleTimeout({ messageID, sessionID, delayMs: 200 });
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
- recoveringSessions.add(sessionID);
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 = pendingByMessageId.get(messageID);
191
+ const currentPending = state.getPending(messageID);
128
192
  if (!currentPending || currentPending.started) {
129
- clearPendingByMessageId({ messageID });
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
- clearPendingByMessageId({ messageID });
147
- const nextPending = getNextPendingMessage({ sessionID });
210
+ state.clearPending(messageID);
211
+ const nextPending = state.getNextPendingForSession(sessionID);
148
212
  if (!nextPending) {
149
213
  return;
150
214
  }
151
- scheduleTimeout({ messageID: nextPending.messageID, sessionID, delayMs: 50 });
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
- recoveringSessions.delete(sessionID);
225
+ state.clearRecovering(sessionID);
155
226
  }
156
227
  }
157
228
  return {
158
229
  async event({ event }) {
159
- Array.from(waiters).forEach((waiter) => {
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 = getNextPendingMessage({
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 (recoveringSessions.has(nextPending.pending.sessionID)) {
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({ messageID: nextPending.messageID });
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
- latestAssistantMessageIDBySession.set(event.properties.info.sessionID, event.properties.info.id);
250
+ state.setLatestAssistantMessage(event.properties.info.sessionID, event.properties.info.id);
187
251
  }
188
- const nextPending = getNextPendingMessage({
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({ messageID: parentID });
260
+ state.markStarted(parentID);
199
261
  return;
200
262
  }
201
263
  if (event.type === 'session.idle') {
202
- latestAssistantMessageIDBySession.delete(event.properties.sessionID);
264
+ state.clearLatestAssistantMessage(event.properties.sessionID);
203
265
  return;
204
266
  }
205
267
  if (event.type === 'session.deleted') {
206
- const sessionID = event.properties.info.id;
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 (pendingByMessageId.has(messageID)) {
285
+ if (state.hasPending(messageID)) {
231
286
  return;
232
287
  }
233
- scheduleTimeout({
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 = pendingByMessageId.get(messageID);
296
+ const pending = state.getPending(messageID);
239
297
  if (!pending) {
240
298
  return;
241
299
  }