kimaki 0.4.46 → 0.4.47
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 +27 -2
- package/dist/commands/abort.js +2 -2
- package/dist/commands/add-project.js +2 -2
- package/dist/commands/agent.js +4 -4
- package/dist/commands/ask-question.js +9 -8
- package/dist/commands/compact.js +126 -0
- package/dist/commands/create-new-project.js +3 -3
- package/dist/commands/fork.js +3 -3
- package/dist/commands/merge-worktree.js +2 -2
- package/dist/commands/model.js +5 -5
- package/dist/commands/permissions.js +2 -2
- package/dist/commands/queue.js +2 -2
- package/dist/commands/remove-project.js +2 -2
- package/dist/commands/resume.js +2 -2
- package/dist/commands/session.js +2 -2
- package/dist/commands/share.js +2 -2
- package/dist/commands/undo-redo.js +2 -2
- package/dist/commands/user-command.js +2 -2
- package/dist/commands/verbosity.js +3 -3
- package/dist/commands/worktree-settings.js +2 -2
- package/dist/commands/worktree.js +18 -8
- package/dist/database.js +2 -2
- package/dist/discord-bot.js +3 -3
- package/dist/discord-utils.js +2 -2
- package/dist/genai-worker-wrapper.js +3 -3
- package/dist/genai-worker.js +2 -2
- package/dist/genai.js +2 -2
- package/dist/interaction-handler.js +6 -2
- package/dist/logger.js +57 -9
- package/dist/markdown.js +2 -2
- package/dist/message-formatting.js +69 -6
- package/dist/openai-realtime.js +2 -2
- package/dist/opencode.js +2 -2
- package/dist/session-handler.js +53 -14
- package/dist/tools.js +2 -2
- package/dist/voice-handler.js +2 -2
- package/dist/voice.js +2 -2
- package/dist/worktree-utils.js +91 -7
- package/dist/xml.js +2 -2
- package/package.json +1 -1
- package/src/cli.ts +28 -2
- package/src/commands/abort.ts +2 -2
- package/src/commands/add-project.ts +2 -2
- package/src/commands/agent.ts +4 -4
- package/src/commands/ask-question.ts +9 -8
- package/src/commands/compact.ts +148 -0
- package/src/commands/create-new-project.ts +3 -3
- package/src/commands/fork.ts +3 -3
- package/src/commands/merge-worktree.ts +2 -2
- package/src/commands/model.ts +5 -5
- package/src/commands/permissions.ts +2 -2
- package/src/commands/queue.ts +2 -2
- package/src/commands/remove-project.ts +2 -2
- package/src/commands/resume.ts +2 -2
- package/src/commands/session.ts +2 -2
- package/src/commands/share.ts +2 -2
- package/src/commands/undo-redo.ts +2 -2
- package/src/commands/user-command.ts +2 -2
- package/src/commands/verbosity.ts +3 -3
- package/src/commands/worktree-settings.ts +2 -2
- package/src/commands/worktree.ts +20 -7
- package/src/database.ts +2 -2
- package/src/discord-bot.ts +3 -3
- package/src/discord-utils.ts +2 -2
- package/src/genai-worker-wrapper.ts +3 -3
- package/src/genai-worker.ts +2 -2
- package/src/genai.ts +2 -2
- package/src/interaction-handler.ts +7 -2
- package/src/logger.ts +64 -10
- package/src/markdown.ts +2 -2
- package/src/message-formatting.ts +82 -6
- package/src/openai-realtime.ts +2 -2
- package/src/opencode.ts +2 -2
- package/src/session-handler.ts +62 -14
- package/src/tools.ts +2 -2
- package/src/voice-handler.ts +2 -2
- package/src/voice.ts +2 -2
- package/src/worktree-utils.ts +111 -7
- package/src/xml.ts +2 -2
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// Spawns and manages the worker thread, handling message passing for
|
|
3
3
|
// audio input/output, tool call completions, and graceful shutdown.
|
|
4
4
|
import { Worker } from 'node:worker_threads';
|
|
5
|
-
import { createLogger } from './logger.js';
|
|
6
|
-
const genaiWorkerLogger = createLogger(
|
|
7
|
-
const genaiWrapperLogger = createLogger(
|
|
5
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
6
|
+
const genaiWorkerLogger = createLogger(LogPrefix.GENAI_WORKER);
|
|
7
|
+
const genaiWrapperLogger = createLogger(LogPrefix.GENAI_WORKER);
|
|
8
8
|
export function createGenAIWorker(options) {
|
|
9
9
|
return new Promise((resolve, reject) => {
|
|
10
10
|
const worker = new Worker(new URL('../dist/genai-worker.js', import.meta.url));
|
package/dist/genai-worker.js
CHANGED
|
@@ -10,11 +10,11 @@ import * as prism from 'prism-media';
|
|
|
10
10
|
import { startGenAiSession } from './genai.js';
|
|
11
11
|
import { getTools } from './tools.js';
|
|
12
12
|
import { mkdir } from 'node:fs/promises';
|
|
13
|
-
import { createLogger } from './logger.js';
|
|
13
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
14
14
|
if (!parentPort) {
|
|
15
15
|
throw new Error('This module must be run as a worker thread');
|
|
16
16
|
}
|
|
17
|
-
const workerLogger = createLogger(
|
|
17
|
+
const workerLogger = createLogger(`${LogPrefix.WORKER}_${threadId}`);
|
|
18
18
|
workerLogger.log('GenAI worker started');
|
|
19
19
|
// Define sendError early so it can be used by global handlers
|
|
20
20
|
function sendError(error) {
|
package/dist/genai.js
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
// and manages the assistant's audio output for Discord voice channels.
|
|
4
4
|
import { GoogleGenAI, LiveServerMessage, MediaResolution, Modality, Session } from '@google/genai';
|
|
5
5
|
import { writeFile } from 'fs';
|
|
6
|
-
import { createLogger } from './logger.js';
|
|
6
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
7
7
|
import { aiToolToCallableTool } from './ai-tool-to-genai.js';
|
|
8
|
-
const genaiLogger = createLogger(
|
|
8
|
+
const genaiLogger = createLogger(LogPrefix.GENAI);
|
|
9
9
|
const audioParts = [];
|
|
10
10
|
function saveBinaryFile(fileName, content) {
|
|
11
11
|
writeFile(fileName, content, 'utf8', (err) => {
|
|
@@ -12,6 +12,7 @@ import { handleRemoveProjectCommand, handleRemoveProjectAutocomplete, } from './
|
|
|
12
12
|
import { handleCreateNewProjectCommand } from './commands/create-new-project.js';
|
|
13
13
|
import { handlePermissionSelectMenu } from './commands/permissions.js';
|
|
14
14
|
import { handleAbortCommand } from './commands/abort.js';
|
|
15
|
+
import { handleCompactCommand } from './commands/compact.js';
|
|
15
16
|
import { handleShareCommand } from './commands/share.js';
|
|
16
17
|
import { handleForkCommand, handleForkSelectMenu } from './commands/fork.js';
|
|
17
18
|
import { handleModelCommand, handleProviderSelectMenu, handleModelSelectMenu, } from './commands/model.js';
|
|
@@ -21,8 +22,8 @@ import { handleQueueCommand, handleClearQueueCommand } from './commands/queue.js
|
|
|
21
22
|
import { handleUndoCommand, handleRedoCommand } from './commands/undo-redo.js';
|
|
22
23
|
import { handleUserCommand } from './commands/user-command.js';
|
|
23
24
|
import { handleVerbosityCommand } from './commands/verbosity.js';
|
|
24
|
-
import { createLogger } from './logger.js';
|
|
25
|
-
const interactionLogger = createLogger(
|
|
25
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
26
|
+
const interactionLogger = createLogger(LogPrefix.INTERACTION);
|
|
26
27
|
export function registerInteractionHandler({ discordClient, appId, }) {
|
|
27
28
|
interactionLogger.log('[REGISTER] Interaction handler registered');
|
|
28
29
|
discordClient.on(Events.InteractionCreate, async (interaction) => {
|
|
@@ -85,6 +86,9 @@ export function registerInteractionHandler({ discordClient, appId, }) {
|
|
|
85
86
|
case 'stop':
|
|
86
87
|
await handleAbortCommand({ command: interaction, appId });
|
|
87
88
|
return;
|
|
89
|
+
case 'compact':
|
|
90
|
+
await handleCompactCommand({ command: interaction, appId });
|
|
91
|
+
return;
|
|
88
92
|
case 'share':
|
|
89
93
|
await handleShareCommand({ command: interaction, appId });
|
|
90
94
|
return;
|
package/dist/logger.js
CHANGED
|
@@ -1,11 +1,49 @@
|
|
|
1
|
-
// Prefixed logging utility
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
import { log } from '@clack/prompts';
|
|
1
|
+
// Prefixed logging utility.
|
|
2
|
+
// Uses picocolors for compact frequent logs (log, info, debug).
|
|
3
|
+
// Uses @clack/prompts only for important events (warn, error) with visual distinction.
|
|
4
|
+
import { log as clackLog } from '@clack/prompts';
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path, { dirname } from 'node:path';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import util from 'node:util';
|
|
9
|
+
import pc from 'picocolors';
|
|
10
|
+
// All known log prefixes - add new ones here to keep alignment consistent
|
|
11
|
+
export const LogPrefix = {
|
|
12
|
+
ABORT: 'ABORT',
|
|
13
|
+
ADD_PROJECT: 'ADD_PROJ',
|
|
14
|
+
AGENT: 'AGENT',
|
|
15
|
+
ASK_QUESTION: 'QUESTION',
|
|
16
|
+
CLI: 'CLI',
|
|
17
|
+
COMPACT: 'COMPACT',
|
|
18
|
+
CREATE_PROJECT: 'NEW_PROJ',
|
|
19
|
+
DB: 'DB',
|
|
20
|
+
DISCORD: 'DISCORD',
|
|
21
|
+
FORK: 'FORK',
|
|
22
|
+
FORMATTING: 'FORMAT',
|
|
23
|
+
GENAI: 'GENAI',
|
|
24
|
+
GENAI_WORKER: 'GENAI_W',
|
|
25
|
+
INTERACTION: 'INTERACT',
|
|
26
|
+
MARKDOWN: 'MARKDOWN',
|
|
27
|
+
MODEL: 'MODEL',
|
|
28
|
+
OPENAI: 'OPENAI',
|
|
29
|
+
OPENCODE: 'OPENCODE',
|
|
30
|
+
PERMISSIONS: 'PERMS',
|
|
31
|
+
QUEUE: 'QUEUE',
|
|
32
|
+
REMOVE_PROJECT: 'RM_PROJ',
|
|
33
|
+
RESUME: 'RESUME',
|
|
34
|
+
SESSION: 'SESSION',
|
|
35
|
+
SHARE: 'SHARE',
|
|
36
|
+
TOOLS: 'TOOLS',
|
|
37
|
+
UNDO_REDO: 'UNDO',
|
|
38
|
+
USER_CMD: 'USER_CMD',
|
|
39
|
+
VERBOSITY: 'VERBOSE',
|
|
40
|
+
VOICE: 'VOICE',
|
|
41
|
+
WORKER: 'WORKER',
|
|
42
|
+
WORKTREE: 'WORKTREE',
|
|
43
|
+
XML: 'XML',
|
|
44
|
+
};
|
|
45
|
+
// compute max length from all known prefixes for alignment
|
|
46
|
+
const MAX_PREFIX_LENGTH = Math.max(...Object.values(LogPrefix).map((p) => p.length));
|
|
9
47
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
48
|
const __dirname = dirname(__filename);
|
|
11
49
|
const isDev = !__dirname.includes('node_modules');
|
|
@@ -32,27 +70,37 @@ function writeToFile(level, prefix, args) {
|
|
|
32
70
|
const message = `[${timestamp}] [${level}] [${prefix}] ${args.map(formatArg).join(' ')}\n`;
|
|
33
71
|
fs.appendFileSync(logFilePath, message);
|
|
34
72
|
}
|
|
73
|
+
function getTimestamp() {
|
|
74
|
+
const now = new Date();
|
|
75
|
+
return `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
|
|
76
|
+
}
|
|
77
|
+
function padPrefix(prefix) {
|
|
78
|
+
return prefix.padEnd(MAX_PREFIX_LENGTH);
|
|
79
|
+
}
|
|
35
80
|
export function createLogger(prefix) {
|
|
81
|
+
const paddedPrefix = padPrefix(prefix);
|
|
36
82
|
return {
|
|
37
83
|
log: (...args) => {
|
|
38
84
|
writeToFile('INFO', prefix, args);
|
|
39
|
-
log.
|
|
85
|
+
console.log(pc.dim(getTimestamp()), pc.cyan(paddedPrefix), ...args.map(formatArg));
|
|
40
86
|
},
|
|
41
87
|
error: (...args) => {
|
|
42
88
|
writeToFile('ERROR', prefix, args);
|
|
43
|
-
|
|
89
|
+
// use clack for errors - visually distinct
|
|
90
|
+
clackLog.error([paddedPrefix, ...args.map(formatArg)].join(' '));
|
|
44
91
|
},
|
|
45
92
|
warn: (...args) => {
|
|
46
93
|
writeToFile('WARN', prefix, args);
|
|
47
|
-
|
|
94
|
+
// use clack for warnings - visually distinct
|
|
95
|
+
clackLog.warn([paddedPrefix, ...args.map(formatArg)].join(' '));
|
|
48
96
|
},
|
|
49
97
|
info: (...args) => {
|
|
50
98
|
writeToFile('INFO', prefix, args);
|
|
51
|
-
log.
|
|
99
|
+
console.log(pc.dim(getTimestamp()), pc.blue(paddedPrefix), ...args.map(formatArg));
|
|
52
100
|
},
|
|
53
101
|
debug: (...args) => {
|
|
54
102
|
writeToFile('DEBUG', prefix, args);
|
|
55
|
-
log.
|
|
103
|
+
console.log(pc.dim(getTimestamp()), pc.dim(paddedPrefix), ...args.map(formatArg));
|
|
56
104
|
},
|
|
57
105
|
};
|
|
58
106
|
}
|
package/dist/markdown.js
CHANGED
|
@@ -7,7 +7,7 @@ import { createTaggedError } from 'errore';
|
|
|
7
7
|
import * as yaml from 'js-yaml';
|
|
8
8
|
import { formatDateTime } from './utils.js';
|
|
9
9
|
import { extractNonXmlContent } from './xml.js';
|
|
10
|
-
import { createLogger } from './logger.js';
|
|
10
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
11
11
|
import { SessionNotFoundError, MessagesNotFoundError } from './errors.js';
|
|
12
12
|
// Generic error for unexpected exceptions in async operations
|
|
13
13
|
class UnexpectedError extends createTaggedError({
|
|
@@ -15,7 +15,7 @@ class UnexpectedError extends createTaggedError({
|
|
|
15
15
|
message: '$message',
|
|
16
16
|
}) {
|
|
17
17
|
}
|
|
18
|
-
const markdownLogger = createLogger(
|
|
18
|
+
const markdownLogger = createLogger(LogPrefix.MARKDOWN);
|
|
19
19
|
export class ShareMarkdown {
|
|
20
20
|
client;
|
|
21
21
|
constructor(client) {
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import * as errore from 'errore';
|
|
7
|
-
import { createLogger } from './logger.js';
|
|
7
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
8
8
|
import { FetchError } from './errors.js';
|
|
9
9
|
const ATTACHMENTS_DIR = path.join(process.cwd(), 'tmp', 'discord-attachments');
|
|
10
|
-
const logger = createLogger(
|
|
10
|
+
const logger = createLogger(LogPrefix.FORMATTING);
|
|
11
11
|
/**
|
|
12
12
|
* Escapes Discord inline markdown characters so dynamic content
|
|
13
13
|
* doesn't break formatting when wrapped in *, _, **, etc.
|
|
@@ -15,6 +15,12 @@ const logger = createLogger('FORMATTING');
|
|
|
15
15
|
function escapeInlineMarkdown(text) {
|
|
16
16
|
return text.replace(/([*_~|`\\])/g, '\\$1');
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Normalize whitespace: convert newlines to spaces and collapse consecutive spaces.
|
|
20
|
+
*/
|
|
21
|
+
function normalizeWhitespace(text) {
|
|
22
|
+
return text.replace(/[\r\n]+/g, ' ').replace(/\s+/g, ' ');
|
|
23
|
+
}
|
|
18
24
|
/**
|
|
19
25
|
* Collects and formats the last N assistant parts from session messages.
|
|
20
26
|
* Used by both /resume and /fork to show recent assistant context.
|
|
@@ -125,6 +131,61 @@ export function getToolSummaryText(part) {
|
|
|
125
131
|
? `*${escapeInlineMarkdown(fileName)}* (+${added}-${removed})`
|
|
126
132
|
: `(+${added}-${removed})`;
|
|
127
133
|
}
|
|
134
|
+
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
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return '';
|
|
188
|
+
}
|
|
128
189
|
if (part.tool === 'write') {
|
|
129
190
|
const filePath = part.state.input?.filePath || '';
|
|
130
191
|
const content = part.state.input?.content || '';
|
|
@@ -175,7 +236,8 @@ export function getToolSummaryText(part) {
|
|
|
175
236
|
if (value === null || value === undefined)
|
|
176
237
|
return null;
|
|
177
238
|
const stringValue = typeof value === 'string' ? value : JSON.stringify(value);
|
|
178
|
-
const
|
|
239
|
+
const normalized = normalizeWhitespace(stringValue);
|
|
240
|
+
const truncatedValue = normalized.length > 50 ? normalized.slice(0, 50) + '…' : normalized;
|
|
179
241
|
return `${key}: ${truncatedValue}`;
|
|
180
242
|
})
|
|
181
243
|
.filter(Boolean);
|
|
@@ -201,7 +263,7 @@ export function formatTodoList(part) {
|
|
|
201
263
|
return `${num} **${escapeInlineMarkdown(content)}**`;
|
|
202
264
|
}
|
|
203
265
|
export function formatPart(part, prefix) {
|
|
204
|
-
const pfx = prefix ? `${prefix}
|
|
266
|
+
const pfx = prefix ? `${prefix} ⋅ ` : '';
|
|
205
267
|
if (part.type === 'text') {
|
|
206
268
|
if (!part.text?.trim())
|
|
207
269
|
return '';
|
|
@@ -278,12 +340,13 @@ export function formatPart(part, prefix) {
|
|
|
278
340
|
if (part.state.status === 'error') {
|
|
279
341
|
return '⨯';
|
|
280
342
|
}
|
|
281
|
-
if (part.tool === 'edit' || part.tool === 'write') {
|
|
343
|
+
if (part.tool === 'edit' || part.tool === 'write' || part.tool === 'apply_patch') {
|
|
282
344
|
return '◼︎';
|
|
283
345
|
}
|
|
284
346
|
return '┣';
|
|
285
347
|
})();
|
|
286
|
-
|
|
348
|
+
const toolParts = [part.tool, toolTitle, summaryText].filter(Boolean).join(' ');
|
|
349
|
+
return `${icon} ${pfx}${toolParts}`;
|
|
287
350
|
}
|
|
288
351
|
logger.warn('Unknown part type:', part);
|
|
289
352
|
return '';
|
package/dist/openai-realtime.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// @ts-nocheck
|
|
4
4
|
import { RealtimeClient } from '@openai/realtime-api-beta';
|
|
5
5
|
import { writeFile } from 'fs';
|
|
6
|
-
import { createLogger } from './logger.js';
|
|
7
|
-
const openaiLogger = createLogger(
|
|
6
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
7
|
+
const openaiLogger = createLogger(LogPrefix.OPENAI);
|
|
8
8
|
const audioParts = [];
|
|
9
9
|
function saveBinaryFile(fileName, content) {
|
|
10
10
|
writeFile(fileName, content, 'utf8', (err) => {
|
package/dist/opencode.js
CHANGED
|
@@ -8,9 +8,9 @@ import net from 'node:net';
|
|
|
8
8
|
import { createOpencodeClient } from '@opencode-ai/sdk';
|
|
9
9
|
import { createOpencodeClient as createOpencodeClientV2, } from '@opencode-ai/sdk/v2';
|
|
10
10
|
import * as errore from 'errore';
|
|
11
|
-
import { createLogger } from './logger.js';
|
|
11
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
12
12
|
import { DirectoryNotAccessibleError, ServerStartError, ServerNotReadyError, FetchError, } from './errors.js';
|
|
13
|
-
const opencodeLogger = createLogger(
|
|
13
|
+
const opencodeLogger = createLogger(LogPrefix.OPENCODE);
|
|
14
14
|
const opencodeServers = new Map();
|
|
15
15
|
const serverRetryCount = new Map();
|
|
16
16
|
async function getOpenPort() {
|
package/dist/session-handler.js
CHANGED
|
@@ -7,14 +7,14 @@ import { initializeOpencodeForDirectory, getOpencodeServers, getOpencodeClientV2
|
|
|
7
7
|
import { sendThreadMessage, NOTIFY_MESSAGE_FLAGS, SILENT_MESSAGE_FLAGS } from './discord-utils.js';
|
|
8
8
|
import { formatPart } from './message-formatting.js';
|
|
9
9
|
import { getOpencodeSystemMessage } from './system-message.js';
|
|
10
|
-
import { createLogger } from './logger.js';
|
|
10
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
11
11
|
import { isAbortError } from './utils.js';
|
|
12
12
|
import { showAskUserQuestionDropdowns, cancelPendingQuestion, pendingQuestionContexts, } from './commands/ask-question.js';
|
|
13
13
|
import { showPermissionDropdown, cleanupPermissionContext, addPermissionRequestToContext, } from './commands/permissions.js';
|
|
14
14
|
import * as errore from 'errore';
|
|
15
|
-
const sessionLogger = createLogger(
|
|
16
|
-
const voiceLogger = createLogger(
|
|
17
|
-
const discordLogger = createLogger(
|
|
15
|
+
const sessionLogger = createLogger(LogPrefix.SESSION);
|
|
16
|
+
const voiceLogger = createLogger(LogPrefix.VOICE);
|
|
17
|
+
const discordLogger = createLogger(LogPrefix.DISCORD);
|
|
18
18
|
export const abortControllers = new Map();
|
|
19
19
|
// Track multiple pending permissions per thread (keyed by permission ID)
|
|
20
20
|
// OpenCode handles blocking/sequencing - we just need to track all pending permissions
|
|
@@ -56,7 +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
|
-
controller.abort('model-change');
|
|
59
|
+
controller.abort(new Error('model-change'));
|
|
60
60
|
// Also call the API abort endpoint
|
|
61
61
|
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
62
62
|
if (getClient instanceof Error) {
|
|
@@ -220,10 +220,10 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
220
220
|
await sendThreadMessage(thread, `⚠️ ${rejectedCount} pending permission request${plural} auto-rejected due to new message`);
|
|
221
221
|
}
|
|
222
222
|
}
|
|
223
|
-
//
|
|
224
|
-
const
|
|
225
|
-
if (
|
|
226
|
-
sessionLogger.log(`[QUESTION]
|
|
223
|
+
// Answer any pending question tool with the user's message (silently, no thread message)
|
|
224
|
+
const questionAnswered = await cancelPendingQuestion(thread.id, prompt);
|
|
225
|
+
if (questionAnswered) {
|
|
226
|
+
sessionLogger.log(`[QUESTION] Answered pending question with user message`);
|
|
227
227
|
}
|
|
228
228
|
const abortController = new AbortController();
|
|
229
229
|
abortControllers.set(session.id, abortController);
|
|
@@ -272,6 +272,7 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
272
272
|
let usedAgent;
|
|
273
273
|
let tokensUsedInSession = 0;
|
|
274
274
|
let lastDisplayedContextPercentage = 0;
|
|
275
|
+
let lastRateLimitDisplayTime = 0;
|
|
275
276
|
let modelContextLimit;
|
|
276
277
|
let assistantMessageId;
|
|
277
278
|
let handlerPromise = null;
|
|
@@ -725,10 +726,44 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
725
726
|
});
|
|
726
727
|
});
|
|
727
728
|
};
|
|
729
|
+
const handleSessionStatus = async (properties) => {
|
|
730
|
+
if (properties.sessionID !== session.id) {
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
if (properties.status.type !== 'retry') {
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
// Throttle to once per 10 seconds
|
|
737
|
+
const now = Date.now();
|
|
738
|
+
if (now - lastRateLimitDisplayTime < 10_000) {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
lastRateLimitDisplayTime = now;
|
|
742
|
+
const { attempt, message, next } = properties.status;
|
|
743
|
+
const remainingMs = Math.max(0, next - now);
|
|
744
|
+
const remainingSec = Math.ceil(remainingMs / 1000);
|
|
745
|
+
const duration = (() => {
|
|
746
|
+
if (remainingSec < 60) {
|
|
747
|
+
return `${remainingSec}s`;
|
|
748
|
+
}
|
|
749
|
+
const mins = Math.floor(remainingSec / 60);
|
|
750
|
+
const secs = remainingSec % 60;
|
|
751
|
+
return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
|
|
752
|
+
})();
|
|
753
|
+
const chunk = `⬦ ${message} - retrying in ${duration} (attempt #${attempt})`;
|
|
754
|
+
await thread.send({ content: chunk, flags: SILENT_MESSAGE_FLAGS });
|
|
755
|
+
};
|
|
728
756
|
const handleSessionIdle = (idleSessionId) => {
|
|
729
757
|
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)`);
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
730
765
|
sessionLogger.log(`[SESSION IDLE] Session ${session.id} is idle, aborting`);
|
|
731
|
-
abortController.abort('finished');
|
|
766
|
+
abortController.abort(new Error('finished'));
|
|
732
767
|
return;
|
|
733
768
|
}
|
|
734
769
|
if (!subtaskSessions.has(idleSessionId)) {
|
|
@@ -763,6 +798,9 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
763
798
|
case 'session.idle':
|
|
764
799
|
handleSessionIdle(event.properties.sessionID);
|
|
765
800
|
break;
|
|
801
|
+
case 'session.status':
|
|
802
|
+
await handleSessionStatus(event.properties);
|
|
803
|
+
break;
|
|
766
804
|
default:
|
|
767
805
|
break;
|
|
768
806
|
}
|
|
@@ -791,7 +829,8 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
791
829
|
stopTyping();
|
|
792
830
|
stopTyping = null;
|
|
793
831
|
}
|
|
794
|
-
|
|
832
|
+
const abortReason = abortController.signal.reason?.message;
|
|
833
|
+
if (!abortController.signal.aborted || abortReason === 'finished') {
|
|
795
834
|
const sessionDuration = prettyMilliseconds(Date.now() - sessionStartTime);
|
|
796
835
|
const attachCommand = port ? ` ⋅ ${session.id}` : '';
|
|
797
836
|
const modelInfo = usedModel ? ` ⋅ ${usedModel}` : '';
|
|
@@ -860,7 +899,7 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
860
899
|
}
|
|
861
900
|
}
|
|
862
901
|
else {
|
|
863
|
-
sessionLogger.log(`Session was aborted (reason: ${
|
|
902
|
+
sessionLogger.log(`Session was aborted (reason: ${abortReason}), skipping duration message`);
|
|
864
903
|
}
|
|
865
904
|
}
|
|
866
905
|
};
|
|
@@ -958,7 +997,7 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
958
997
|
})();
|
|
959
998
|
throw new Error(`OpenCode API error (${response.response.status}): ${errorMessage}`);
|
|
960
999
|
}
|
|
961
|
-
abortController.abort('finished');
|
|
1000
|
+
abortController.abort(new Error('finished'));
|
|
962
1001
|
sessionLogger.log(`Successfully sent prompt, got response`);
|
|
963
1002
|
if (originalMessage) {
|
|
964
1003
|
const reactionResult = await errore.tryAsync(async () => {
|
|
@@ -987,7 +1026,7 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
987
1026
|
return;
|
|
988
1027
|
}
|
|
989
1028
|
sessionLogger.error(`ERROR: Failed to send prompt:`, promptError);
|
|
990
|
-
abortController.abort('error');
|
|
1029
|
+
abortController.abort(new Error('error'));
|
|
991
1030
|
if (originalMessage) {
|
|
992
1031
|
const reactionResult = await errore.tryAsync(async () => {
|
|
993
1032
|
await originalMessage.reactions.removeAll();
|
package/dist/tools.js
CHANGED
|
@@ -6,9 +6,9 @@ import { z } from 'zod';
|
|
|
6
6
|
import { spawn } from 'node:child_process';
|
|
7
7
|
import net from 'node:net';
|
|
8
8
|
import { createOpencodeClient, } from '@opencode-ai/sdk';
|
|
9
|
-
import { createLogger } from './logger.js';
|
|
9
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
10
10
|
import * as errore from 'errore';
|
|
11
|
-
const toolsLogger = createLogger(
|
|
11
|
+
const toolsLogger = createLogger(LogPrefix.TOOLS);
|
|
12
12
|
import { ShareMarkdown } from './markdown.js';
|
|
13
13
|
import { formatDistanceToNow } from './utils.js';
|
|
14
14
|
import pc from 'picocolors';
|
package/dist/voice-handler.js
CHANGED
|
@@ -17,8 +17,8 @@ import { getDatabase } from './database.js';
|
|
|
17
17
|
import { sendThreadMessage, escapeDiscordFormatting, SILENT_MESSAGE_FLAGS, } from './discord-utils.js';
|
|
18
18
|
import { transcribeAudio } from './voice.js';
|
|
19
19
|
import { FetchError } from './errors.js';
|
|
20
|
-
import { createLogger } from './logger.js';
|
|
21
|
-
const voiceLogger = createLogger(
|
|
20
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
21
|
+
const voiceLogger = createLogger(LogPrefix.VOICE);
|
|
22
22
|
export const voiceConnections = new Map();
|
|
23
23
|
export function convertToMono16k(buffer) {
|
|
24
24
|
const inputSampleRate = 48000;
|
package/dist/voice.js
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
// Uses errore for type-safe error handling.
|
|
5
5
|
import { GoogleGenAI, Type } from '@google/genai';
|
|
6
6
|
import * as errore from 'errore';
|
|
7
|
-
import { createLogger } from './logger.js';
|
|
7
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
8
8
|
import { glob } from 'glob';
|
|
9
9
|
import { ripGrep } from 'ripgrep-js';
|
|
10
10
|
import { ApiKeyMissingError, InvalidAudioFormatError, TranscriptionError, EmptyTranscriptionError, NoResponseContentError, NoToolResponseError, GrepSearchError, GlobSearchError, } from './errors.js';
|
|
11
|
-
const voiceLogger = createLogger(
|
|
11
|
+
const voiceLogger = createLogger(LogPrefix.VOICE);
|
|
12
12
|
function runGrep({ pattern, directory }) {
|
|
13
13
|
return errore.tryAsync({
|
|
14
14
|
try: async () => {
|
package/dist/worktree-utils.js
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
// Worktree utility functions.
|
|
2
2
|
// Wrapper for OpenCode worktree creation that also initializes git submodules.
|
|
3
|
-
|
|
3
|
+
// Also handles capturing and applying git diffs when creating worktrees from threads.
|
|
4
|
+
import { exec, spawn } from 'node:child_process';
|
|
4
5
|
import { promisify } from 'node:util';
|
|
5
|
-
import { createLogger } from './logger.js';
|
|
6
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
6
7
|
export const execAsync = promisify(exec);
|
|
7
|
-
const logger = createLogger(
|
|
8
|
+
const logger = createLogger(LogPrefix.WORKTREE);
|
|
8
9
|
/**
|
|
9
10
|
* Create a worktree using OpenCode SDK and initialize git submodules.
|
|
10
11
|
* This wrapper ensures submodules are properly set up in new worktrees.
|
|
12
|
+
*
|
|
13
|
+
* If diff is provided, it's applied BEFORE submodule update to ensure
|
|
14
|
+
* any submodule pointer changes in the diff are respected.
|
|
11
15
|
*/
|
|
12
|
-
export async function createWorktreeWithSubmodules({ clientV2, directory, name, }) {
|
|
16
|
+
export async function createWorktreeWithSubmodules({ clientV2, directory, name, diff, }) {
|
|
13
17
|
// 1. Create worktree via OpenCode SDK
|
|
14
18
|
const response = await clientV2.worktree.create({
|
|
15
19
|
directory,
|
|
@@ -22,7 +26,17 @@ export async function createWorktreeWithSubmodules({ clientV2, directory, name,
|
|
|
22
26
|
return new Error('No worktree data returned from SDK');
|
|
23
27
|
}
|
|
24
28
|
const worktreeDir = response.data.directory;
|
|
25
|
-
|
|
29
|
+
let diffApplied = false;
|
|
30
|
+
// 2. Apply diff BEFORE submodule update (if provided)
|
|
31
|
+
// This ensures any submodule pointer changes in the diff are applied first,
|
|
32
|
+
// so submodule update checks out the correct commits.
|
|
33
|
+
if (diff) {
|
|
34
|
+
logger.log(`Applying diff to ${worktreeDir} before submodule init`);
|
|
35
|
+
diffApplied = await applyGitDiff(worktreeDir, diff);
|
|
36
|
+
}
|
|
37
|
+
// 3. Init submodules in new worktree (don't block on failure)
|
|
38
|
+
// Uses --init to initialize, --recursive for nested submodules.
|
|
39
|
+
// Submodules will be checked out at the commit specified by the (possibly updated) index.
|
|
26
40
|
try {
|
|
27
41
|
logger.log(`Initializing submodules in ${worktreeDir}`);
|
|
28
42
|
await execAsync('git submodule update --init --recursive', {
|
|
@@ -34,7 +48,7 @@ export async function createWorktreeWithSubmodules({ clientV2, directory, name,
|
|
|
34
48
|
// Log but don't fail - submodules might not exist
|
|
35
49
|
logger.warn(`Failed to init submodules in ${worktreeDir}: ${e instanceof Error ? e.message : String(e)}`);
|
|
36
50
|
}
|
|
37
|
-
//
|
|
51
|
+
// 4. Install dependencies using ni (detects package manager from lockfile)
|
|
38
52
|
try {
|
|
39
53
|
logger.log(`Installing dependencies in ${worktreeDir}`);
|
|
40
54
|
await execAsync('npx -y ni', {
|
|
@@ -46,5 +60,75 @@ export async function createWorktreeWithSubmodules({ clientV2, directory, name,
|
|
|
46
60
|
// Log but don't fail - might not be a JS project or might fail for various reasons
|
|
47
61
|
logger.warn(`Failed to install dependencies in ${worktreeDir}: ${e instanceof Error ? e.message : String(e)}`);
|
|
48
62
|
}
|
|
49
|
-
return response.data;
|
|
63
|
+
return { ...response.data, diffApplied };
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Capture git diff from a directory (both staged and unstaged changes).
|
|
67
|
+
* Returns null if no changes or on error.
|
|
68
|
+
*/
|
|
69
|
+
export async function captureGitDiff(directory) {
|
|
70
|
+
try {
|
|
71
|
+
// Capture unstaged changes
|
|
72
|
+
const unstagedResult = await execAsync('git diff', { cwd: directory });
|
|
73
|
+
const unstaged = unstagedResult.stdout.trim();
|
|
74
|
+
// Capture staged changes
|
|
75
|
+
const stagedResult = await execAsync('git diff --staged', { cwd: directory });
|
|
76
|
+
const staged = stagedResult.stdout.trim();
|
|
77
|
+
if (!unstaged && !staged) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
return { unstaged, staged };
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
logger.warn(`Failed to capture git diff from ${directory}: ${e instanceof Error ? e.message : String(e)}`);
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Run a git command with stdin input.
|
|
89
|
+
* Uses spawn to pipe the diff content to git apply.
|
|
90
|
+
*/
|
|
91
|
+
function runGitWithStdin(args, cwd, input) {
|
|
92
|
+
return new Promise((resolve, reject) => {
|
|
93
|
+
const child = spawn('git', args, { cwd, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
94
|
+
let stderr = '';
|
|
95
|
+
child.stderr?.on('data', (data) => {
|
|
96
|
+
stderr += data.toString();
|
|
97
|
+
});
|
|
98
|
+
child.on('close', (code) => {
|
|
99
|
+
if (code === 0) {
|
|
100
|
+
resolve();
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
reject(new Error(stderr || `git ${args.join(' ')} failed with code ${code}`));
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
child.on('error', reject);
|
|
107
|
+
child.stdin?.write(input);
|
|
108
|
+
child.stdin?.end();
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Apply a captured git diff to a directory.
|
|
113
|
+
* Applies staged changes first, then unstaged.
|
|
114
|
+
*/
|
|
115
|
+
export async function applyGitDiff(directory, diff) {
|
|
116
|
+
try {
|
|
117
|
+
// Apply staged changes first (and stage them)
|
|
118
|
+
if (diff.staged) {
|
|
119
|
+
logger.log(`Applying staged diff to ${directory}`);
|
|
120
|
+
await runGitWithStdin(['apply', '--index'], directory, diff.staged);
|
|
121
|
+
}
|
|
122
|
+
// Apply unstaged changes (don't stage them)
|
|
123
|
+
if (diff.unstaged) {
|
|
124
|
+
logger.log(`Applying unstaged diff to ${directory}`);
|
|
125
|
+
await runGitWithStdin(['apply'], directory, diff.unstaged);
|
|
126
|
+
}
|
|
127
|
+
logger.log(`Successfully applied diff to ${directory}`);
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
logger.warn(`Failed to apply git diff to ${directory}: ${e instanceof Error ? e.message : String(e)}`);
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
50
134
|
}
|
package/dist/xml.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// Parses XML-like tags from strings (e.g., channel topics) to extract
|
|
3
3
|
// Kimaki configuration like directory paths and app IDs.
|
|
4
4
|
import { DomHandler, Parser, ElementType } from 'htmlparser2';
|
|
5
|
-
import { createLogger } from './logger.js';
|
|
6
|
-
const xmlLogger = createLogger(
|
|
5
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
6
|
+
const xmlLogger = createLogger(LogPrefix.XML);
|
|
7
7
|
export function extractTagsArrays({ xml, tags, }) {
|
|
8
8
|
const result = {
|
|
9
9
|
others: [],
|