kimaki 0.4.38 → 0.4.40
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 -23
- package/dist/commands/abort.js +15 -6
- package/dist/commands/add-project.js +9 -0
- package/dist/commands/agent.js +13 -1
- package/dist/commands/fork.js +13 -2
- package/dist/commands/model.js +12 -0
- package/dist/commands/remove-project.js +26 -16
- package/dist/commands/resume.js +9 -0
- package/dist/commands/session.js +14 -1
- package/dist/commands/share.js +10 -1
- package/dist/commands/undo-redo.js +13 -4
- package/dist/commands/worktree.js +180 -0
- package/dist/database.js +57 -5
- package/dist/discord-bot.js +48 -10
- package/dist/discord-utils.js +36 -0
- package/dist/errors.js +109 -0
- package/dist/genai-worker.js +18 -16
- package/dist/interaction-handler.js +6 -2
- package/dist/markdown.js +100 -85
- package/dist/markdown.test.js +10 -3
- package/dist/message-formatting.js +50 -37
- package/dist/opencode.js +43 -46
- package/dist/session-handler.js +100 -2
- package/dist/system-message.js +2 -0
- package/dist/tools.js +18 -8
- package/dist/voice-handler.js +48 -25
- package/dist/voice.js +159 -131
- package/package.json +4 -2
- package/src/cli.ts +31 -32
- package/src/commands/abort.ts +17 -7
- package/src/commands/add-project.ts +9 -0
- package/src/commands/agent.ts +13 -1
- package/src/commands/fork.ts +18 -7
- package/src/commands/model.ts +12 -0
- package/src/commands/remove-project.ts +28 -16
- package/src/commands/resume.ts +9 -0
- package/src/commands/session.ts +14 -1
- package/src/commands/share.ts +11 -1
- package/src/commands/undo-redo.ts +15 -6
- package/src/commands/worktree.ts +243 -0
- package/src/database.ts +104 -4
- package/src/discord-bot.ts +49 -9
- package/src/discord-utils.ts +50 -0
- package/src/errors.ts +138 -0
- package/src/genai-worker.ts +20 -17
- package/src/interaction-handler.ts +7 -2
- package/src/markdown.test.ts +13 -3
- package/src/markdown.ts +112 -95
- package/src/message-formatting.ts +55 -38
- package/src/opencode.ts +52 -49
- package/src/session-handler.ts +118 -3
- package/src/system-message.ts +2 -0
- package/src/tools.ts +18 -8
- package/src/voice-handler.ts +48 -23
- package/src/voice.ts +195 -148
package/dist/database.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import Database from 'better-sqlite3';
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
+
import * as errore from 'errore';
|
|
7
8
|
import { createLogger } from './logger.js';
|
|
8
9
|
import { getDataDir } from './config.js';
|
|
9
10
|
const dbLogger = createLogger('DB');
|
|
@@ -11,11 +12,14 @@ let db = null;
|
|
|
11
12
|
export function getDatabase() {
|
|
12
13
|
if (!db) {
|
|
13
14
|
const dataDir = getDataDir();
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
const mkdirError = errore.tryFn({
|
|
16
|
+
try: () => {
|
|
17
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
18
|
+
},
|
|
19
|
+
catch: (e) => e,
|
|
20
|
+
});
|
|
21
|
+
if (mkdirError instanceof Error) {
|
|
22
|
+
dbLogger.error(`Failed to create data directory ${dataDir}:`, mkdirError.message);
|
|
19
23
|
}
|
|
20
24
|
const dbPath = path.join(dataDir, 'discord-sessions.db');
|
|
21
25
|
dbLogger.log(`Opening database at: ${dbPath}`);
|
|
@@ -71,6 +75,19 @@ export function getDatabase() {
|
|
|
71
75
|
xai_api_key TEXT,
|
|
72
76
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
73
77
|
)
|
|
78
|
+
`);
|
|
79
|
+
// Track worktrees created for threads (for /new-worktree command)
|
|
80
|
+
// status: 'pending' while creating, 'ready' when done, 'error' if failed
|
|
81
|
+
db.exec(`
|
|
82
|
+
CREATE TABLE IF NOT EXISTS thread_worktrees (
|
|
83
|
+
thread_id TEXT PRIMARY KEY,
|
|
84
|
+
worktree_name TEXT NOT NULL,
|
|
85
|
+
worktree_directory TEXT,
|
|
86
|
+
project_directory TEXT NOT NULL,
|
|
87
|
+
status TEXT DEFAULT 'pending',
|
|
88
|
+
error_message TEXT,
|
|
89
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
90
|
+
)
|
|
74
91
|
`);
|
|
75
92
|
runModelMigrations(db);
|
|
76
93
|
}
|
|
@@ -198,6 +215,41 @@ export function setSessionAgent(sessionId, agentName) {
|
|
|
198
215
|
const db = getDatabase();
|
|
199
216
|
db.prepare(`INSERT OR REPLACE INTO session_agents (session_id, agent_name) VALUES (?, ?)`).run(sessionId, agentName);
|
|
200
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* Get the worktree info for a thread.
|
|
220
|
+
*/
|
|
221
|
+
export function getThreadWorktree(threadId) {
|
|
222
|
+
const db = getDatabase();
|
|
223
|
+
return db.prepare('SELECT * FROM thread_worktrees WHERE thread_id = ?').get(threadId);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Create a pending worktree entry for a thread.
|
|
227
|
+
*/
|
|
228
|
+
export function createPendingWorktree({ threadId, worktreeName, projectDirectory, }) {
|
|
229
|
+
const db = getDatabase();
|
|
230
|
+
db.prepare(`INSERT OR REPLACE INTO thread_worktrees (thread_id, worktree_name, project_directory, status) VALUES (?, ?, ?, 'pending')`).run(threadId, worktreeName, projectDirectory);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Mark a worktree as ready with its directory.
|
|
234
|
+
*/
|
|
235
|
+
export function setWorktreeReady({ threadId, worktreeDirectory, }) {
|
|
236
|
+
const db = getDatabase();
|
|
237
|
+
db.prepare(`UPDATE thread_worktrees SET worktree_directory = ?, status = 'ready' WHERE thread_id = ?`).run(worktreeDirectory, threadId);
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Mark a worktree as failed with error message.
|
|
241
|
+
*/
|
|
242
|
+
export function setWorktreeError({ threadId, errorMessage, }) {
|
|
243
|
+
const db = getDatabase();
|
|
244
|
+
db.prepare(`UPDATE thread_worktrees SET status = 'error', error_message = ? WHERE thread_id = ?`).run(errorMessage, threadId);
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Delete the worktree info for a thread.
|
|
248
|
+
*/
|
|
249
|
+
export function deleteThreadWorktree(threadId) {
|
|
250
|
+
const db = getDatabase();
|
|
251
|
+
db.prepare('DELETE FROM thread_worktrees WHERE thread_id = ?').run(threadId);
|
|
252
|
+
}
|
|
201
253
|
export function closeDatabase() {
|
|
202
254
|
if (db) {
|
|
203
255
|
db.close();
|
package/dist/discord-bot.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Core Discord bot module that handles message events and bot lifecycle.
|
|
2
2
|
// Bridges Discord messages to OpenCode sessions, manages voice connections,
|
|
3
3
|
// and orchestrates the main event loop for the Kimaki bot.
|
|
4
|
-
import { getDatabase, closeDatabase } from './database.js';
|
|
4
|
+
import { getDatabase, closeDatabase, getThreadWorktree } from './database.js';
|
|
5
5
|
import { initializeOpencodeForDirectory, getOpencodeServers } from './opencode.js';
|
|
6
6
|
import { escapeBackticksInCodeBlocks, splitMarkdownForDiscord, SILENT_MESSAGE_FLAGS, } from './discord-utils.js';
|
|
7
7
|
import { getOpencodeSystemMessage } from './system-message.js';
|
|
@@ -18,10 +18,14 @@ export { getOpencodeSystemMessage } from './system-message.js';
|
|
|
18
18
|
export { ensureKimakiCategory, ensureKimakiAudioCategory, createProjectChannels, getChannelsWithDescriptions, } from './channel-management.js';
|
|
19
19
|
import { ChannelType, Client, Events, GatewayIntentBits, Partials, PermissionsBitField, ThreadAutoArchiveDuration, } from 'discord.js';
|
|
20
20
|
import fs from 'node:fs';
|
|
21
|
+
import * as errore from 'errore';
|
|
21
22
|
import { extractTagsArrays } from './xml.js';
|
|
22
23
|
import { createLogger } from './logger.js';
|
|
23
24
|
import { setGlobalDispatcher, Agent } from 'undici';
|
|
24
|
-
|
|
25
|
+
// Increase connection pool to prevent deadlock when multiple sessions have open SSE streams.
|
|
26
|
+
// Each session's event.subscribe() holds a connection; without enough connections,
|
|
27
|
+
// regular HTTP requests (question.reply, session.prompt) get blocked → deadlock.
|
|
28
|
+
setGlobalDispatcher(new Agent({ headersTimeout: 0, bodyTimeout: 0, connections: 500 }));
|
|
25
29
|
const discordLogger = createLogger('DISCORD');
|
|
26
30
|
const voiceLogger = createLogger('VOICE');
|
|
27
31
|
export async function createDiscordClient() {
|
|
@@ -89,11 +93,12 @@ export async function startDiscordBot({ token, appId, discordClient, }) {
|
|
|
89
93
|
}
|
|
90
94
|
if (message.partial) {
|
|
91
95
|
discordLogger.log(`Fetching partial message ${message.id}`);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
const fetched = await errore.tryAsync({
|
|
97
|
+
try: () => message.fetch(),
|
|
98
|
+
catch: (e) => e,
|
|
99
|
+
});
|
|
100
|
+
if (fetched instanceof Error) {
|
|
101
|
+
discordLogger.log(`Failed to fetch partial message ${message.id}:`, fetched.message);
|
|
97
102
|
return;
|
|
98
103
|
}
|
|
99
104
|
}
|
|
@@ -130,6 +135,28 @@ export async function startDiscordBot({ token, appId, discordClient, }) {
|
|
|
130
135
|
projectDirectory = extracted['kimaki.directory']?.[0]?.trim();
|
|
131
136
|
channelAppId = extracted['kimaki.app']?.[0]?.trim();
|
|
132
137
|
}
|
|
138
|
+
// Check if this thread is a worktree thread
|
|
139
|
+
const worktreeInfo = getThreadWorktree(thread.id);
|
|
140
|
+
if (worktreeInfo) {
|
|
141
|
+
if (worktreeInfo.status === 'pending') {
|
|
142
|
+
await message.reply({
|
|
143
|
+
content: '⏳ Worktree is still being created. Please wait...',
|
|
144
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
145
|
+
});
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (worktreeInfo.status === 'error') {
|
|
149
|
+
await message.reply({
|
|
150
|
+
content: `❌ Worktree creation failed: ${worktreeInfo.error_message}`,
|
|
151
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
152
|
+
});
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (worktreeInfo.worktree_directory) {
|
|
156
|
+
projectDirectory = worktreeInfo.worktree_directory;
|
|
157
|
+
discordLogger.log(`Using worktree directory: ${projectDirectory}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
133
160
|
if (channelAppId && channelAppId !== currentAppId) {
|
|
134
161
|
voiceLogger.log(`[IGNORED] Thread belongs to different bot app (expected: ${currentAppId}, got: ${channelAppId})`);
|
|
135
162
|
return;
|
|
@@ -173,28 +200,39 @@ export async function startDiscordBot({ token, appId, discordClient, }) {
|
|
|
173
200
|
if (projectDirectory) {
|
|
174
201
|
try {
|
|
175
202
|
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
203
|
+
if (getClient instanceof Error) {
|
|
204
|
+
voiceLogger.error(`[SESSION] Failed to initialize OpenCode client:`, getClient.message);
|
|
205
|
+
throw new Error(getClient.message);
|
|
206
|
+
}
|
|
176
207
|
const client = getClient();
|
|
177
208
|
// get current session context (without system prompt, it would be duplicated)
|
|
178
209
|
if (row.session_id) {
|
|
179
|
-
|
|
210
|
+
const result = await getCompactSessionContext({
|
|
180
211
|
client,
|
|
181
212
|
sessionId: row.session_id,
|
|
182
213
|
includeSystemPrompt: false,
|
|
183
214
|
maxMessages: 15,
|
|
184
215
|
});
|
|
216
|
+
if (errore.isOk(result)) {
|
|
217
|
+
currentSessionContext = result;
|
|
218
|
+
}
|
|
185
219
|
}
|
|
186
220
|
// get last session context (with system prompt for project context)
|
|
187
|
-
const
|
|
221
|
+
const lastSessionResult = await getLastSessionId({
|
|
188
222
|
client,
|
|
189
223
|
excludeSessionId: row.session_id,
|
|
190
224
|
});
|
|
225
|
+
const lastSessionId = errore.unwrapOr(lastSessionResult, null);
|
|
191
226
|
if (lastSessionId) {
|
|
192
|
-
|
|
227
|
+
const result = await getCompactSessionContext({
|
|
193
228
|
client,
|
|
194
229
|
sessionId: lastSessionId,
|
|
195
230
|
includeSystemPrompt: true,
|
|
196
231
|
maxMessages: 10,
|
|
197
232
|
});
|
|
233
|
+
if (errore.isOk(result)) {
|
|
234
|
+
lastSessionContext = result;
|
|
235
|
+
}
|
|
198
236
|
}
|
|
199
237
|
}
|
|
200
238
|
catch (e) {
|
package/dist/discord-utils.js
CHANGED
|
@@ -8,6 +8,9 @@ import { formatMarkdownTables } from './format-tables.js';
|
|
|
8
8
|
import { limitHeadingDepth } from './limit-heading-depth.js';
|
|
9
9
|
import { unnestCodeBlocksFromLists } from './unnest-code-blocks.js';
|
|
10
10
|
import { createLogger } from './logger.js';
|
|
11
|
+
import mime from 'mime';
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
11
14
|
const discordLogger = createLogger('DISCORD');
|
|
12
15
|
export const SILENT_MESSAGE_FLAGS = 4 | 4096;
|
|
13
16
|
// Same as SILENT but without SuppressNotifications - triggers badge/notification
|
|
@@ -244,3 +247,36 @@ export function getKimakiMetadata(textChannel) {
|
|
|
244
247
|
const channelAppId = extracted['kimaki.app']?.[0]?.trim();
|
|
245
248
|
return { projectDirectory, channelAppId };
|
|
246
249
|
}
|
|
250
|
+
/**
|
|
251
|
+
* Upload files to a Discord thread/channel in a single message.
|
|
252
|
+
* Sending all files in one message causes Discord to display images in a grid layout.
|
|
253
|
+
*/
|
|
254
|
+
export async function uploadFilesToDiscord({ threadId, botToken, files, }) {
|
|
255
|
+
if (files.length === 0) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
// Build attachments array for all files
|
|
259
|
+
const attachments = files.map((file, index) => ({
|
|
260
|
+
id: index,
|
|
261
|
+
filename: path.basename(file),
|
|
262
|
+
}));
|
|
263
|
+
const formData = new FormData();
|
|
264
|
+
formData.append('payload_json', JSON.stringify({ attachments }));
|
|
265
|
+
// Append each file with its array index, with correct MIME type for grid display
|
|
266
|
+
files.forEach((file, index) => {
|
|
267
|
+
const buffer = fs.readFileSync(file);
|
|
268
|
+
const mimeType = mime.getType(file) || 'application/octet-stream';
|
|
269
|
+
formData.append(`files[${index}]`, new Blob([buffer], { type: mimeType }), path.basename(file));
|
|
270
|
+
});
|
|
271
|
+
const response = await fetch(`https://discord.com/api/v10/channels/${threadId}/messages`, {
|
|
272
|
+
method: 'POST',
|
|
273
|
+
headers: {
|
|
274
|
+
Authorization: `Bot ${botToken}`,
|
|
275
|
+
},
|
|
276
|
+
body: formData,
|
|
277
|
+
});
|
|
278
|
+
if (!response.ok) {
|
|
279
|
+
const error = await response.text();
|
|
280
|
+
throw new Error(`Discord API error: ${response.status} - ${error}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// TaggedError definitions for type-safe error handling with errore.
|
|
2
|
+
// Errors are grouped by category: infrastructure, domain, and validation.
|
|
3
|
+
// Use errore.matchError() for exhaustive error handling in command handlers.
|
|
4
|
+
import { createTaggedError } from 'errore';
|
|
5
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
6
|
+
// INFRASTRUCTURE ERRORS - Server, filesystem, external services
|
|
7
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
8
|
+
export class DirectoryNotAccessibleError extends createTaggedError({
|
|
9
|
+
name: 'DirectoryNotAccessibleError',
|
|
10
|
+
message: 'Directory does not exist or is not accessible: $directory',
|
|
11
|
+
}) {
|
|
12
|
+
}
|
|
13
|
+
export class ServerStartError extends createTaggedError({
|
|
14
|
+
name: 'ServerStartError',
|
|
15
|
+
message: 'Server failed to start on port $port: $reason',
|
|
16
|
+
}) {
|
|
17
|
+
}
|
|
18
|
+
export class ServerNotFoundError extends createTaggedError({
|
|
19
|
+
name: 'ServerNotFoundError',
|
|
20
|
+
message: 'OpenCode server not found for directory: $directory',
|
|
21
|
+
}) {
|
|
22
|
+
}
|
|
23
|
+
export class ServerNotReadyError extends createTaggedError({
|
|
24
|
+
name: 'ServerNotReadyError',
|
|
25
|
+
message: 'OpenCode server for directory "$directory" is in an error state (no client available)',
|
|
26
|
+
}) {
|
|
27
|
+
}
|
|
28
|
+
export class ApiKeyMissingError extends createTaggedError({
|
|
29
|
+
name: 'ApiKeyMissingError',
|
|
30
|
+
message: '$service API key is required',
|
|
31
|
+
}) {
|
|
32
|
+
}
|
|
33
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
34
|
+
// DOMAIN ERRORS - Sessions, messages, transcription
|
|
35
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
36
|
+
export class SessionNotFoundError extends createTaggedError({
|
|
37
|
+
name: 'SessionNotFoundError',
|
|
38
|
+
message: 'Session $sessionId not found',
|
|
39
|
+
}) {
|
|
40
|
+
}
|
|
41
|
+
export class SessionCreateError extends createTaggedError({
|
|
42
|
+
name: 'SessionCreateError',
|
|
43
|
+
message: '$message',
|
|
44
|
+
}) {
|
|
45
|
+
}
|
|
46
|
+
export class MessagesNotFoundError extends createTaggedError({
|
|
47
|
+
name: 'MessagesNotFoundError',
|
|
48
|
+
message: 'No messages found for session $sessionId',
|
|
49
|
+
}) {
|
|
50
|
+
}
|
|
51
|
+
export class TranscriptionError extends createTaggedError({
|
|
52
|
+
name: 'TranscriptionError',
|
|
53
|
+
message: 'Transcription failed: $reason',
|
|
54
|
+
}) {
|
|
55
|
+
}
|
|
56
|
+
export class GrepSearchError extends createTaggedError({
|
|
57
|
+
name: 'GrepSearchError',
|
|
58
|
+
message: 'Grep search failed for pattern: $pattern',
|
|
59
|
+
}) {
|
|
60
|
+
}
|
|
61
|
+
export class GlobSearchError extends createTaggedError({
|
|
62
|
+
name: 'GlobSearchError',
|
|
63
|
+
message: 'Glob search failed for pattern: $pattern',
|
|
64
|
+
}) {
|
|
65
|
+
}
|
|
66
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
67
|
+
// VALIDATION ERRORS - Input validation, format checks
|
|
68
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
69
|
+
export class InvalidAudioFormatError extends createTaggedError({
|
|
70
|
+
name: 'InvalidAudioFormatError',
|
|
71
|
+
message: 'Invalid audio format',
|
|
72
|
+
}) {
|
|
73
|
+
}
|
|
74
|
+
export class EmptyTranscriptionError extends createTaggedError({
|
|
75
|
+
name: 'EmptyTranscriptionError',
|
|
76
|
+
message: 'Model returned empty transcription',
|
|
77
|
+
}) {
|
|
78
|
+
}
|
|
79
|
+
export class NoResponseContentError extends createTaggedError({
|
|
80
|
+
name: 'NoResponseContentError',
|
|
81
|
+
message: 'No response content from model',
|
|
82
|
+
}) {
|
|
83
|
+
}
|
|
84
|
+
export class NoToolResponseError extends createTaggedError({
|
|
85
|
+
name: 'NoToolResponseError',
|
|
86
|
+
message: 'No valid tool responses',
|
|
87
|
+
}) {
|
|
88
|
+
}
|
|
89
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
90
|
+
// NETWORK ERRORS - Fetch and HTTP
|
|
91
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
92
|
+
export class FetchError extends createTaggedError({
|
|
93
|
+
name: 'FetchError',
|
|
94
|
+
message: 'Fetch failed for $url',
|
|
95
|
+
}) {
|
|
96
|
+
}
|
|
97
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
98
|
+
// API ERRORS - External service responses
|
|
99
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
100
|
+
export class DiscordApiError extends createTaggedError({
|
|
101
|
+
name: 'DiscordApiError',
|
|
102
|
+
message: 'Discord API error: $status $body',
|
|
103
|
+
}) {
|
|
104
|
+
}
|
|
105
|
+
export class OpenCodeApiError extends createTaggedError({
|
|
106
|
+
name: 'OpenCodeApiError',
|
|
107
|
+
message: 'OpenCode API error ($status): $body',
|
|
108
|
+
}) {
|
|
109
|
+
}
|
package/dist/genai-worker.js
CHANGED
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
// Resamples 24kHz GenAI output to 48kHz stereo Opus packets for Discord.
|
|
4
4
|
import { parentPort, threadId } from 'node:worker_threads';
|
|
5
5
|
import { createWriteStream } from 'node:fs';
|
|
6
|
-
import { mkdir } from 'node:fs/promises';
|
|
7
6
|
import path from 'node:path';
|
|
7
|
+
import * as errore from 'errore';
|
|
8
8
|
import { Resampler } from '@purinton/resampler';
|
|
9
9
|
import * as prism from 'prism-media';
|
|
10
10
|
import { startGenAiSession } from './genai.js';
|
|
11
11
|
import { getTools } from './tools.js';
|
|
12
|
+
import { mkdir } from 'node:fs/promises';
|
|
12
13
|
import { createLogger } from './logger.js';
|
|
13
14
|
if (!parentPort) {
|
|
14
15
|
throw new Error('This module must be run as a worker thread');
|
|
@@ -98,23 +99,24 @@ async function createAssistantAudioLogStream(guildId, channelId) {
|
|
|
98
99
|
return null;
|
|
99
100
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
100
101
|
const audioDir = path.join(process.cwd(), 'discord-audio-logs', guildId, channelId);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
// Add error handler to prevent crashes
|
|
108
|
-
outputAudioStream.on('error', (error) => {
|
|
109
|
-
workerLogger.error(`Assistant audio log stream error:`, error);
|
|
110
|
-
});
|
|
111
|
-
workerLogger.log(`Created assistant audio log: ${outputFilePath}`);
|
|
112
|
-
return outputAudioStream;
|
|
113
|
-
}
|
|
114
|
-
catch (error) {
|
|
115
|
-
workerLogger.error(`Failed to create audio log directory:`, error);
|
|
102
|
+
const mkdirError = await errore.tryAsync({
|
|
103
|
+
try: () => mkdir(audioDir, { recursive: true }),
|
|
104
|
+
catch: (e) => e,
|
|
105
|
+
});
|
|
106
|
+
if (mkdirError instanceof Error) {
|
|
107
|
+
workerLogger.error(`Failed to create audio log directory:`, mkdirError.message);
|
|
116
108
|
return null;
|
|
117
109
|
}
|
|
110
|
+
// Create stream for assistant audio (24kHz mono s16le PCM)
|
|
111
|
+
const outputFileName = `assistant_${timestamp}.24.pcm`;
|
|
112
|
+
const outputFilePath = path.join(audioDir, outputFileName);
|
|
113
|
+
const outputAudioStream = createWriteStream(outputFilePath);
|
|
114
|
+
// Add error handler to prevent crashes
|
|
115
|
+
outputAudioStream.on('error', (error) => {
|
|
116
|
+
workerLogger.error(`Assistant audio log stream error:`, error);
|
|
117
|
+
});
|
|
118
|
+
workerLogger.log(`Created assistant audio log: ${outputFilePath}`);
|
|
119
|
+
return outputAudioStream;
|
|
118
120
|
}
|
|
119
121
|
// Handle encoded Opus packets
|
|
120
122
|
opusEncoder.on('data', (packet) => {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// and manages autocomplete, select menu interactions for the bot.
|
|
4
4
|
import { Events } from 'discord.js';
|
|
5
5
|
import { handleSessionCommand, handleSessionAutocomplete } from './commands/session.js';
|
|
6
|
+
import { handleNewWorktreeCommand } from './commands/worktree.js';
|
|
6
7
|
import { handleResumeCommand, handleResumeAutocomplete } from './commands/resume.js';
|
|
7
8
|
import { handleAddProjectCommand, handleAddProjectAutocomplete } from './commands/add-project.js';
|
|
8
9
|
import { handleRemoveProjectCommand, handleRemoveProjectAutocomplete, } from './commands/remove-project.js';
|
|
@@ -30,7 +31,7 @@ export function registerInteractionHandler({ discordClient, appId, }) {
|
|
|
30
31
|
: 'other'}`);
|
|
31
32
|
if (interaction.isAutocomplete()) {
|
|
32
33
|
switch (interaction.commandName) {
|
|
33
|
-
case 'session':
|
|
34
|
+
case 'new-session':
|
|
34
35
|
await handleSessionAutocomplete({ interaction, appId });
|
|
35
36
|
return;
|
|
36
37
|
case 'resume':
|
|
@@ -50,9 +51,12 @@ export function registerInteractionHandler({ discordClient, appId, }) {
|
|
|
50
51
|
if (interaction.isChatInputCommand()) {
|
|
51
52
|
interactionLogger.log(`[COMMAND] Processing: ${interaction.commandName}`);
|
|
52
53
|
switch (interaction.commandName) {
|
|
53
|
-
case 'session':
|
|
54
|
+
case 'new-session':
|
|
54
55
|
await handleSessionCommand({ command: interaction, appId });
|
|
55
56
|
return;
|
|
57
|
+
case 'new-worktree':
|
|
58
|
+
await handleNewWorktreeCommand({ command: interaction, appId });
|
|
59
|
+
return;
|
|
56
60
|
case 'resume':
|
|
57
61
|
await handleResumeCommand({ command: interaction, appId });
|
|
58
62
|
return;
|