kimaki 0.4.42 → 0.4.43
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 +61 -16
- package/dist/session-handler.js +52 -25
- package/dist/system-message.js +19 -4
- package/dist/voice-handler.js +1 -0
- package/package.json +2 -2
- package/src/cli.ts +74 -19
- package/src/session-handler.ts +57 -29
- package/src/system-message.ts +19 -4
- package/src/voice-handler.ts +1 -0
package/dist/cli.js
CHANGED
|
@@ -1027,23 +1027,68 @@ cli
|
|
|
1027
1027
|
throw new Error(`Channel belongs to a different bot (expected: ${appId}, got: ${channelAppId})`);
|
|
1028
1028
|
}
|
|
1029
1029
|
s.message('Creating starter message...');
|
|
1030
|
-
//
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1030
|
+
// Discord has a 2000 character limit for messages.
|
|
1031
|
+
// If prompt exceeds this, send it as a file attachment instead.
|
|
1032
|
+
const DISCORD_MAX_LENGTH = 2000;
|
|
1033
|
+
let starterMessage;
|
|
1034
|
+
if (prompt.length > DISCORD_MAX_LENGTH) {
|
|
1035
|
+
// Send as file attachment with a short summary
|
|
1036
|
+
const preview = prompt.slice(0, 100).replace(/\n/g, ' ');
|
|
1037
|
+
const summaryContent = `📄 **Prompt attached as file** (${prompt.length} chars)\n\n> ${preview}...`;
|
|
1038
|
+
// Write prompt to a temp file
|
|
1039
|
+
const tmpDir = path.join(process.cwd(), 'tmp');
|
|
1040
|
+
if (!fs.existsSync(tmpDir)) {
|
|
1041
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
1042
|
+
}
|
|
1043
|
+
const tmpFile = path.join(tmpDir, `prompt-${Date.now()}.md`);
|
|
1044
|
+
fs.writeFileSync(tmpFile, prompt);
|
|
1045
|
+
try {
|
|
1046
|
+
// Create message with file attachment
|
|
1047
|
+
const formData = new FormData();
|
|
1048
|
+
formData.append('payload_json', JSON.stringify({
|
|
1049
|
+
content: summaryContent,
|
|
1050
|
+
attachments: [{ id: 0, filename: 'prompt.md' }],
|
|
1051
|
+
}));
|
|
1052
|
+
const buffer = fs.readFileSync(tmpFile);
|
|
1053
|
+
formData.append('files[0]', new Blob([buffer], { type: 'text/markdown' }), 'prompt.md');
|
|
1054
|
+
const starterMessageResponse = await fetch(`https://discord.com/api/v10/channels/${channelId}/messages`, {
|
|
1055
|
+
method: 'POST',
|
|
1056
|
+
headers: {
|
|
1057
|
+
Authorization: `Bot ${botToken}`,
|
|
1058
|
+
},
|
|
1059
|
+
body: formData,
|
|
1060
|
+
});
|
|
1061
|
+
if (!starterMessageResponse.ok) {
|
|
1062
|
+
const error = await starterMessageResponse.text();
|
|
1063
|
+
s.stop('Failed to create message');
|
|
1064
|
+
throw new Error(`Discord API error: ${starterMessageResponse.status} - ${error}`);
|
|
1065
|
+
}
|
|
1066
|
+
starterMessage = (await starterMessageResponse.json());
|
|
1067
|
+
}
|
|
1068
|
+
finally {
|
|
1069
|
+
// Clean up temp file
|
|
1070
|
+
fs.unlinkSync(tmpFile);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
else {
|
|
1074
|
+
// Normal case: send prompt inline
|
|
1075
|
+
const starterMessageResponse = await fetch(`https://discord.com/api/v10/channels/${channelId}/messages`, {
|
|
1076
|
+
method: 'POST',
|
|
1077
|
+
headers: {
|
|
1078
|
+
Authorization: `Bot ${botToken}`,
|
|
1079
|
+
'Content-Type': 'application/json',
|
|
1080
|
+
},
|
|
1081
|
+
body: JSON.stringify({
|
|
1082
|
+
content: prompt,
|
|
1083
|
+
}),
|
|
1084
|
+
});
|
|
1085
|
+
if (!starterMessageResponse.ok) {
|
|
1086
|
+
const error = await starterMessageResponse.text();
|
|
1087
|
+
s.stop('Failed to create message');
|
|
1088
|
+
throw new Error(`Discord API error: ${starterMessageResponse.status} - ${error}`);
|
|
1089
|
+
}
|
|
1090
|
+
starterMessage = (await starterMessageResponse.json());
|
|
1045
1091
|
}
|
|
1046
|
-
const starterMessage = (await starterMessageResponse.json());
|
|
1047
1092
|
s.message('Creating thread...');
|
|
1048
1093
|
// Create thread from the message
|
|
1049
1094
|
const threadName = name || (prompt.length > 80 ? prompt.slice(0, 77) + '...' : prompt);
|
package/dist/session-handler.js
CHANGED
|
@@ -16,6 +16,9 @@ const sessionLogger = createLogger('SESSION');
|
|
|
16
16
|
const voiceLogger = createLogger('VOICE');
|
|
17
17
|
const discordLogger = createLogger('DISCORD');
|
|
18
18
|
export const abortControllers = new Map();
|
|
19
|
+
// Track multiple pending permissions per thread (keyed by permission ID)
|
|
20
|
+
// OpenCode handles blocking/sequencing - we just need to track all pending permissions
|
|
21
|
+
// to avoid duplicates and properly clean up on auto-reject
|
|
19
22
|
export const pendingPermissions = new Map();
|
|
20
23
|
// Queue of messages waiting to be sent after current response finishes
|
|
21
24
|
// Key is threadId, value is array of queued messages
|
|
@@ -148,26 +151,32 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
148
151
|
voiceLogger.log(`[ABORT] Cancelling existing request for session: ${session.id}`);
|
|
149
152
|
existingController.abort(new Error('New request started'));
|
|
150
153
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
154
|
+
// Auto-reject ALL pending permissions for this thread
|
|
155
|
+
const threadPermissions = pendingPermissions.get(thread.id);
|
|
156
|
+
if (threadPermissions && threadPermissions.size > 0) {
|
|
157
|
+
const clientV2 = getOpencodeClientV2(directory);
|
|
158
|
+
let rejectedCount = 0;
|
|
159
|
+
for (const [permId, pendingPerm] of threadPermissions) {
|
|
160
|
+
try {
|
|
161
|
+
sessionLogger.log(`[PERMISSION] Auto-rejecting permission ${permId} due to new message`);
|
|
162
|
+
if (clientV2) {
|
|
163
|
+
await clientV2.permission.reply({
|
|
164
|
+
requestID: permId,
|
|
165
|
+
reply: 'reject',
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
cleanupPermissionContext(pendingPerm.contextHash);
|
|
169
|
+
rejectedCount++;
|
|
170
|
+
}
|
|
171
|
+
catch (e) {
|
|
172
|
+
sessionLogger.log(`[PERMISSION] Failed to auto-reject permission ${permId}:`, e);
|
|
173
|
+
cleanupPermissionContext(pendingPerm.contextHash);
|
|
161
174
|
}
|
|
162
|
-
// Clean up both the pending permission and its dropdown context
|
|
163
|
-
cleanupPermissionContext(pendingPerm.contextHash);
|
|
164
|
-
pendingPermissions.delete(thread.id);
|
|
165
|
-
await sendThreadMessage(thread, `⚠️ Previous permission request auto-rejected due to new message`);
|
|
166
175
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
176
|
+
pendingPermissions.delete(thread.id);
|
|
177
|
+
if (rejectedCount > 0) {
|
|
178
|
+
const plural = rejectedCount > 1 ? 's' : '';
|
|
179
|
+
await sendThreadMessage(thread, `⚠️ ${rejectedCount} pending permission request${plural} auto-rejected due to new message`);
|
|
171
180
|
}
|
|
172
181
|
}
|
|
173
182
|
// Cancel any pending question tool if user sends a new message (silently, no thread message)
|
|
@@ -382,7 +391,7 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
382
391
|
if (part.type === 'step-start') {
|
|
383
392
|
// Don't start typing if user needs to respond to a question or permission
|
|
384
393
|
const hasPendingQuestion = [...pendingQuestionContexts.values()].some((ctx) => ctx.thread.id === thread.id);
|
|
385
|
-
const hasPendingPermission = pendingPermissions.
|
|
394
|
+
const hasPendingPermission = (pendingPermissions.get(thread.id)?.size ?? 0) > 0;
|
|
386
395
|
if (!hasPendingQuestion && !hasPendingPermission) {
|
|
387
396
|
stopTyping = startTyping();
|
|
388
397
|
}
|
|
@@ -451,7 +460,7 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
451
460
|
return;
|
|
452
461
|
// Don't restart typing if user needs to respond to a question or permission
|
|
453
462
|
const hasPendingQuestion = [...pendingQuestionContexts.values()].some((ctx) => ctx.thread.id === thread.id);
|
|
454
|
-
const hasPendingPermission = pendingPermissions.
|
|
463
|
+
const hasPendingPermission = (pendingPermissions.get(thread.id)?.size ?? 0) > 0;
|
|
455
464
|
if (hasPendingQuestion || hasPendingPermission)
|
|
456
465
|
return;
|
|
457
466
|
stopTyping = startTyping();
|
|
@@ -487,6 +496,12 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
487
496
|
voiceLogger.log(`[PERMISSION IGNORED] Permission for different session (expected: ${session.id}, got: ${permission.sessionID})`);
|
|
488
497
|
continue;
|
|
489
498
|
}
|
|
499
|
+
// Skip if this exact permission ID is already pending (dedupe)
|
|
500
|
+
const threadPermissions = pendingPermissions.get(thread.id);
|
|
501
|
+
if (threadPermissions?.has(permission.id)) {
|
|
502
|
+
sessionLogger.log(`[PERMISSION] Skipping duplicate permission ${permission.id} (already pending)`);
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
490
505
|
sessionLogger.log(`Permission requested: permission=${permission.permission}, patterns=${permission.patterns.join(', ')}`);
|
|
491
506
|
// Stop typing - user needs to respond now, not the bot
|
|
492
507
|
if (stopTyping) {
|
|
@@ -499,7 +514,11 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
499
514
|
permission,
|
|
500
515
|
directory,
|
|
501
516
|
});
|
|
502
|
-
|
|
517
|
+
// Track permission in nested map (threadId -> permissionId -> data)
|
|
518
|
+
if (!pendingPermissions.has(thread.id)) {
|
|
519
|
+
pendingPermissions.set(thread.id, new Map());
|
|
520
|
+
}
|
|
521
|
+
pendingPermissions.get(thread.id).set(permission.id, {
|
|
503
522
|
permission,
|
|
504
523
|
messageId,
|
|
505
524
|
directory,
|
|
@@ -512,10 +531,18 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
512
531
|
continue;
|
|
513
532
|
}
|
|
514
533
|
sessionLogger.log(`Permission ${requestID} replied with: ${reply}`);
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
534
|
+
// Clean up the specific permission from nested map
|
|
535
|
+
const threadPermissions = pendingPermissions.get(thread.id);
|
|
536
|
+
if (threadPermissions) {
|
|
537
|
+
const pending = threadPermissions.get(requestID);
|
|
538
|
+
if (pending) {
|
|
539
|
+
cleanupPermissionContext(pending.contextHash);
|
|
540
|
+
threadPermissions.delete(requestID);
|
|
541
|
+
// Remove thread entry if no more pending permissions
|
|
542
|
+
if (threadPermissions.size === 0) {
|
|
543
|
+
pendingPermissions.delete(thread.id);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
519
546
|
}
|
|
520
547
|
}
|
|
521
548
|
else if (event.type === 'question.asked') {
|
package/dist/system-message.js
CHANGED
|
@@ -35,6 +35,21 @@ Use --notify-only to create a notification thread without starting an AI session
|
|
|
35
35
|
npx -y kimaki send --channel ${channelId} --prompt "User cancelled subscription" --notify-only
|
|
36
36
|
|
|
37
37
|
This is useful for automation (cron jobs, GitHub webhooks, n8n, etc.)
|
|
38
|
+
|
|
39
|
+
### Session handoff
|
|
40
|
+
|
|
41
|
+
When you are approaching the **context window limit** or the user explicitly asks to **handoff to a new thread**, use the \`kimaki send\` command to start a fresh session with context:
|
|
42
|
+
|
|
43
|
+
\`\`\`bash
|
|
44
|
+
npx -y kimaki send --channel ${channelId} --prompt "Continuing from previous session: <summary of current task and state>"
|
|
45
|
+
\`\`\`
|
|
46
|
+
|
|
47
|
+
The command automatically handles long prompts (over 2000 chars) by sending them as file attachments.
|
|
48
|
+
|
|
49
|
+
Use this for handoff when:
|
|
50
|
+
- User asks to "handoff", "continue in new thread", or "start fresh session"
|
|
51
|
+
- You detect you're running low on context window space
|
|
52
|
+
- A complex task would benefit from a clean slate with summarized context
|
|
38
53
|
`
|
|
39
54
|
: ''}
|
|
40
55
|
## showing diffs
|
|
@@ -43,17 +58,17 @@ IMPORTANT: After editing any files, you MUST execute the critique command using
|
|
|
43
58
|
|
|
44
59
|
Execute this after making changes:
|
|
45
60
|
|
|
46
|
-
bunx critique web
|
|
61
|
+
bunx critique --web "Add user authentication flow"
|
|
47
62
|
|
|
48
63
|
If there are other unrelated changes in the working directory, filter to only show the files you edited:
|
|
49
64
|
|
|
50
|
-
bunx critique web
|
|
65
|
+
bunx critique --web "Fix database connection retry" --filter "path/to/file1.ts" --filter "path/to/file2.ts"
|
|
51
66
|
|
|
52
67
|
You can also show latest commit changes using:
|
|
53
68
|
|
|
54
|
-
bunx critique
|
|
69
|
+
bunx critique HEAD --web "Refactor API endpoints"
|
|
55
70
|
|
|
56
|
-
bunx critique
|
|
71
|
+
bunx critique HEAD~1 --web "Update dependencies"
|
|
57
72
|
|
|
58
73
|
Do this in case you committed the changes yourself (only if the user asks so, never commit otherwise).
|
|
59
74
|
|
package/dist/voice-handler.js
CHANGED
|
@@ -375,6 +375,7 @@ export async function processVoiceAttachment({ message, thread, projectDirectory
|
|
|
375
375
|
EmptyTranscriptionError: (e) => e.message,
|
|
376
376
|
NoResponseContentError: (e) => e.message,
|
|
377
377
|
NoToolResponseError: (e) => e.message,
|
|
378
|
+
Error: (e) => e.message,
|
|
378
379
|
});
|
|
379
380
|
voiceLogger.error(`Transcription failed:`, transcription);
|
|
380
381
|
await sendThreadMessage(thread, `⚠️ Transcription failed: ${errMsg}`);
|
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.43",
|
|
6
6
|
"repository": "https://github.com/remorses/kimaki",
|
|
7
7
|
"bin": "bin.js",
|
|
8
8
|
"files": [
|
|
@@ -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.9.0"
|
|
45
45
|
},
|
|
46
46
|
"optionalDependencies": {
|
|
47
47
|
"@discordjs/opus": "^0.10.0",
|
package/src/cli.ts
CHANGED
|
@@ -1381,28 +1381,83 @@ cli
|
|
|
1381
1381
|
|
|
1382
1382
|
s.message('Creating starter message...')
|
|
1383
1383
|
|
|
1384
|
-
//
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1384
|
+
// Discord has a 2000 character limit for messages.
|
|
1385
|
+
// If prompt exceeds this, send it as a file attachment instead.
|
|
1386
|
+
const DISCORD_MAX_LENGTH = 2000
|
|
1387
|
+
let starterMessage: { id: string }
|
|
1388
|
+
|
|
1389
|
+
if (prompt.length > DISCORD_MAX_LENGTH) {
|
|
1390
|
+
// Send as file attachment with a short summary
|
|
1391
|
+
const preview = prompt.slice(0, 100).replace(/\n/g, ' ')
|
|
1392
|
+
const summaryContent = `📄 **Prompt attached as file** (${prompt.length} chars)\n\n> ${preview}...`
|
|
1393
|
+
|
|
1394
|
+
// Write prompt to a temp file
|
|
1395
|
+
const tmpDir = path.join(process.cwd(), 'tmp')
|
|
1396
|
+
if (!fs.existsSync(tmpDir)) {
|
|
1397
|
+
fs.mkdirSync(tmpDir, { recursive: true })
|
|
1398
|
+
}
|
|
1399
|
+
const tmpFile = path.join(tmpDir, `prompt-${Date.now()}.md`)
|
|
1400
|
+
fs.writeFileSync(tmpFile, prompt)
|
|
1401
|
+
|
|
1402
|
+
try {
|
|
1403
|
+
// Create message with file attachment
|
|
1404
|
+
const formData = new FormData()
|
|
1405
|
+
formData.append(
|
|
1406
|
+
'payload_json',
|
|
1407
|
+
JSON.stringify({
|
|
1408
|
+
content: summaryContent,
|
|
1409
|
+
attachments: [{ id: 0, filename: 'prompt.md' }],
|
|
1410
|
+
}),
|
|
1411
|
+
)
|
|
1412
|
+
const buffer = fs.readFileSync(tmpFile)
|
|
1413
|
+
formData.append('files[0]', new Blob([buffer], { type: 'text/markdown' }), 'prompt.md')
|
|
1414
|
+
|
|
1415
|
+
const starterMessageResponse = await fetch(
|
|
1416
|
+
`https://discord.com/api/v10/channels/${channelId}/messages`,
|
|
1417
|
+
{
|
|
1418
|
+
method: 'POST',
|
|
1419
|
+
headers: {
|
|
1420
|
+
Authorization: `Bot ${botToken}`,
|
|
1421
|
+
},
|
|
1422
|
+
body: formData,
|
|
1423
|
+
},
|
|
1424
|
+
)
|
|
1425
|
+
|
|
1426
|
+
if (!starterMessageResponse.ok) {
|
|
1427
|
+
const error = await starterMessageResponse.text()
|
|
1428
|
+
s.stop('Failed to create message')
|
|
1429
|
+
throw new Error(`Discord API error: ${starterMessageResponse.status} - ${error}`)
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
starterMessage = (await starterMessageResponse.json()) as { id: string }
|
|
1433
|
+
} finally {
|
|
1434
|
+
// Clean up temp file
|
|
1435
|
+
fs.unlinkSync(tmpFile)
|
|
1436
|
+
}
|
|
1437
|
+
} else {
|
|
1438
|
+
// Normal case: send prompt inline
|
|
1439
|
+
const starterMessageResponse = await fetch(
|
|
1440
|
+
`https://discord.com/api/v10/channels/${channelId}/messages`,
|
|
1441
|
+
{
|
|
1442
|
+
method: 'POST',
|
|
1443
|
+
headers: {
|
|
1444
|
+
Authorization: `Bot ${botToken}`,
|
|
1445
|
+
'Content-Type': 'application/json',
|
|
1446
|
+
},
|
|
1447
|
+
body: JSON.stringify({
|
|
1448
|
+
content: prompt,
|
|
1449
|
+
}),
|
|
1392
1450
|
},
|
|
1393
|
-
|
|
1394
|
-
content: prompt,
|
|
1395
|
-
}),
|
|
1396
|
-
},
|
|
1397
|
-
)
|
|
1451
|
+
)
|
|
1398
1452
|
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1453
|
+
if (!starterMessageResponse.ok) {
|
|
1454
|
+
const error = await starterMessageResponse.text()
|
|
1455
|
+
s.stop('Failed to create message')
|
|
1456
|
+
throw new Error(`Discord API error: ${starterMessageResponse.status} - ${error}`)
|
|
1457
|
+
}
|
|
1404
1458
|
|
|
1405
|
-
|
|
1459
|
+
starterMessage = (await starterMessageResponse.json()) as { id: string }
|
|
1460
|
+
}
|
|
1406
1461
|
|
|
1407
1462
|
s.message('Creating thread...')
|
|
1408
1463
|
|
package/src/session-handler.ts
CHANGED
|
@@ -38,9 +38,12 @@ const discordLogger = createLogger('DISCORD')
|
|
|
38
38
|
|
|
39
39
|
export const abortControllers = new Map<string, AbortController>()
|
|
40
40
|
|
|
41
|
+
// Track multiple pending permissions per thread (keyed by permission ID)
|
|
42
|
+
// OpenCode handles blocking/sequencing - we just need to track all pending permissions
|
|
43
|
+
// to avoid duplicates and properly clean up on auto-reject
|
|
41
44
|
export const pendingPermissions = new Map<
|
|
42
|
-
string,
|
|
43
|
-
{ permission: PermissionRequest; messageId: string; directory: string; contextHash: string }
|
|
45
|
+
string, // threadId
|
|
46
|
+
Map<string, { permission: PermissionRequest; messageId: string; directory: string; contextHash: string }> // permissionId -> data
|
|
44
47
|
>()
|
|
45
48
|
|
|
46
49
|
export type QueuedMessage = {
|
|
@@ -246,30 +249,34 @@ export async function handleOpencodeSession({
|
|
|
246
249
|
existingController.abort(new Error('New request started'))
|
|
247
250
|
}
|
|
248
251
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
252
|
+
// Auto-reject ALL pending permissions for this thread
|
|
253
|
+
const threadPermissions = pendingPermissions.get(thread.id)
|
|
254
|
+
if (threadPermissions && threadPermissions.size > 0) {
|
|
255
|
+
const clientV2 = getOpencodeClientV2(directory)
|
|
256
|
+
let rejectedCount = 0
|
|
257
|
+
for (const [permId, pendingPerm] of threadPermissions) {
|
|
258
|
+
try {
|
|
259
|
+
sessionLogger.log(`[PERMISSION] Auto-rejecting permission ${permId} due to new message`)
|
|
260
|
+
if (clientV2) {
|
|
261
|
+
await clientV2.permission.reply({
|
|
262
|
+
requestID: permId,
|
|
263
|
+
reply: 'reject',
|
|
264
|
+
})
|
|
265
|
+
}
|
|
266
|
+
cleanupPermissionContext(pendingPerm.contextHash)
|
|
267
|
+
rejectedCount++
|
|
268
|
+
} catch (e) {
|
|
269
|
+
sessionLogger.log(`[PERMISSION] Failed to auto-reject permission ${permId}:`, e)
|
|
270
|
+
cleanupPermissionContext(pendingPerm.contextHash)
|
|
261
271
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
272
|
+
}
|
|
273
|
+
pendingPermissions.delete(thread.id)
|
|
274
|
+
if (rejectedCount > 0) {
|
|
275
|
+
const plural = rejectedCount > 1 ? 's' : ''
|
|
265
276
|
await sendThreadMessage(
|
|
266
277
|
thread,
|
|
267
|
-
`⚠️
|
|
278
|
+
`⚠️ ${rejectedCount} pending permission request${plural} auto-rejected due to new message`,
|
|
268
279
|
)
|
|
269
|
-
} catch (e) {
|
|
270
|
-
sessionLogger.log(`[PERMISSION] Failed to auto-reject permission:`, e)
|
|
271
|
-
cleanupPermissionContext(pendingPerm.contextHash)
|
|
272
|
-
pendingPermissions.delete(thread.id)
|
|
273
280
|
}
|
|
274
281
|
}
|
|
275
282
|
|
|
@@ -536,7 +543,7 @@ export async function handleOpencodeSession({
|
|
|
536
543
|
const hasPendingQuestion = [...pendingQuestionContexts.values()].some(
|
|
537
544
|
(ctx) => ctx.thread.id === thread.id,
|
|
538
545
|
)
|
|
539
|
-
const hasPendingPermission = pendingPermissions.
|
|
546
|
+
const hasPendingPermission = (pendingPermissions.get(thread.id)?.size ?? 0) > 0
|
|
540
547
|
if (!hasPendingQuestion && !hasPendingPermission) {
|
|
541
548
|
stopTyping = startTyping()
|
|
542
549
|
}
|
|
@@ -612,7 +619,7 @@ export async function handleOpencodeSession({
|
|
|
612
619
|
const hasPendingQuestion = [...pendingQuestionContexts.values()].some(
|
|
613
620
|
(ctx) => ctx.thread.id === thread.id,
|
|
614
621
|
)
|
|
615
|
-
const hasPendingPermission = pendingPermissions.
|
|
622
|
+
const hasPendingPermission = (pendingPermissions.get(thread.id)?.size ?? 0) > 0
|
|
616
623
|
if (hasPendingQuestion || hasPendingPermission) return
|
|
617
624
|
stopTyping = startTyping()
|
|
618
625
|
}, 300)
|
|
@@ -650,6 +657,15 @@ export async function handleOpencodeSession({
|
|
|
650
657
|
continue
|
|
651
658
|
}
|
|
652
659
|
|
|
660
|
+
// Skip if this exact permission ID is already pending (dedupe)
|
|
661
|
+
const threadPermissions = pendingPermissions.get(thread.id)
|
|
662
|
+
if (threadPermissions?.has(permission.id)) {
|
|
663
|
+
sessionLogger.log(
|
|
664
|
+
`[PERMISSION] Skipping duplicate permission ${permission.id} (already pending)`,
|
|
665
|
+
)
|
|
666
|
+
continue
|
|
667
|
+
}
|
|
668
|
+
|
|
653
669
|
sessionLogger.log(
|
|
654
670
|
`Permission requested: permission=${permission.permission}, patterns=${permission.patterns.join(', ')}`,
|
|
655
671
|
)
|
|
@@ -667,7 +683,11 @@ export async function handleOpencodeSession({
|
|
|
667
683
|
directory,
|
|
668
684
|
})
|
|
669
685
|
|
|
670
|
-
|
|
686
|
+
// Track permission in nested map (threadId -> permissionId -> data)
|
|
687
|
+
if (!pendingPermissions.has(thread.id)) {
|
|
688
|
+
pendingPermissions.set(thread.id, new Map())
|
|
689
|
+
}
|
|
690
|
+
pendingPermissions.get(thread.id)!.set(permission.id, {
|
|
671
691
|
permission,
|
|
672
692
|
messageId,
|
|
673
693
|
directory,
|
|
@@ -681,10 +701,18 @@ export async function handleOpencodeSession({
|
|
|
681
701
|
|
|
682
702
|
sessionLogger.log(`Permission ${requestID} replied with: ${reply}`)
|
|
683
703
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
704
|
+
// Clean up the specific permission from nested map
|
|
705
|
+
const threadPermissions = pendingPermissions.get(thread.id)
|
|
706
|
+
if (threadPermissions) {
|
|
707
|
+
const pending = threadPermissions.get(requestID)
|
|
708
|
+
if (pending) {
|
|
709
|
+
cleanupPermissionContext(pending.contextHash)
|
|
710
|
+
threadPermissions.delete(requestID)
|
|
711
|
+
// Remove thread entry if no more pending permissions
|
|
712
|
+
if (threadPermissions.size === 0) {
|
|
713
|
+
pendingPermissions.delete(thread.id)
|
|
714
|
+
}
|
|
715
|
+
}
|
|
688
716
|
}
|
|
689
717
|
} else if (event.type === 'question.asked') {
|
|
690
718
|
const questionRequest = event.properties
|
package/src/system-message.ts
CHANGED
|
@@ -43,6 +43,21 @@ Use --notify-only to create a notification thread without starting an AI session
|
|
|
43
43
|
npx -y kimaki send --channel ${channelId} --prompt "User cancelled subscription" --notify-only
|
|
44
44
|
|
|
45
45
|
This is useful for automation (cron jobs, GitHub webhooks, n8n, etc.)
|
|
46
|
+
|
|
47
|
+
### Session handoff
|
|
48
|
+
|
|
49
|
+
When you are approaching the **context window limit** or the user explicitly asks to **handoff to a new thread**, use the \`kimaki send\` command to start a fresh session with context:
|
|
50
|
+
|
|
51
|
+
\`\`\`bash
|
|
52
|
+
npx -y kimaki send --channel ${channelId} --prompt "Continuing from previous session: <summary of current task and state>"
|
|
53
|
+
\`\`\`
|
|
54
|
+
|
|
55
|
+
The command automatically handles long prompts (over 2000 chars) by sending them as file attachments.
|
|
56
|
+
|
|
57
|
+
Use this for handoff when:
|
|
58
|
+
- User asks to "handoff", "continue in new thread", or "start fresh session"
|
|
59
|
+
- You detect you're running low on context window space
|
|
60
|
+
- A complex task would benefit from a clean slate with summarized context
|
|
46
61
|
`
|
|
47
62
|
: ''
|
|
48
63
|
}
|
|
@@ -52,17 +67,17 @@ IMPORTANT: After editing any files, you MUST execute the critique command using
|
|
|
52
67
|
|
|
53
68
|
Execute this after making changes:
|
|
54
69
|
|
|
55
|
-
bunx critique web
|
|
70
|
+
bunx critique --web "Add user authentication flow"
|
|
56
71
|
|
|
57
72
|
If there are other unrelated changes in the working directory, filter to only show the files you edited:
|
|
58
73
|
|
|
59
|
-
bunx critique web
|
|
74
|
+
bunx critique --web "Fix database connection retry" --filter "path/to/file1.ts" --filter "path/to/file2.ts"
|
|
60
75
|
|
|
61
76
|
You can also show latest commit changes using:
|
|
62
77
|
|
|
63
|
-
bunx critique
|
|
78
|
+
bunx critique HEAD --web "Refactor API endpoints"
|
|
64
79
|
|
|
65
|
-
bunx critique
|
|
80
|
+
bunx critique HEAD~1 --web "Update dependencies"
|
|
66
81
|
|
|
67
82
|
Do this in case you committed the changes yourself (only if the user asks so, never commit otherwise).
|
|
68
83
|
|
package/src/voice-handler.ts
CHANGED
|
@@ -506,6 +506,7 @@ export async function processVoiceAttachment({
|
|
|
506
506
|
EmptyTranscriptionError: (e) => e.message,
|
|
507
507
|
NoResponseContentError: (e) => e.message,
|
|
508
508
|
NoToolResponseError: (e) => e.message,
|
|
509
|
+
Error: (e) => e.message,
|
|
509
510
|
})
|
|
510
511
|
voiceLogger.error(`Transcription failed:`, transcription)
|
|
511
512
|
await sendThreadMessage(thread, `⚠️ Transcription failed: ${errMsg}`)
|