kimaki 0.4.39 → 0.4.41
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/LICENSE +21 -0
- package/dist/cli.js +108 -51
- package/dist/commands/abort.js +1 -1
- package/dist/commands/add-project.js +2 -2
- package/dist/commands/agent.js +2 -2
- package/dist/commands/fork.js +2 -2
- package/dist/commands/model.js +2 -2
- package/dist/commands/remove-project.js +2 -2
- package/dist/commands/resume.js +2 -2
- package/dist/commands/session.js +4 -4
- package/dist/commands/share.js +1 -1
- package/dist/commands/undo-redo.js +2 -2
- package/dist/commands/worktree.js +180 -0
- package/dist/database.js +49 -1
- package/dist/discord-bot.js +29 -4
- package/dist/discord-utils.js +36 -0
- package/dist/errors.js +86 -87
- package/dist/genai-worker.js +1 -1
- package/dist/interaction-handler.js +6 -2
- package/dist/markdown.js +5 -1
- package/dist/message-formatting.js +2 -2
- package/dist/opencode.js +4 -4
- package/dist/session-handler.js +2 -2
- package/dist/tools.js +3 -3
- package/dist/voice-handler.js +3 -3
- package/dist/voice.js +4 -4
- package/package.json +16 -16
- package/src/cli.ts +166 -85
- package/src/commands/abort.ts +1 -1
- package/src/commands/add-project.ts +2 -2
- package/src/commands/agent.ts +2 -2
- package/src/commands/fork.ts +2 -2
- package/src/commands/model.ts +2 -2
- package/src/commands/remove-project.ts +2 -2
- package/src/commands/resume.ts +2 -2
- package/src/commands/session.ts +4 -4
- package/src/commands/share.ts +1 -1
- package/src/commands/undo-redo.ts +2 -2
- package/src/commands/worktree.ts +243 -0
- package/src/database.ts +96 -1
- package/src/discord-bot.ts +30 -4
- package/src/discord-utils.ts +50 -0
- package/src/errors.ts +90 -160
- package/src/genai-worker.ts +1 -1
- package/src/interaction-handler.ts +7 -2
- package/src/markdown.ts +5 -4
- package/src/message-formatting.ts +2 -2
- package/src/opencode.ts +4 -4
- package/src/session-handler.ts +2 -2
- package/src/tools.ts +3 -3
- package/src/voice-handler.ts +3 -3
- package/src/voice.ts +4 -4
package/dist/database.js
CHANGED
|
@@ -18,7 +18,7 @@ export function getDatabase() {
|
|
|
18
18
|
},
|
|
19
19
|
catch: (e) => e,
|
|
20
20
|
});
|
|
21
|
-
if (
|
|
21
|
+
if (mkdirError instanceof Error) {
|
|
22
22
|
dbLogger.error(`Failed to create data directory ${dataDir}:`, mkdirError.message);
|
|
23
23
|
}
|
|
24
24
|
const dbPath = path.join(dataDir, 'discord-sessions.db');
|
|
@@ -75,6 +75,19 @@ export function getDatabase() {
|
|
|
75
75
|
xai_api_key TEXT,
|
|
76
76
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
77
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
|
+
)
|
|
78
91
|
`);
|
|
79
92
|
runModelMigrations(db);
|
|
80
93
|
}
|
|
@@ -202,6 +215,41 @@ export function setSessionAgent(sessionId, agentName) {
|
|
|
202
215
|
const db = getDatabase();
|
|
203
216
|
db.prepare(`INSERT OR REPLACE INTO session_agents (session_id, agent_name) VALUES (?, ?)`).run(sessionId, agentName);
|
|
204
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
|
+
}
|
|
205
253
|
export function closeDatabase() {
|
|
206
254
|
if (db) {
|
|
207
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';
|
|
@@ -22,7 +22,10 @@ import * as errore from 'errore';
|
|
|
22
22
|
import { extractTagsArrays } from './xml.js';
|
|
23
23
|
import { createLogger } from './logger.js';
|
|
24
24
|
import { setGlobalDispatcher, Agent } from 'undici';
|
|
25
|
-
|
|
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 }));
|
|
26
29
|
const discordLogger = createLogger('DISCORD');
|
|
27
30
|
const voiceLogger = createLogger('VOICE');
|
|
28
31
|
export async function createDiscordClient() {
|
|
@@ -94,7 +97,7 @@ export async function startDiscordBot({ token, appId, discordClient, }) {
|
|
|
94
97
|
try: () => message.fetch(),
|
|
95
98
|
catch: (e) => e,
|
|
96
99
|
});
|
|
97
|
-
if (
|
|
100
|
+
if (fetched instanceof Error) {
|
|
98
101
|
discordLogger.log(`Failed to fetch partial message ${message.id}:`, fetched.message);
|
|
99
102
|
return;
|
|
100
103
|
}
|
|
@@ -132,6 +135,28 @@ export async function startDiscordBot({ token, appId, discordClient, }) {
|
|
|
132
135
|
projectDirectory = extracted['kimaki.directory']?.[0]?.trim();
|
|
133
136
|
channelAppId = extracted['kimaki.app']?.[0]?.trim();
|
|
134
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
|
+
}
|
|
135
160
|
if (channelAppId && channelAppId !== currentAppId) {
|
|
136
161
|
voiceLogger.log(`[IGNORED] Thread belongs to different bot app (expected: ${currentAppId}, got: ${channelAppId})`);
|
|
137
162
|
return;
|
|
@@ -175,7 +200,7 @@ export async function startDiscordBot({ token, appId, discordClient, }) {
|
|
|
175
200
|
if (projectDirectory) {
|
|
176
201
|
try {
|
|
177
202
|
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
178
|
-
if (
|
|
203
|
+
if (getClient instanceof Error) {
|
|
179
204
|
voiceLogger.error(`[SESSION] Failed to initialize OpenCode client:`, getClient.message);
|
|
180
205
|
throw new Error(getClient.message);
|
|
181
206
|
}
|
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
CHANGED
|
@@ -1,110 +1,109 @@
|
|
|
1
1
|
// TaggedError definitions for type-safe error handling with errore.
|
|
2
2
|
// Errors are grouped by category: infrastructure, domain, and validation.
|
|
3
3
|
// Use errore.matchError() for exhaustive error handling in command handlers.
|
|
4
|
-
import
|
|
4
|
+
import { createTaggedError } from 'errore';
|
|
5
5
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
6
6
|
// INFRASTRUCTURE ERRORS - Server, filesystem, external services
|
|
7
7
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
8
|
-
export class DirectoryNotAccessibleError extends
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
export class ServerStartError extends
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
export class ServerNotFoundError extends
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
export class ServerNotReadyError extends
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
constructor(args) {
|
|
33
|
-
super({ ...args, message: `${args.service} API key is required` });
|
|
34
|
-
}
|
|
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
|
+
}) {
|
|
35
32
|
}
|
|
36
33
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
37
34
|
// DOMAIN ERRORS - Sessions, messages, transcription
|
|
38
35
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
39
|
-
export class SessionNotFoundError extends
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
export class SessionCreateError extends
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
65
|
}
|
|
66
66
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
67
67
|
// VALIDATION ERRORS - Input validation, format checks
|
|
68
68
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
69
|
-
export class InvalidAudioFormatError extends
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
export class EmptyTranscriptionError extends
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
export class NoResponseContentError extends
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
export class NoToolResponseError extends
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
88
|
}
|
|
89
89
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
90
90
|
// NETWORK ERRORS - Fetch and HTTP
|
|
91
91
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
92
|
-
export class FetchError extends
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
92
|
+
export class FetchError extends createTaggedError({
|
|
93
|
+
name: 'FetchError',
|
|
94
|
+
message: 'Fetch failed for $url',
|
|
95
|
+
}) {
|
|
97
96
|
}
|
|
98
97
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
99
98
|
// API ERRORS - External service responses
|
|
100
99
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
101
|
-
export class DiscordApiError extends
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
export class OpenCodeApiError extends
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
+
}) {
|
|
110
109
|
}
|
package/dist/genai-worker.js
CHANGED
|
@@ -103,7 +103,7 @@ async function createAssistantAudioLogStream(guildId, channelId) {
|
|
|
103
103
|
try: () => mkdir(audioDir, { recursive: true }),
|
|
104
104
|
catch: (e) => e,
|
|
105
105
|
});
|
|
106
|
-
if (
|
|
106
|
+
if (mkdirError instanceof Error) {
|
|
107
107
|
workerLogger.error(`Failed to create audio log directory:`, mkdirError.message);
|
|
108
108
|
return null;
|
|
109
109
|
}
|
|
@@ -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;
|
package/dist/markdown.js
CHANGED
|
@@ -3,13 +3,17 @@
|
|
|
3
3
|
// user messages, assistant responses, tool calls, and reasoning blocks.
|
|
4
4
|
// Uses errore for type-safe error handling.
|
|
5
5
|
import * as errore from 'errore';
|
|
6
|
+
import { createTaggedError } from 'errore';
|
|
6
7
|
import * as yaml from 'js-yaml';
|
|
7
8
|
import { formatDateTime } from './utils.js';
|
|
8
9
|
import { extractNonXmlContent } from './xml.js';
|
|
9
10
|
import { createLogger } from './logger.js';
|
|
10
11
|
import { SessionNotFoundError, MessagesNotFoundError } from './errors.js';
|
|
11
12
|
// Generic error for unexpected exceptions in async operations
|
|
12
|
-
class UnexpectedError extends
|
|
13
|
+
class UnexpectedError extends createTaggedError({
|
|
14
|
+
name: 'UnexpectedError',
|
|
15
|
+
message: '$message',
|
|
16
|
+
}) {
|
|
13
17
|
}
|
|
14
18
|
const markdownLogger = createLogger('MARKDOWN');
|
|
15
19
|
export class ShareMarkdown {
|
|
@@ -62,7 +62,7 @@ export async function getTextAttachments(message) {
|
|
|
62
62
|
try: () => fetch(attachment.url),
|
|
63
63
|
catch: (e) => new FetchError({ url: attachment.url, cause: e }),
|
|
64
64
|
});
|
|
65
|
-
if (
|
|
65
|
+
if (response instanceof Error) {
|
|
66
66
|
return `<attachment filename="${attachment.name}" error="${response.message}" />`;
|
|
67
67
|
}
|
|
68
68
|
if (!response.ok) {
|
|
@@ -90,7 +90,7 @@ export async function getFileAttachments(message) {
|
|
|
90
90
|
try: () => fetch(attachment.url),
|
|
91
91
|
catch: (e) => new FetchError({ url: attachment.url, cause: e }),
|
|
92
92
|
});
|
|
93
|
-
if (
|
|
93
|
+
if (response instanceof Error) {
|
|
94
94
|
logger.error(`Error downloading attachment ${attachment.name}:`, response.message);
|
|
95
95
|
return null;
|
|
96
96
|
}
|
package/dist/opencode.js
CHANGED
|
@@ -43,7 +43,7 @@ async function waitForServer(port, maxAttempts = 30) {
|
|
|
43
43
|
try: () => fetch(endpoint),
|
|
44
44
|
catch: (e) => new FetchError({ url: endpoint, cause: e }),
|
|
45
45
|
});
|
|
46
|
-
if (
|
|
46
|
+
if (response instanceof Error) {
|
|
47
47
|
// Connection refused or other transient errors - continue polling
|
|
48
48
|
opencodeLogger.debug(`Server polling attempt failed: ${response.message}`);
|
|
49
49
|
continue;
|
|
@@ -80,7 +80,7 @@ export async function initializeOpencodeForDirectory(directory) {
|
|
|
80
80
|
},
|
|
81
81
|
catch: () => new DirectoryNotAccessibleError({ directory }),
|
|
82
82
|
});
|
|
83
|
-
if (
|
|
83
|
+
if (accessCheck instanceof Error) {
|
|
84
84
|
return accessCheck;
|
|
85
85
|
}
|
|
86
86
|
const port = await getOpenPort();
|
|
@@ -125,7 +125,7 @@ export async function initializeOpencodeForDirectory(directory) {
|
|
|
125
125
|
serverRetryCount.set(directory, retryCount + 1);
|
|
126
126
|
opencodeLogger.log(`Restarting server for directory: ${directory} (attempt ${retryCount + 1}/5)`);
|
|
127
127
|
initializeOpencodeForDirectory(directory).then((result) => {
|
|
128
|
-
if (
|
|
128
|
+
if (result instanceof Error) {
|
|
129
129
|
opencodeLogger.error(`Failed to restart opencode server:`, result);
|
|
130
130
|
}
|
|
131
131
|
});
|
|
@@ -139,7 +139,7 @@ export async function initializeOpencodeForDirectory(directory) {
|
|
|
139
139
|
}
|
|
140
140
|
});
|
|
141
141
|
const waitResult = await waitForServer(port);
|
|
142
|
-
if (
|
|
142
|
+
if (waitResult instanceof Error) {
|
|
143
143
|
// Dump buffered logs on failure
|
|
144
144
|
opencodeLogger.error(`Server failed to start for ${directory}:`);
|
|
145
145
|
for (const line of logBuffer) {
|
package/dist/session-handler.js
CHANGED
|
@@ -49,7 +49,7 @@ export async function abortAndRetrySession({ sessionId, thread, projectDirectory
|
|
|
49
49
|
controller.abort('model-change');
|
|
50
50
|
// Also call the API abort endpoint
|
|
51
51
|
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
52
|
-
if (
|
|
52
|
+
if (getClient instanceof Error) {
|
|
53
53
|
sessionLogger.error(`[ABORT+RETRY] Failed to initialize OpenCode client:`, getClient.message);
|
|
54
54
|
return false;
|
|
55
55
|
}
|
|
@@ -98,7 +98,7 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
98
98
|
const directory = projectDirectory || process.cwd();
|
|
99
99
|
sessionLogger.log(`Using directory: ${directory}`);
|
|
100
100
|
const getClient = await initializeOpencodeForDirectory(directory);
|
|
101
|
-
if (
|
|
101
|
+
if (getClient instanceof Error) {
|
|
102
102
|
await sendThreadMessage(thread, `✗ ${getClient.message}`);
|
|
103
103
|
return;
|
|
104
104
|
}
|
package/dist/tools.js
CHANGED
|
@@ -15,7 +15,7 @@ import pc from 'picocolors';
|
|
|
15
15
|
import { initializeOpencodeForDirectory, getOpencodeSystemMessage } from './discord-bot.js';
|
|
16
16
|
export async function getTools({ onMessageCompleted, directory, }) {
|
|
17
17
|
const getClient = await initializeOpencodeForDirectory(directory);
|
|
18
|
-
if (
|
|
18
|
+
if (getClient instanceof Error) {
|
|
19
19
|
throw new Error(getClient.message);
|
|
20
20
|
}
|
|
21
21
|
const client = getClient();
|
|
@@ -248,7 +248,7 @@ export async function getTools({ onMessageCompleted, directory, }) {
|
|
|
248
248
|
sessionID: sessionId,
|
|
249
249
|
lastAssistantOnly: true,
|
|
250
250
|
});
|
|
251
|
-
if (
|
|
251
|
+
if (markdownResult instanceof Error) {
|
|
252
252
|
throw new Error(markdownResult.message);
|
|
253
253
|
}
|
|
254
254
|
return {
|
|
@@ -261,7 +261,7 @@ export async function getTools({ onMessageCompleted, directory, }) {
|
|
|
261
261
|
const markdownResult = await markdownRenderer.generate({
|
|
262
262
|
sessionID: sessionId,
|
|
263
263
|
});
|
|
264
|
-
if (
|
|
264
|
+
if (markdownResult instanceof Error) {
|
|
265
265
|
throw new Error(markdownResult.message);
|
|
266
266
|
}
|
|
267
267
|
const messages = await getClient().session.messages({
|
package/dist/voice-handler.js
CHANGED
|
@@ -326,7 +326,7 @@ export async function processVoiceAttachment({ message, thread, projectDirectory
|
|
|
326
326
|
try: () => fetch(audioAttachment.url),
|
|
327
327
|
catch: (e) => new FetchError({ url: audioAttachment.url, cause: e }),
|
|
328
328
|
});
|
|
329
|
-
if (
|
|
329
|
+
if (audioResponse instanceof Error) {
|
|
330
330
|
voiceLogger.error(`Failed to download audio attachment:`, audioResponse.message);
|
|
331
331
|
await sendThreadMessage(thread, `⚠️ Failed to download audio: ${audioResponse.message}`);
|
|
332
332
|
return null;
|
|
@@ -367,7 +367,7 @@ export async function processVoiceAttachment({ message, thread, projectDirectory
|
|
|
367
367
|
currentSessionContext,
|
|
368
368
|
lastSessionContext,
|
|
369
369
|
});
|
|
370
|
-
if (
|
|
370
|
+
if (transcription instanceof Error) {
|
|
371
371
|
const errMsg = errore.matchError(transcription, {
|
|
372
372
|
ApiKeyMissingError: (e) => e.message,
|
|
373
373
|
InvalidAudioFormatError: (e) => e.message,
|
|
@@ -398,7 +398,7 @@ export async function processVoiceAttachment({ message, thread, projectDirectory
|
|
|
398
398
|
if (renamed === null) {
|
|
399
399
|
voiceLogger.log(`Thread name update timed out`);
|
|
400
400
|
}
|
|
401
|
-
else if (
|
|
401
|
+
else if (renamed instanceof Error) {
|
|
402
402
|
voiceLogger.log(`Could not update thread name:`, renamed.message);
|
|
403
403
|
}
|
|
404
404
|
else {
|
package/dist/voice.js
CHANGED
|
@@ -103,7 +103,7 @@ function createToolRunner({ directory }) {
|
|
|
103
103
|
voiceLogger.log(`Grep search: "${pattern}"`);
|
|
104
104
|
const result = await runGrep({ pattern, directory });
|
|
105
105
|
const output = (() => {
|
|
106
|
-
if (
|
|
106
|
+
if (result instanceof Error) {
|
|
107
107
|
voiceLogger.error('grep search failed:', result);
|
|
108
108
|
return 'grep search failed';
|
|
109
109
|
}
|
|
@@ -117,7 +117,7 @@ function createToolRunner({ directory }) {
|
|
|
117
117
|
voiceLogger.log(`Glob search: "${pattern}"`);
|
|
118
118
|
const result = await runGlob({ pattern, directory });
|
|
119
119
|
const output = (() => {
|
|
120
|
-
if (
|
|
120
|
+
if (result instanceof Error) {
|
|
121
121
|
voiceLogger.error('glob search failed:', result);
|
|
122
122
|
return 'glob search failed';
|
|
123
123
|
}
|
|
@@ -145,7 +145,7 @@ export async function runTranscriptionLoop({ genAI, model, initialContents, tool
|
|
|
145
145
|
}),
|
|
146
146
|
catch: (e) => new TranscriptionError({ reason: `API call failed: ${String(e)}`, cause: e }),
|
|
147
147
|
});
|
|
148
|
-
if (
|
|
148
|
+
if (initialResponse instanceof Error) {
|
|
149
149
|
return initialResponse;
|
|
150
150
|
}
|
|
151
151
|
let response = initialResponse;
|
|
@@ -233,7 +233,7 @@ export async function runTranscriptionLoop({ genAI, model, initialContents, tool
|
|
|
233
233
|
}),
|
|
234
234
|
catch: (e) => new TranscriptionError({ reason: `API call failed: ${String(e)}`, cause: e }),
|
|
235
235
|
});
|
|
236
|
-
if (
|
|
236
|
+
if (nextResponse instanceof Error) {
|
|
237
237
|
return nextResponse;
|
|
238
238
|
}
|
|
239
239
|
response = nextResponse;
|