kimaki 0.4.37 → 0.4.39
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/channel-management.js +6 -2
- package/dist/cli.js +41 -15
- package/dist/commands/abort.js +15 -6
- package/dist/commands/add-project.js +9 -0
- package/dist/commands/agent.js +114 -20
- 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 +13 -0
- package/dist/commands/share.js +10 -1
- package/dist/commands/undo-redo.js +13 -4
- package/dist/database.js +24 -5
- package/dist/discord-bot.js +38 -31
- package/dist/errors.js +110 -0
- package/dist/genai-worker.js +18 -16
- package/dist/interaction-handler.js +6 -1
- package/dist/markdown.js +96 -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 +136 -8
- 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 +2 -1
- package/src/channel-management.ts +6 -2
- package/src/cli.ts +67 -19
- package/src/commands/abort.ts +17 -7
- package/src/commands/add-project.ts +9 -0
- package/src/commands/agent.ts +160 -25
- 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 +13 -0
- package/src/commands/share.ts +11 -1
- package/src/commands/undo-redo.ts +15 -6
- package/src/database.ts +26 -4
- package/src/discord-bot.ts +42 -34
- package/src/errors.ts +208 -0
- package/src/genai-worker.ts +20 -17
- package/src/interaction-handler.ts +7 -1
- package/src/markdown.test.ts +13 -3
- package/src/markdown.ts +111 -95
- package/src/message-formatting.ts +55 -38
- package/src/opencode.ts +52 -49
- package/src/session-handler.ts +164 -11
- 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/voice.js
CHANGED
|
@@ -1,49 +1,51 @@
|
|
|
1
1
|
// Audio transcription service using Google Gemini.
|
|
2
2
|
// Transcribes voice messages with code-aware context, using grep/glob tools
|
|
3
3
|
// to verify technical terms, filenames, and function names in the codebase.
|
|
4
|
+
// Uses errore for type-safe error handling.
|
|
4
5
|
import { GoogleGenAI, Type } from '@google/genai';
|
|
6
|
+
import * as errore from 'errore';
|
|
5
7
|
import { createLogger } from './logger.js';
|
|
6
8
|
import { glob } from 'glob';
|
|
7
9
|
import { ripGrep } from 'ripgrep-js';
|
|
10
|
+
import { ApiKeyMissingError, InvalidAudioFormatError, TranscriptionError, EmptyTranscriptionError, NoResponseContentError, NoToolResponseError, GrepSearchError, GlobSearchError, } from './errors.js';
|
|
8
11
|
const voiceLogger = createLogger('VOICE');
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
12
|
+
function runGrep({ pattern, directory }) {
|
|
13
|
+
return errore.tryAsync({
|
|
14
|
+
try: async () => {
|
|
15
|
+
const results = await ripGrep(directory, {
|
|
16
|
+
string: pattern,
|
|
17
|
+
globs: ['!node_modules/**', '!.git/**', '!dist/**', '!build/**'],
|
|
18
|
+
});
|
|
19
|
+
if (results.length === 0) {
|
|
20
|
+
return 'No matches found';
|
|
21
|
+
}
|
|
22
|
+
const output = results
|
|
23
|
+
.slice(0, 10)
|
|
24
|
+
.map((match) => {
|
|
25
|
+
return `${match.path.text}:${match.line_number}: ${match.lines.text.trim()}`;
|
|
26
|
+
})
|
|
27
|
+
.join('\n');
|
|
28
|
+
return output.slice(0, 2000);
|
|
29
|
+
},
|
|
30
|
+
catch: (e) => new GrepSearchError({ pattern, cause: e }),
|
|
31
|
+
});
|
|
30
32
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
33
|
+
function runGlob({ pattern, directory }) {
|
|
34
|
+
return errore.tryAsync({
|
|
35
|
+
try: async () => {
|
|
36
|
+
const files = await glob(pattern, {
|
|
37
|
+
cwd: directory,
|
|
38
|
+
nodir: false,
|
|
39
|
+
ignore: ['node_modules/**', '.git/**', 'dist/**', 'build/**'],
|
|
40
|
+
maxDepth: 10,
|
|
41
|
+
});
|
|
42
|
+
if (files.length === 0) {
|
|
43
|
+
return 'No files found';
|
|
44
|
+
}
|
|
45
|
+
return files.slice(0, 30).join('\n');
|
|
46
|
+
},
|
|
47
|
+
catch: (e) => new GlobSearchError({ pattern, cause: e }),
|
|
48
|
+
});
|
|
47
49
|
}
|
|
48
50
|
const grepToolDeclaration = {
|
|
49
51
|
name: 'grep',
|
|
@@ -99,14 +101,28 @@ function createToolRunner({ directory }) {
|
|
|
99
101
|
if (name === 'grep' && hasDirectory) {
|
|
100
102
|
const pattern = args?.pattern || '';
|
|
101
103
|
voiceLogger.log(`Grep search: "${pattern}"`);
|
|
102
|
-
const
|
|
104
|
+
const result = await runGrep({ pattern, directory });
|
|
105
|
+
const output = (() => {
|
|
106
|
+
if (errore.isError(result)) {
|
|
107
|
+
voiceLogger.error('grep search failed:', result);
|
|
108
|
+
return 'grep search failed';
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
111
|
+
})();
|
|
103
112
|
voiceLogger.log(`Grep result: ${output.slice(0, 100)}...`);
|
|
104
113
|
return { type: 'toolResponse', name: 'grep', output };
|
|
105
114
|
}
|
|
106
115
|
if (name === 'glob' && hasDirectory) {
|
|
107
116
|
const pattern = args?.pattern || '';
|
|
108
117
|
voiceLogger.log(`Glob search: "${pattern}"`);
|
|
109
|
-
const
|
|
118
|
+
const result = await runGlob({ pattern, directory });
|
|
119
|
+
const output = (() => {
|
|
120
|
+
if (errore.isError(result)) {
|
|
121
|
+
voiceLogger.error('glob search failed:', result);
|
|
122
|
+
return 'glob search failed';
|
|
123
|
+
}
|
|
124
|
+
return result;
|
|
125
|
+
})();
|
|
110
126
|
voiceLogger.log(`Glob result: ${output.slice(0, 100)}...`);
|
|
111
127
|
return { type: 'toolResponse', name: 'glob', output };
|
|
112
128
|
}
|
|
@@ -114,17 +130,25 @@ function createToolRunner({ directory }) {
|
|
|
114
130
|
};
|
|
115
131
|
}
|
|
116
132
|
export async function runTranscriptionLoop({ genAI, model, initialContents, tools, temperature, toolRunner, maxSteps = 10, }) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
133
|
+
// Wrap external API call that can throw
|
|
134
|
+
const initialResponse = await errore.tryAsync({
|
|
135
|
+
try: () => genAI.models.generateContent({
|
|
136
|
+
model,
|
|
137
|
+
contents: initialContents,
|
|
138
|
+
config: {
|
|
139
|
+
temperature,
|
|
140
|
+
thinkingConfig: {
|
|
141
|
+
thinkingBudget: 1024,
|
|
142
|
+
},
|
|
143
|
+
tools,
|
|
124
144
|
},
|
|
125
|
-
|
|
126
|
-
},
|
|
145
|
+
}),
|
|
146
|
+
catch: (e) => new TranscriptionError({ reason: `API call failed: ${String(e)}`, cause: e }),
|
|
127
147
|
});
|
|
148
|
+
if (errore.isError(initialResponse)) {
|
|
149
|
+
return initialResponse;
|
|
150
|
+
}
|
|
151
|
+
let response = initialResponse;
|
|
128
152
|
const conversationHistory = [...initialContents];
|
|
129
153
|
let stepsRemaining = maxSteps;
|
|
130
154
|
while (true) {
|
|
@@ -135,7 +159,7 @@ export async function runTranscriptionLoop({ genAI, model, initialContents, tool
|
|
|
135
159
|
voiceLogger.log(`No parts but got text response: "${text.slice(0, 100)}..."`);
|
|
136
160
|
return text;
|
|
137
161
|
}
|
|
138
|
-
|
|
162
|
+
return new NoResponseContentError();
|
|
139
163
|
}
|
|
140
164
|
const functionCalls = candidate.content.parts.filter((part) => 'functionCall' in part && !!part.functionCall);
|
|
141
165
|
if (functionCalls.length === 0) {
|
|
@@ -144,7 +168,7 @@ export async function runTranscriptionLoop({ genAI, model, initialContents, tool
|
|
|
144
168
|
voiceLogger.log(`No function calls but got text: "${text.slice(0, 100)}..."`);
|
|
145
169
|
return text;
|
|
146
170
|
}
|
|
147
|
-
|
|
171
|
+
return new TranscriptionError({ reason: 'Model did not produce a transcription' });
|
|
148
172
|
}
|
|
149
173
|
conversationHistory.push({
|
|
150
174
|
role: 'model',
|
|
@@ -159,7 +183,7 @@ export async function runTranscriptionLoop({ genAI, model, initialContents, tool
|
|
|
159
183
|
const transcription = result.transcription?.trim() || '';
|
|
160
184
|
voiceLogger.log(`Transcription result received: "${transcription.slice(0, 100)}..."`);
|
|
161
185
|
if (!transcription) {
|
|
162
|
-
|
|
186
|
+
return new EmptyTranscriptionError();
|
|
163
187
|
}
|
|
164
188
|
return transcription;
|
|
165
189
|
}
|
|
@@ -186,67 +210,76 @@ export async function runTranscriptionLoop({ genAI, model, initialContents, tool
|
|
|
186
210
|
}
|
|
187
211
|
}
|
|
188
212
|
if (functionResponseParts.length === 0) {
|
|
189
|
-
|
|
213
|
+
return new NoToolResponseError();
|
|
190
214
|
}
|
|
191
215
|
conversationHistory.push({
|
|
192
216
|
role: 'user',
|
|
193
217
|
parts: functionResponseParts,
|
|
194
218
|
});
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
219
|
+
// Wrap external API call that can throw
|
|
220
|
+
const nextResponse = await errore.tryAsync({
|
|
221
|
+
try: () => genAI.models.generateContent({
|
|
222
|
+
model,
|
|
223
|
+
contents: conversationHistory,
|
|
224
|
+
config: {
|
|
225
|
+
temperature,
|
|
226
|
+
thinkingConfig: {
|
|
227
|
+
thinkingBudget: 512,
|
|
228
|
+
},
|
|
229
|
+
tools: stepsRemaining <= 0
|
|
230
|
+
? [{ functionDeclarations: [transcriptionResultToolDeclaration] }]
|
|
231
|
+
: tools,
|
|
202
232
|
},
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
: tools,
|
|
206
|
-
},
|
|
233
|
+
}),
|
|
234
|
+
catch: (e) => new TranscriptionError({ reason: `API call failed: ${String(e)}`, cause: e }),
|
|
207
235
|
});
|
|
236
|
+
if (errore.isError(nextResponse)) {
|
|
237
|
+
return nextResponse;
|
|
238
|
+
}
|
|
239
|
+
response = nextResponse;
|
|
208
240
|
}
|
|
209
241
|
}
|
|
210
|
-
export
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
let audioBase64;
|
|
242
|
+
export function transcribeAudio({ audio, prompt, language, temperature, geminiApiKey, directory, currentSessionContext, lastSessionContext, }) {
|
|
243
|
+
const apiKey = geminiApiKey || process.env.GEMINI_API_KEY;
|
|
244
|
+
if (!apiKey) {
|
|
245
|
+
return Promise.resolve(new ApiKeyMissingError({ service: 'Gemini' }));
|
|
246
|
+
}
|
|
247
|
+
const genAI = new GoogleGenAI({ apiKey });
|
|
248
|
+
const audioBase64 = (() => {
|
|
218
249
|
if (typeof audio === 'string') {
|
|
219
|
-
|
|
220
|
-
}
|
|
221
|
-
else if (audio instanceof Buffer) {
|
|
222
|
-
audioBase64 = audio.toString('base64');
|
|
250
|
+
return audio;
|
|
223
251
|
}
|
|
224
|
-
|
|
225
|
-
|
|
252
|
+
if (audio instanceof Buffer) {
|
|
253
|
+
return audio.toString('base64');
|
|
226
254
|
}
|
|
227
|
-
|
|
228
|
-
|
|
255
|
+
if (audio instanceof Uint8Array) {
|
|
256
|
+
return Buffer.from(audio).toString('base64');
|
|
229
257
|
}
|
|
230
|
-
|
|
231
|
-
|
|
258
|
+
if (audio instanceof ArrayBuffer) {
|
|
259
|
+
return Buffer.from(audio).toString('base64');
|
|
232
260
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
261
|
+
return '';
|
|
262
|
+
})();
|
|
263
|
+
if (!audioBase64) {
|
|
264
|
+
return Promise.resolve(new InvalidAudioFormatError());
|
|
265
|
+
}
|
|
266
|
+
const languageHint = language ? `The audio is in ${language}.\n\n` : '';
|
|
267
|
+
// build session context section
|
|
268
|
+
const sessionContextParts = [];
|
|
269
|
+
if (lastSessionContext) {
|
|
270
|
+
sessionContextParts.push(`<last_session>
|
|
238
271
|
${lastSessionContext}
|
|
239
272
|
</last_session>`);
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
273
|
+
}
|
|
274
|
+
if (currentSessionContext) {
|
|
275
|
+
sessionContextParts.push(`<current_session>
|
|
243
276
|
${currentSessionContext}
|
|
244
277
|
</current_session>`);
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
278
|
+
}
|
|
279
|
+
const sessionContextSection = sessionContextParts.length > 0
|
|
280
|
+
? `\nSession context (use to understand references to files, functions, tools used):\n${sessionContextParts.join('\n\n')}`
|
|
281
|
+
: '';
|
|
282
|
+
const transcriptionPrompt = `${languageHint}Transcribe this audio for a coding agent (like Claude Code or OpenCode).
|
|
250
283
|
|
|
251
284
|
CRITICAL REQUIREMENT: You MUST call the "transcriptionResult" tool to complete this task.
|
|
252
285
|
- The transcriptionResult tool is the ONLY way to return results
|
|
@@ -275,42 +308,37 @@ ${sessionContextSection}
|
|
|
275
308
|
REMEMBER: Call "transcriptionResult" tool with your transcription. This is mandatory.
|
|
276
309
|
|
|
277
310
|
Note: "critique" is a CLI tool for showing diffs in the browser.`;
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
},
|
|
311
|
+
// const hasDirectory = directory && directory.trim().length > 0
|
|
312
|
+
const tools = [
|
|
313
|
+
{
|
|
314
|
+
functionDeclarations: [
|
|
315
|
+
transcriptionResultToolDeclaration,
|
|
316
|
+
// grep/glob disabled - was causing transcription to hang
|
|
317
|
+
// ...(hasDirectory ? [grepToolDeclaration, globToolDeclaration] : []),
|
|
318
|
+
],
|
|
319
|
+
},
|
|
320
|
+
];
|
|
321
|
+
const initialContents = [
|
|
322
|
+
{
|
|
323
|
+
role: 'user',
|
|
324
|
+
parts: [
|
|
325
|
+
{ text: transcriptionPrompt },
|
|
326
|
+
{
|
|
327
|
+
inlineData: {
|
|
328
|
+
data: audioBase64,
|
|
329
|
+
mimeType: 'audio/mpeg',
|
|
298
330
|
},
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
312
|
-
catch (error) {
|
|
313
|
-
voiceLogger.error('Failed to transcribe audio:', error);
|
|
314
|
-
throw new Error(`Audio transcription failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
315
|
-
}
|
|
331
|
+
},
|
|
332
|
+
],
|
|
333
|
+
},
|
|
334
|
+
];
|
|
335
|
+
const toolRunner = createToolRunner({ directory });
|
|
336
|
+
return runTranscriptionLoop({
|
|
337
|
+
genAI,
|
|
338
|
+
model: 'gemini-2.5-flash',
|
|
339
|
+
initialContents,
|
|
340
|
+
tools,
|
|
341
|
+
temperature: temperature ?? 0.3,
|
|
342
|
+
toolRunner,
|
|
343
|
+
});
|
|
316
344
|
}
|
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.39",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "tsx --env-file .env src/cli.ts",
|
|
8
8
|
"prepublishOnly": "pnpm tsc",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"cac": "^6.7.14",
|
|
42
42
|
"discord.js": "^14.16.3",
|
|
43
43
|
"domhandler": "^5.0.3",
|
|
44
|
+
"errore": "^0.5.2",
|
|
44
45
|
"glob": "^13.0.0",
|
|
45
46
|
"htmlparser2": "^10.0.0",
|
|
46
47
|
"js-yaml": "^4.1.0",
|
|
@@ -11,7 +11,9 @@ export async function ensureKimakiCategory(
|
|
|
11
11
|
guild: Guild,
|
|
12
12
|
botName?: string,
|
|
13
13
|
): Promise<CategoryChannel> {
|
|
14
|
-
|
|
14
|
+
// Skip appending bot name if it's already "kimaki" to avoid "Kimaki kimaki"
|
|
15
|
+
const isKimakiBot = botName?.toLowerCase() === 'kimaki'
|
|
16
|
+
const categoryName = botName && !isKimakiBot ? `Kimaki ${botName}` : 'Kimaki'
|
|
15
17
|
|
|
16
18
|
const existingCategory = guild.channels.cache.find((channel): channel is CategoryChannel => {
|
|
17
19
|
if (channel.type !== ChannelType.GuildCategory) {
|
|
@@ -35,7 +37,9 @@ export async function ensureKimakiAudioCategory(
|
|
|
35
37
|
guild: Guild,
|
|
36
38
|
botName?: string,
|
|
37
39
|
): Promise<CategoryChannel> {
|
|
38
|
-
|
|
40
|
+
// Skip appending bot name if it's already "kimaki" to avoid "Kimaki Audio kimaki"
|
|
41
|
+
const isKimakiBot = botName?.toLowerCase() === 'kimaki'
|
|
42
|
+
const categoryName = botName && !isKimakiBot ? `Kimaki Audio ${botName}` : 'Kimaki Audio'
|
|
39
43
|
|
|
40
44
|
const existingCategory = guild.channels.cache.find((channel): channel is CategoryChannel => {
|
|
41
45
|
if (channel.type !== ChannelType.GuildCategory) {
|
package/src/cli.ts
CHANGED
|
@@ -40,12 +40,14 @@ import {
|
|
|
40
40
|
} from 'discord.js'
|
|
41
41
|
import path from 'node:path'
|
|
42
42
|
import fs from 'node:fs'
|
|
43
|
+
import * as errore from 'errore'
|
|
43
44
|
|
|
44
45
|
import { createLogger } from './logger.js'
|
|
45
46
|
import { spawn, spawnSync, execSync, type ExecSyncOptions } from 'node:child_process'
|
|
46
47
|
import http from 'node:http'
|
|
47
48
|
import { setDataDir, getDataDir, getLockPort } from './config.js'
|
|
48
49
|
import { extractTagsArrays } from './xml.js'
|
|
50
|
+
import { sanitizeAgentName } from './commands/agent.js'
|
|
49
51
|
|
|
50
52
|
const cliLogger = createLogger('CLI')
|
|
51
53
|
const cli = cac('kimaki')
|
|
@@ -176,11 +178,24 @@ type CliOptions = {
|
|
|
176
178
|
// Commands to skip when registering user commands (reserved names)
|
|
177
179
|
const SKIP_USER_COMMANDS = ['init']
|
|
178
180
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
181
|
+
type AgentInfo = {
|
|
182
|
+
name: string
|
|
183
|
+
description?: string
|
|
184
|
+
mode: string
|
|
185
|
+
hidden?: boolean
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function registerCommands({
|
|
189
|
+
token,
|
|
190
|
+
appId,
|
|
191
|
+
userCommands = [],
|
|
192
|
+
agents = [],
|
|
193
|
+
}: {
|
|
194
|
+
token: string
|
|
195
|
+
appId: string
|
|
196
|
+
userCommands?: OpencodeCommand[]
|
|
197
|
+
agents?: AgentInfo[]
|
|
198
|
+
}) {
|
|
184
199
|
const commands = [
|
|
185
200
|
new SlashCommandBuilder()
|
|
186
201
|
.setName('resume')
|
|
@@ -329,6 +344,24 @@ async function registerCommands(
|
|
|
329
344
|
)
|
|
330
345
|
}
|
|
331
346
|
|
|
347
|
+
// Add agent-specific quick commands like /plan-agent, /build-agent
|
|
348
|
+
// Filter to primary/all mode agents (same as /agent command shows), excluding hidden agents
|
|
349
|
+
const primaryAgents = agents.filter(
|
|
350
|
+
(a) => (a.mode === 'primary' || a.mode === 'all') && !a.hidden,
|
|
351
|
+
)
|
|
352
|
+
for (const agent of primaryAgents) {
|
|
353
|
+
const sanitizedName = sanitizeAgentName(agent.name)
|
|
354
|
+
const commandName = `${sanitizedName}-agent`
|
|
355
|
+
const description = agent.description || `Switch to ${agent.name} agent`
|
|
356
|
+
|
|
357
|
+
commands.push(
|
|
358
|
+
new SlashCommandBuilder()
|
|
359
|
+
.setName(commandName.slice(0, 32)) // Discord limits to 32 chars
|
|
360
|
+
.setDescription(description.slice(0, 100))
|
|
361
|
+
.toJSON(),
|
|
362
|
+
)
|
|
363
|
+
}
|
|
364
|
+
|
|
332
365
|
const rest = new REST().setToken(token)
|
|
333
366
|
|
|
334
367
|
try {
|
|
@@ -548,7 +581,12 @@ async function run({ restart, addChannels }: CliOptions) {
|
|
|
548
581
|
// This is the biggest startup bottleneck (can take 1-30 seconds to spawn and wait for ready)
|
|
549
582
|
const currentDir = process.cwd()
|
|
550
583
|
s.start('Starting OpenCode server...')
|
|
551
|
-
const opencodePromise = initializeOpencodeForDirectory(currentDir)
|
|
584
|
+
const opencodePromise = initializeOpencodeForDirectory(currentDir).then((result) => {
|
|
585
|
+
if (errore.isError(result)) {
|
|
586
|
+
throw new Error(result.message)
|
|
587
|
+
}
|
|
588
|
+
return result
|
|
589
|
+
})
|
|
552
590
|
|
|
553
591
|
s.message('Connecting to Discord...')
|
|
554
592
|
const discordClient = await createDiscordClient()
|
|
@@ -669,8 +707,8 @@ async function run({ restart, addChannels }: CliOptions) {
|
|
|
669
707
|
|
|
670
708
|
s.start('Fetching OpenCode data...')
|
|
671
709
|
|
|
672
|
-
// Fetch projects and
|
|
673
|
-
const [projects, allUserCommands] = await Promise.all([
|
|
710
|
+
// Fetch projects, commands, and agents in parallel
|
|
711
|
+
const [projects, allUserCommands, allAgents] = await Promise.all([
|
|
674
712
|
getClient()
|
|
675
713
|
.project.list({})
|
|
676
714
|
.then((r) => r.data || [])
|
|
@@ -684,6 +722,10 @@ async function run({ restart, addChannels }: CliOptions) {
|
|
|
684
722
|
.command.list({ query: { directory: currentDir } })
|
|
685
723
|
.then((r) => r.data || [])
|
|
686
724
|
.catch(() => []),
|
|
725
|
+
getClient()
|
|
726
|
+
.app.agents({ query: { directory: currentDir } })
|
|
727
|
+
.then((r) => r.data || [])
|
|
728
|
+
.catch(() => []),
|
|
687
729
|
])
|
|
688
730
|
|
|
689
731
|
s.stop(`Found ${projects.length} OpenCode project(s)`)
|
|
@@ -805,7 +847,7 @@ async function run({ restart, addChannels }: CliOptions) {
|
|
|
805
847
|
}
|
|
806
848
|
|
|
807
849
|
cliLogger.log('Registering slash commands asynchronously...')
|
|
808
|
-
void registerCommands(token, appId, allUserCommands)
|
|
850
|
+
void registerCommands({ token, appId, userCommands: allUserCommands, agents: allAgents })
|
|
809
851
|
.then(() => {
|
|
810
852
|
cliLogger.log('Slash commands registered!')
|
|
811
853
|
})
|
|
@@ -999,12 +1041,7 @@ cli
|
|
|
999
1041
|
}
|
|
1000
1042
|
})
|
|
1001
1043
|
|
|
1002
|
-
|
|
1003
|
-
// The running bot will recognize this prefix and start a session.
|
|
1004
|
-
const BOT_SESSION_PREFIX = '🤖 **Bot-initiated session**'
|
|
1005
|
-
// Notify-only prefix - bot won't start a session, just creates thread for notifications.
|
|
1006
|
-
// Reply to the thread to start a session with the notification as context.
|
|
1007
|
-
const BOT_NOTIFY_PREFIX = '📢 **Notification**'
|
|
1044
|
+
|
|
1008
1045
|
|
|
1009
1046
|
cli
|
|
1010
1047
|
.command(
|
|
@@ -1263,9 +1300,7 @@ cli
|
|
|
1263
1300
|
|
|
1264
1301
|
s.message('Creating starter message...')
|
|
1265
1302
|
|
|
1266
|
-
// Create starter message with
|
|
1267
|
-
// BOT_SESSION_PREFIX triggers AI session, BOT_NOTIFY_PREFIX is notification-only
|
|
1268
|
-
const messagePrefix = notifyOnly ? BOT_NOTIFY_PREFIX : BOT_SESSION_PREFIX
|
|
1303
|
+
// Create starter message with just the prompt (no prefix)
|
|
1269
1304
|
const starterMessageResponse = await fetch(
|
|
1270
1305
|
`https://discord.com/api/v10/channels/${channelId}/messages`,
|
|
1271
1306
|
{
|
|
@@ -1275,7 +1310,7 @@ cli
|
|
|
1275
1310
|
'Content-Type': 'application/json',
|
|
1276
1311
|
},
|
|
1277
1312
|
body: JSON.stringify({
|
|
1278
|
-
content:
|
|
1313
|
+
content: prompt,
|
|
1279
1314
|
}),
|
|
1280
1315
|
},
|
|
1281
1316
|
)
|
|
@@ -1315,6 +1350,19 @@ cli
|
|
|
1315
1350
|
|
|
1316
1351
|
const threadData = (await threadResponse.json()) as { id: string; name: string }
|
|
1317
1352
|
|
|
1353
|
+
// Mark thread for auto-start if not notify-only
|
|
1354
|
+
// This is optional - only works if local database exists (for local bot auto-start)
|
|
1355
|
+
if (!notifyOnly) {
|
|
1356
|
+
try {
|
|
1357
|
+
const db = getDatabase()
|
|
1358
|
+
db.prepare('INSERT OR REPLACE INTO pending_auto_start (thread_id) VALUES (?)').run(
|
|
1359
|
+
threadData.id,
|
|
1360
|
+
)
|
|
1361
|
+
} catch {
|
|
1362
|
+
// Database not available (e.g., CI environment) - skip auto-start marking
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1318
1366
|
s.stop('Thread created!')
|
|
1319
1367
|
|
|
1320
1368
|
const threadUrl = `https://discord.com/channels/${channelData.guild_id}/${threadData.id}`
|
package/src/commands/abort.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
|
7
7
|
import { resolveTextChannel, getKimakiMetadata, SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
|
|
8
8
|
import { abortControllers } from '../session-handler.js'
|
|
9
9
|
import { createLogger } from '../logger.js'
|
|
10
|
+
import * as errore from 'errore'
|
|
10
11
|
|
|
11
12
|
const logger = createLogger('ABORT')
|
|
12
13
|
|
|
@@ -64,14 +65,23 @@ export async function handleAbortCommand({ command }: CommandContext): Promise<v
|
|
|
64
65
|
|
|
65
66
|
const sessionId = row.session_id
|
|
66
67
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
68
|
+
const existingController = abortControllers.get(sessionId)
|
|
69
|
+
if (existingController) {
|
|
70
|
+
existingController.abort(new Error('User requested abort'))
|
|
71
|
+
abortControllers.delete(sessionId)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const getClient = await initializeOpencodeForDirectory(directory)
|
|
75
|
+
if (errore.isError(getClient)) {
|
|
76
|
+
await command.reply({
|
|
77
|
+
content: `Failed to abort: ${getClient.message}`,
|
|
78
|
+
ephemeral: true,
|
|
79
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
80
|
+
})
|
|
81
|
+
return
|
|
82
|
+
}
|
|
73
83
|
|
|
74
|
-
|
|
84
|
+
try {
|
|
75
85
|
await getClient().session.abort({
|
|
76
86
|
path: { id: sessionId },
|
|
77
87
|
})
|
|
@@ -8,6 +8,7 @@ import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
|
8
8
|
import { createProjectChannels } from '../channel-management.js'
|
|
9
9
|
import { createLogger } from '../logger.js'
|
|
10
10
|
import { abbreviatePath } from '../utils.js'
|
|
11
|
+
import * as errore from 'errore'
|
|
11
12
|
|
|
12
13
|
const logger = createLogger('ADD-PROJECT')
|
|
13
14
|
|
|
@@ -25,6 +26,10 @@ export async function handleAddProjectCommand({ command, appId }: CommandContext
|
|
|
25
26
|
try {
|
|
26
27
|
const currentDir = process.cwd()
|
|
27
28
|
const getClient = await initializeOpencodeForDirectory(currentDir)
|
|
29
|
+
if (errore.isError(getClient)) {
|
|
30
|
+
await command.editReply(getClient.message)
|
|
31
|
+
return
|
|
32
|
+
}
|
|
28
33
|
|
|
29
34
|
const projectsResponse = await getClient().project.list({})
|
|
30
35
|
if (!projectsResponse.data) {
|
|
@@ -89,6 +94,10 @@ export async function handleAddProjectAutocomplete({
|
|
|
89
94
|
try {
|
|
90
95
|
const currentDir = process.cwd()
|
|
91
96
|
const getClient = await initializeOpencodeForDirectory(currentDir)
|
|
97
|
+
if (errore.isError(getClient)) {
|
|
98
|
+
await interaction.respond([])
|
|
99
|
+
return
|
|
100
|
+
}
|
|
92
101
|
|
|
93
102
|
const projectsResponse = await getClient().project.list({})
|
|
94
103
|
if (!projectsResponse.data) {
|