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.
- package/dist/cli.js +42 -19
- package/dist/commands/abort.js +2 -0
- package/dist/commands/create-new-project.js +57 -27
- package/dist/commands/merge-worktree.js +21 -8
- package/dist/commands/permissions.js +3 -1
- package/dist/commands/session.js +4 -1
- package/dist/commands/verbosity.js +3 -3
- package/dist/config.js +7 -0
- package/dist/database.js +8 -5
- package/dist/discord-bot.js +27 -9
- package/dist/message-formatting.js +74 -52
- package/dist/opencode.js +17 -23
- package/dist/session-handler.js +42 -21
- package/dist/system-message.js +11 -9
- package/dist/tools.js +1 -0
- package/dist/utils.js +1 -0
- package/package.json +3 -3
- package/src/cli.ts +80 -19
- package/src/commands/abort.ts +2 -0
- package/src/commands/create-new-project.ts +84 -33
- package/src/commands/merge-worktree.ts +45 -8
- package/src/commands/permissions.ts +4 -0
- package/src/commands/session.ts +4 -1
- package/src/commands/verbosity.ts +3 -3
- package/src/config.ts +14 -0
- package/src/database.ts +11 -5
- package/src/discord-bot.ts +42 -9
- package/src/message-formatting.ts +81 -63
- package/src/opencode.ts +17 -24
- package/src/session-handler.ts +46 -21
- package/src/system-message.ts +11 -9
- package/src/tools.ts +1 -0
- package/src/utils.ts +1 -0
|
@@ -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
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
}
|
package/dist/session-handler.js
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
323
|
+
// Read verbosity dynamically so mid-session /verbosity changes take effect immediately
|
|
317
324
|
const verbosityChannelId = channelId || thread.parentId || thread.id;
|
|
318
|
-
const
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
603
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
759
|
-
|
|
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,
|
|
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:
|
|
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
|
-
|
|
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 () => {
|
package/dist/system-message.js
CHANGED
|
@@ -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
|
-
##
|
|
114
|
+
## proactivity
|
|
115
115
|
|
|
116
|
-
|
|
116
|
+
Be proactive. When the user asks you to do something, do it. Do NOT stop to ask for confirmation.
|
|
117
117
|
|
|
118
|
-
|
|
118
|
+
Only ask questions when the request is genuinely ambiguous with multiple valid approaches, or the action is destructive and irreversible.
|
|
119
119
|
|
|
120
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
44
|
+
"errore": "^0.10.0"
|
|
45
45
|
},
|
|
46
46
|
"optionalDependencies": {
|
|
47
47
|
"@discordjs/opus": "^0.10.0",
|