kimaki 0.4.47 → 0.4.48

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.
@@ -15,6 +15,67 @@ const logger = createLogger(LogPrefix.FORMATTING);
15
15
  function escapeInlineMarkdown(text) {
16
16
  return text.replace(/([*_~|`\\])/g, '\\$1');
17
17
  }
18
+ /**
19
+ * Parses a patchText string (apply_patch format) and counts additions/deletions per file.
20
+ * Patch format uses `*** Add File:`, `*** Update File:`, `*** Delete File:` headers,
21
+ * with diff lines prefixed by `+` (addition) or `-` (deletion) inside `@@` hunks.
22
+ */
23
+ function parsePatchCounts(patchText) {
24
+ const counts = new Map();
25
+ const lines = patchText.split('\n');
26
+ let currentFile = '';
27
+ let currentType = '';
28
+ let inHunk = false;
29
+ for (const line of lines) {
30
+ const addMatch = line.match(/^\*\*\* Add File:\s*(.+)/);
31
+ const updateMatch = line.match(/^\*\*\* Update File:\s*(.+)/);
32
+ const deleteMatch = line.match(/^\*\*\* Delete File:\s*(.+)/);
33
+ if (addMatch || updateMatch || deleteMatch) {
34
+ const match = addMatch || updateMatch || deleteMatch;
35
+ currentFile = (match?.[1] ?? '').trim();
36
+ currentType = addMatch ? 'add' : updateMatch ? 'update' : 'delete';
37
+ counts.set(currentFile, { additions: 0, deletions: 0 });
38
+ inHunk = false;
39
+ continue;
40
+ }
41
+ if (line.startsWith('@@')) {
42
+ inHunk = true;
43
+ continue;
44
+ }
45
+ if (line.startsWith('*** ')) {
46
+ inHunk = false;
47
+ continue;
48
+ }
49
+ if (!currentFile) {
50
+ continue;
51
+ }
52
+ const entry = counts.get(currentFile);
53
+ if (!entry) {
54
+ continue;
55
+ }
56
+ if (currentType === 'add') {
57
+ // all content lines in Add File are additions
58
+ if (line.length > 0 && !line.startsWith('*** ')) {
59
+ entry.additions++;
60
+ }
61
+ }
62
+ else if (currentType === 'delete') {
63
+ // all content lines in Delete File are deletions
64
+ if (line.length > 0 && !line.startsWith('*** ')) {
65
+ entry.deletions++;
66
+ }
67
+ }
68
+ else if (inHunk) {
69
+ if (line.startsWith('+')) {
70
+ entry.additions++;
71
+ }
72
+ else if (line.startsWith('-')) {
73
+ entry.deletions++;
74
+ }
75
+ }
76
+ }
77
+ return counts;
78
+ }
18
79
  /**
19
80
  * Normalize whitespace: convert newlines to spaces and collapse consecutive spaces.
20
81
  */
@@ -132,59 +193,20 @@ export function getToolSummaryText(part) {
132
193
  : `(+${added}-${removed})`;
133
194
  }
134
195
  if (part.tool === 'apply_patch') {
135
- const state = part.state;
136
- const rawFiles = state.metadata?.files;
137
- const partMetaFiles = part.metadata?.files;
138
- const filesList = Array.isArray(rawFiles)
139
- ? rawFiles
140
- : Array.isArray(partMetaFiles)
141
- ? partMetaFiles
142
- : [];
143
- const summarizeFiles = (files) => {
144
- const summarized = files
145
- .map((f) => {
146
- if (!f) {
147
- return null;
148
- }
149
- if (typeof f === 'string') {
150
- const fileName = f.split('/').pop() || '';
151
- return fileName ? `*${escapeInlineMarkdown(fileName)}* (+0-0)` : `(+0-0)`;
152
- }
153
- if (typeof f !== 'object') {
154
- return null;
155
- }
156
- const file = f;
157
- const pathStr = String(file.relativePath || file.filePath || file.path || '');
158
- const fileName = pathStr.split('/').pop() || '';
159
- const added = typeof file.additions === 'number' ? file.additions : 0;
160
- const removed = typeof file.deletions === 'number' ? file.deletions : 0;
161
- return fileName
162
- ? `*${escapeInlineMarkdown(fileName)}* (+${added}-${removed})`
163
- : `(+${added}-${removed})`;
164
- })
165
- .filter(Boolean)
166
- .join(', ');
167
- return summarized;
168
- };
169
- if (filesList.length > 0) {
170
- const summarized = summarizeFiles(filesList);
171
- if (summarized) {
172
- return summarized;
173
- }
174
- }
175
- const outputText = typeof state.output === 'string' ? state.output : '';
176
- const outputLines = outputText.split('\n');
177
- const updatedIndex = outputLines.findIndex((line) => line.startsWith('Success. Updated the following files:'));
178
- if (updatedIndex !== -1) {
179
- const fileLines = outputLines.slice(updatedIndex + 1).filter(Boolean);
180
- if (fileLines.length > 0) {
181
- const summarized = summarizeFiles(fileLines.map((line) => line.replace(/^[AMD]\s+/, '').trim()));
182
- if (summarized) {
183
- return summarized;
184
- }
185
- }
196
+ // Only inputs are available when parts are sent during streaming (output/metadata not yet populated)
197
+ const patchText = part.state.input?.patchText || '';
198
+ if (!patchText) {
199
+ return '';
186
200
  }
187
- return '';
201
+ const patchCounts = parsePatchCounts(patchText);
202
+ return [...patchCounts.entries()]
203
+ .map(([filePath, { additions, deletions }]) => {
204
+ const fileName = filePath.split('/').pop() || '';
205
+ return fileName
206
+ ? `*${escapeInlineMarkdown(fileName)}* (+${additions}-${deletions})`
207
+ : `(+${additions}-${deletions})`;
208
+ })
209
+ .join(', ');
188
210
  }
189
211
  if (part.tool === 'write') {
190
212
  const filePath = part.state.input?.filePath || '';
package/dist/opencode.js CHANGED
@@ -32,30 +32,24 @@ async function getOpenPort() {
32
32
  });
33
33
  }
34
34
  async function waitForServer(port, maxAttempts = 30) {
35
+ const endpoint = `http://127.0.0.1:${port}/api/health`;
35
36
  for (let i = 0; i < maxAttempts; i++) {
36
- const endpoints = [
37
- `http://127.0.0.1:${port}/api/health`,
38
- `http://127.0.0.1:${port}/`,
39
- `http://127.0.0.1:${port}/api`,
40
- ];
41
- for (const endpoint of endpoints) {
42
- const response = await errore.tryAsync({
43
- try: () => fetch(endpoint),
44
- catch: (e) => new FetchError({ url: endpoint, cause: e }),
45
- });
46
- if (response instanceof Error) {
47
- // Connection refused or other transient errors - continue polling
48
- opencodeLogger.debug(`Server polling attempt failed: ${response.message}`);
49
- continue;
50
- }
51
- if (response.status < 500) {
52
- return true;
53
- }
54
- const body = await response.text();
55
- // Fatal errors that won't resolve with retrying
56
- if (body.includes('BunInstallFailedError')) {
57
- return new ServerStartError({ port, reason: body.slice(0, 200) });
58
- }
37
+ const response = await errore.tryAsync({
38
+ try: () => fetch(endpoint),
39
+ catch: (e) => new FetchError({ url: endpoint, cause: e }),
40
+ });
41
+ if (response instanceof Error) {
42
+ // Connection refused or other transient errors - continue polling
43
+ await new Promise((resolve) => setTimeout(resolve, 1000));
44
+ continue;
45
+ }
46
+ if (response.status < 500) {
47
+ return true;
48
+ }
49
+ const body = await response.text();
50
+ // Fatal errors that won't resolve with retrying
51
+ if (body.includes('BunInstallFailedError')) {
52
+ return new ServerStartError({ port, reason: body.slice(0, 200) });
59
53
  }
60
54
  await new Promise((resolve) => setTimeout(resolve, 1000));
61
55
  }
@@ -56,6 +56,7 @@ export async function abortAndRetrySession({ sessionId, thread, projectDirectory
56
56
  }
57
57
  sessionLogger.log(`[ABORT+RETRY] Aborting session ${sessionId} for model change`);
58
58
  // Abort with special reason so we don't show "completed" message
59
+ sessionLogger.log(`[ABORT] reason=model-change sessionId=${sessionId} - user changed model mid-request, will retry with new model`);
59
60
  controller.abort(new Error('model-change'));
60
61
  // Also call the API abort endpoint
61
62
  const getClient = await initializeOpencodeForDirectory(projectDirectory);
@@ -63,11 +64,12 @@ export async function abortAndRetrySession({ sessionId, thread, projectDirectory
63
64
  sessionLogger.error(`[ABORT+RETRY] Failed to initialize OpenCode client:`, getClient.message);
64
65
  return false;
65
66
  }
67
+ sessionLogger.log(`[ABORT-API] reason=model-change sessionId=${sessionId} - sending API abort for model change retry`);
66
68
  const abortResult = await errore.tryAsync(() => {
67
69
  return getClient().session.abort({ path: { id: sessionId } });
68
70
  });
69
71
  if (abortResult instanceof Error) {
70
- sessionLogger.log(`[ABORT+RETRY] API abort call failed (may already be done):`, abortResult);
72
+ sessionLogger.log(`[ABORT-API] API abort call failed (may already be done):`, abortResult);
71
73
  }
72
74
  // Small delay to let the abort propagate
73
75
  await new Promise((resolve) => {
@@ -176,7 +178,9 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
176
178
  const existingController = abortControllers.get(session.id);
177
179
  if (existingController) {
178
180
  voiceLogger.log(`[ABORT] Cancelling existing request for session: ${session.id}`);
181
+ sessionLogger.log(`[ABORT] reason=new-request sessionId=${session.id} threadId=${thread.id} - new user message arrived while previous request was still running`);
179
182
  existingController.abort(new Error('New request started'));
183
+ sessionLogger.log(`[ABORT-API] reason=new-request sessionId=${session.id} - sending API abort because new message arrived`);
180
184
  const abortResult = await errore.tryAsync(() => {
181
185
  return getClient().session.abort({
182
186
  path: { id: session.id },
@@ -184,7 +188,7 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
184
188
  });
185
189
  });
186
190
  if (abortResult instanceof Error) {
187
- sessionLogger.log(`[ABORT] Server abort failed (may be already done):`, abortResult);
191
+ sessionLogger.log(`[ABORT-API] Server abort failed (may be already done):`, abortResult);
188
192
  }
189
193
  }
190
194
  // Auto-reject ALL pending permissions for this thread
@@ -277,6 +281,9 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
277
281
  let assistantMessageId;
278
282
  let handlerPromise = null;
279
283
  let typingInterval = null;
284
+ let hasSentParts = false;
285
+ let promptResolved = false;
286
+ let hasReceivedEvent = false;
280
287
  function startTyping() {
281
288
  if (abortController.signal.aborted) {
282
289
  discordLogger.log(`Not starting typing, already aborted`);
@@ -313,12 +320,14 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
313
320
  }
314
321
  };
315
322
  }
316
- // Get verbosity setting for this channel (use parent channel for threads)
323
+ // Read verbosity dynamically so mid-session /verbosity changes take effect immediately
317
324
  const verbosityChannelId = channelId || thread.parentId || thread.id;
318
- const verbosity = getChannelVerbosity(verbosityChannelId);
325
+ const getVerbosity = () => {
326
+ return getChannelVerbosity(verbosityChannelId);
327
+ };
319
328
  const sendPartMessage = async (part) => {
320
329
  // In text-only mode, only send text parts (the ⬥ diamond messages)
321
- if (verbosity === 'text-only' && part.type !== 'text') {
330
+ if (getVerbosity() === 'text-only' && part.type !== 'text') {
322
331
  return;
323
332
  }
324
333
  const content = formatPart(part) + '\n\n';
@@ -336,6 +345,7 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
336
345
  discordLogger.error(`ERROR: Failed to send part ${part.id}:`, sendResult);
337
346
  return;
338
347
  }
348
+ hasSentParts = true;
339
349
  sentPartIds.add(part.id);
340
350
  getDatabase()
341
351
  .prepare('INSERT OR REPLACE INTO part_messages (part_id, message_id, thread_id) VALUES (?, ?, ?)')
@@ -392,6 +402,7 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
392
402
  if (msg.sessionID !== session.id) {
393
403
  return;
394
404
  }
405
+ hasReceivedEvent = true;
395
406
  if (msg.role !== 'assistant') {
396
407
  return;
397
408
  }
@@ -477,7 +488,7 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
477
488
  const label = `${agent}-${agentSpawnCounts[agent]}`;
478
489
  subtaskSessions.set(childSessionId, { label, assistantMessageId: undefined });
479
490
  // Skip task messages in text-only mode
480
- if (verbosity !== 'text-only') {
491
+ if (getVerbosity() !== 'text-only') {
481
492
  const taskDisplay = `┣ task **${label}** _${description}_`;
482
493
  await sendThreadMessage(thread, taskDisplay + '\n\n');
483
494
  }
@@ -486,7 +497,7 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
486
497
  }
487
498
  return;
488
499
  }
489
- if (part.type === 'tool' && part.state.status === 'completed') {
500
+ if (part.type === 'tool' && part.state.status === 'completed' && getVerbosity() !== 'text-only') {
490
501
  const output = part.state.output || '';
491
502
  const outputTokens = Math.ceil(output.length / 4);
492
503
  const largeOutputThreshold = 3000;
@@ -532,7 +543,7 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
532
543
  };
533
544
  const handleSubtaskPart = async (part, subtaskInfo) => {
534
545
  // In text-only mode, skip all subtask output (they're tool-related)
535
- if (verbosity === 'text-only') {
546
+ if (getVerbosity() === 'text-only') {
536
547
  return;
537
548
  }
538
549
  if (part.type === 'step-start' || part.type === 'step-finish') {
@@ -599,10 +610,15 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
599
610
  }
600
611
  };
601
612
  const handlePermissionAsked = async (permission) => {
602
- if (permission.sessionID !== session.id) {
603
- voiceLogger.log(`[PERMISSION IGNORED] Permission for different session (expected: ${session.id}, got: ${permission.sessionID})`);
613
+ const isMainSession = permission.sessionID === session.id;
614
+ const isSubtaskSession = subtaskSessions.has(permission.sessionID);
615
+ if (!isMainSession && !isSubtaskSession) {
616
+ voiceLogger.log(`[PERMISSION IGNORED] Permission for unknown session (expected: ${session.id} or subtask, got: ${permission.sessionID})`);
604
617
  return;
605
618
  }
619
+ const subtaskLabel = isSubtaskSession
620
+ ? subtaskSessions.get(permission.sessionID)?.label
621
+ : undefined;
606
622
  const dedupeKey = buildPermissionDedupeKey({ permission, directory });
607
623
  const threadPermissions = pendingPermissions.get(thread.id);
608
624
  const existingPending = threadPermissions
@@ -635,7 +651,7 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
635
651
  }
636
652
  return;
637
653
  }
638
- sessionLogger.log(`Permission requested: permission=${permission.permission}, patterns=${permission.patterns.join(', ')}`);
654
+ sessionLogger.log(`Permission requested: permission=${permission.permission}, patterns=${permission.patterns.join(', ')}${subtaskLabel ? `, subtask=${subtaskLabel}` : ''}`);
639
655
  if (stopTyping) {
640
656
  stopTyping();
641
657
  stopTyping = null;
@@ -644,6 +660,7 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
644
660
  thread,
645
661
  permission,
646
662
  directory,
663
+ subtaskLabel,
647
664
  });
648
665
  if (!pendingPermissions.has(thread.id)) {
649
666
  pendingPermissions.set(thread.id, new Map());
@@ -657,7 +674,9 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
657
674
  });
658
675
  };
659
676
  const handlePermissionReplied = ({ requestID, reply, sessionID, }) => {
660
- if (sessionID !== session.id) {
677
+ const isMainSession = sessionID === session.id;
678
+ const isSubtaskSession = subtaskSessions.has(sessionID);
679
+ if (!isMainSession && !isSubtaskSession) {
661
680
  return;
662
681
  }
663
682
  sessionLogger.log(`Permission ${requestID} replied with: ${reply}`);
@@ -707,10 +726,11 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
707
726
  sessionLogger.log(`[QUEUE] Question shown but queue has messages, processing from ${nextMessage.username}`);
708
727
  await sendThreadMessage(thread, `» **${nextMessage.username}:** ${nextMessage.prompt.slice(0, 150)}${nextMessage.prompt.length > 150 ? '...' : ''}`);
709
728
  setImmediate(() => {
729
+ const prefixedPrompt = `<discord-user name="${nextMessage.username}" />\n${nextMessage.prompt}`;
710
730
  void errore
711
731
  .tryAsync(async () => {
712
732
  return handleOpencodeSession({
713
- prompt: nextMessage.prompt,
733
+ prompt: prefixedPrompt,
714
734
  thread,
715
735
  projectDirectory: directory,
716
736
  images: nextMessage.images,
@@ -755,14 +775,12 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
755
775
  };
756
776
  const handleSessionIdle = (idleSessionId) => {
757
777
  if (idleSessionId === session.id) {
758
- // Ignore stale session.idle events - if we haven't received any content yet
759
- // (no assistantMessageId set), this is likely a stale event from before
760
- // the prompt was sent or from a previous request's subscription state.
761
- if (!assistantMessageId) {
762
- sessionLogger.log(`[SESSION IDLE] Ignoring stale idle event for ${session.id} (no content received yet)`);
778
+ if (!promptResolved || !hasReceivedEvent) {
779
+ sessionLogger.log(`[SESSION IDLE] Ignoring idle event for ${session.id} (prompt not resolved or no events yet)`);
763
780
  return;
764
781
  }
765
- sessionLogger.log(`[SESSION IDLE] Session ${session.id} is idle, aborting`);
782
+ sessionLogger.log(`[SESSION IDLE] Session ${session.id} is idle, ending stream`);
783
+ sessionLogger.log(`[ABORT] reason=finished sessionId=${session.id} threadId=${thread.id} - session completed normally, received idle event after prompt resolved`);
766
784
  abortController.abort(new Error('finished'));
767
785
  return;
768
786
  }
@@ -884,8 +902,9 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
884
902
  // Send the queued message as a new prompt (recursive call)
885
903
  // Use setImmediate to avoid blocking and allow this finally to complete
886
904
  setImmediate(() => {
905
+ const prefixedPrompt = `<discord-user name="${nextMessage.username}" />\n${nextMessage.prompt}`;
887
906
  handleOpencodeSession({
888
- prompt: nextMessage.prompt,
907
+ prompt: prefixedPrompt,
889
908
  thread,
890
909
  projectDirectory,
891
910
  images: nextMessage.images,
@@ -960,6 +979,7 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
960
979
  mainRepoDirectory: worktreeInfo.project_directory,
961
980
  }
962
981
  : undefined;
982
+ hasSentParts = false;
963
983
  const response = command
964
984
  ? await getClient().session.command({
965
985
  path: { id: session.id },
@@ -997,7 +1017,7 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
997
1017
  })();
998
1018
  throw new Error(`OpenCode API error (${response.response.status}): ${errorMessage}`);
999
1019
  }
1000
- abortController.abort(new Error('finished'));
1020
+ promptResolved = true;
1001
1021
  sessionLogger.log(`Successfully sent prompt, got response`);
1002
1022
  if (originalMessage) {
1003
1023
  const reactionResult = await errore.tryAsync(async () => {
@@ -1026,6 +1046,7 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
1026
1046
  return;
1027
1047
  }
1028
1048
  sessionLogger.error(`ERROR: Failed to send prompt:`, promptError);
1049
+ sessionLogger.log(`[ABORT] reason=error sessionId=${session.id} threadId=${thread.id} - prompt failed with error: ${promptError.message}`);
1029
1050
  abortController.abort(new Error('error'));
1030
1051
  if (originalMessage) {
1031
1052
  const reactionResult = await errore.tryAsync(async () => {
@@ -111,19 +111,21 @@ headings are discouraged anyway. instead try to use bold text for titles which r
111
111
 
112
112
  you can create diagrams wrapping them in code blocks.
113
113
 
114
- ## ending conversations with options
114
+ ## proactivity
115
115
 
116
- IMPORTANT: At the end of each response, especially after completing a task or presenting a plan, use the question tool to offer the user clear options for what to do next.
116
+ Be proactive. When the user asks you to do something, do it. Do NOT stop to ask for confirmation.
117
117
 
118
- IMPORTANT: The question tool must be called last, after all text parts. If it is called before your final text response, the user will not see the text.
118
+ Only ask questions when the request is genuinely ambiguous with multiple valid approaches, or the action is destructive and irreversible.
119
119
 
120
- Examples:
121
- - After showing a plan: offer "Start implementing?" with Yes/No options
122
- - After completing edits: offer "Commit changes?" with Yes/No options
123
- - After debugging: offer "How to proceed?" with options like "Apply fix", "Investigate further", "Try different approach"
120
+ ## ending conversations with options
124
121
 
125
- The user can always select "Other" to type a custom response if the provided options don't fit their needs, or if the plan needs updating.
122
+ After **completing** a task, use the question tool to offer follow-up options. The question tool must be called last, after all text parts.
126
123
 
127
- This makes the interaction more guided and reduces friction for the user.
124
+ IMPORTANT: Do NOT use the question tool to ask permission before doing work. Do the work first, then offer follow-ups.
125
+
126
+ Examples:
127
+ - After completing edits: offer "Commit changes?" or "Run tests?"
128
+ - After debugging: offer "Apply fix", "Investigate further", "Try different approach"
129
+ - After a genuinely ambiguous request where you cannot infer intent: offer the different approaches
128
130
  `;
129
131
  }
package/dist/tools.js CHANGED
@@ -289,6 +289,7 @@ export async function getTools({ onMessageCompleted, directory, }) {
289
289
  }),
290
290
  execute: async ({ sessionId }) => {
291
291
  try {
292
+ toolsLogger.log(`[ABORT] reason=voice-tool sessionId=${sessionId} - user requested abort via voice assistant tool`);
292
293
  const result = await getClient().session.abort({
293
294
  path: { id: sessionId },
294
295
  });
package/dist/utils.js CHANGED
@@ -50,6 +50,7 @@ export function isAbortError(error, signal) {
50
50
  error.name === 'Aborterror' ||
51
51
  error.name === 'aborterror' ||
52
52
  error.name.toLowerCase() === 'aborterror' ||
53
+ error.name === 'MessageAbortedError' ||
53
54
  error.message?.includes('aborted') ||
54
55
  (signal?.aborted ?? false))) ||
55
56
  (error instanceof DOMException && error.name === 'AbortError'));
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "kimaki",
3
3
  "module": "index.ts",
4
4
  "type": "module",
5
- "version": "0.4.47",
5
+ "version": "0.4.48",
6
6
  "repository": "https://github.com/remorses/kimaki",
7
7
  "bin": "bin.js",
8
8
  "files": [
@@ -20,7 +20,7 @@
20
20
  "tsx": "^4.20.5"
21
21
  },
22
22
  "dependencies": {
23
- "@clack/prompts": "^0.11.0",
23
+ "@clack/prompts": "^1.0.0",
24
24
  "@discordjs/voice": "^0.19.0",
25
25
  "@google/genai": "^1.34.0",
26
26
  "@opencode-ai/sdk": "^1.1.31",
@@ -41,7 +41,7 @@
41
41
  "string-dedent": "^3.0.2",
42
42
  "undici": "^7.16.0",
43
43
  "zod": "^4.2.1",
44
- "errore": "^0.9.0"
44
+ "errore": "^0.10.0"
45
45
  },
46
46
  "optionalDependencies": {
47
47
  "@discordjs/opus": "^0.10.0",