kimaki 0.4.62 → 0.4.64
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/ai-tool-to-genai.js +72 -53
- package/dist/ai-tool-to-genai.test.js +1 -1
- package/dist/ai-tool.js +6 -0
- package/dist/cli.js +47 -9
- package/dist/commands/permissions.js +1 -4
- package/dist/commands/queue.js +1 -1
- package/dist/commands/restart-opencode-server.js +50 -2
- package/dist/commands/user-command.js +8 -4
- package/dist/config.js +9 -0
- package/dist/db.js +15 -9
- package/dist/discord-bot.js +2 -2
- package/dist/genai-worker.js +4 -3
- package/dist/logger.js +10 -0
- package/dist/session-handler.js +11 -12
- package/dist/system-message.js +46 -33
- package/dist/tools.js +1 -1
- package/dist/unnest-code-blocks.js +32 -20
- package/dist/unnest-code-blocks.test.js +184 -1
- package/dist/voice-handler.js +10 -1
- package/package.json +6 -5
- package/schema.prisma +138 -0
- package/src/ai-tool-to-genai.test.ts +1 -1
- package/src/ai-tool-to-genai.ts +93 -68
- package/src/ai-tool.ts +37 -0
- package/src/cli.ts +49 -9
- package/src/commands/permissions.ts +1 -4
- package/src/commands/queue.ts +1 -1
- package/src/commands/restart-opencode-server.ts +55 -2
- package/src/commands/user-command.ts +12 -4
- package/src/config.ts +12 -0
- package/src/db.ts +15 -12
- package/src/discord-bot.ts +2 -2
- package/src/genai-worker-wrapper.ts +2 -3
- package/src/genai-worker.ts +4 -3
- package/src/genai.ts +2 -2
- package/src/logger.ts +12 -0
- package/src/openai-realtime.ts +0 -1
- package/src/session-handler.ts +12 -15
- package/src/system-message.ts +48 -33
- package/src/tools.ts +1 -1
- package/src/unnest-code-blocks.test.ts +196 -1
- package/src/unnest-code-blocks.ts +44 -18
- package/src/voice-handler.ts +11 -1
- package/src/worker-types.ts +2 -4
package/dist/ai-tool-to-genai.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Transforms
|
|
1
|
+
// Tool definition to Google GenAI tool converter.
|
|
2
|
+
// Transforms Kimaki's minimal Tool definitions into Google GenAI CallableTool format
|
|
3
3
|
// for use with Gemini's function calling in the voice assistant.
|
|
4
4
|
import { Type } from '@google/genai';
|
|
5
5
|
import { z, toJSONSchema } from 'zod';
|
|
@@ -8,93 +8,112 @@ import { z, toJSONSchema } from 'zod';
|
|
|
8
8
|
* Based on the actual implementation used by the GenAI package:
|
|
9
9
|
* https://github.com/googleapis/js-genai/blob/027f09db662ce6b30f737b10b4d2efcb4282a9b6/src/_transformers.ts#L294
|
|
10
10
|
*/
|
|
11
|
+
function isRecord(value) {
|
|
12
|
+
return typeof value === 'object' && value !== null;
|
|
13
|
+
}
|
|
11
14
|
function jsonSchemaToGenAISchema(jsonSchema) {
|
|
12
15
|
const schema = {};
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
if (typeof jsonSchema === 'boolean') {
|
|
17
|
+
return schema;
|
|
18
|
+
}
|
|
19
|
+
const jsonSchemaType = (() => {
|
|
20
|
+
if (!jsonSchema.type) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
if (typeof jsonSchema.type === 'string') {
|
|
24
|
+
return jsonSchema.type;
|
|
25
|
+
}
|
|
26
|
+
if (Array.isArray(jsonSchema.type)) {
|
|
27
|
+
return jsonSchema.type.find((t) => t !== 'null') || jsonSchema.type[0];
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
})();
|
|
31
|
+
if (Array.isArray(jsonSchema.type) && jsonSchema.type.includes('null')) {
|
|
32
|
+
schema.nullable = true;
|
|
33
|
+
}
|
|
34
|
+
if (jsonSchemaType) {
|
|
35
|
+
switch (jsonSchemaType) {
|
|
16
36
|
case 'string':
|
|
17
37
|
schema.type = Type.STRING;
|
|
18
38
|
break;
|
|
19
39
|
case 'number':
|
|
20
40
|
schema.type = Type.NUMBER;
|
|
21
|
-
schema.format = jsonSchema.format
|
|
41
|
+
schema.format = typeof jsonSchema.format === 'string' ? jsonSchema.format : 'float';
|
|
22
42
|
break;
|
|
23
43
|
case 'integer':
|
|
24
44
|
schema.type = Type.INTEGER;
|
|
25
|
-
schema.format = jsonSchema.format
|
|
45
|
+
schema.format = typeof jsonSchema.format === 'string' ? jsonSchema.format : 'int32';
|
|
26
46
|
break;
|
|
27
47
|
case 'boolean':
|
|
28
48
|
schema.type = Type.BOOLEAN;
|
|
29
49
|
break;
|
|
30
|
-
case 'array':
|
|
50
|
+
case 'array': {
|
|
31
51
|
schema.type = Type.ARRAY;
|
|
32
|
-
|
|
33
|
-
|
|
52
|
+
const itemsSchema = (() => {
|
|
53
|
+
if (!jsonSchema.items) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
if (Array.isArray(jsonSchema.items)) {
|
|
57
|
+
return jsonSchema.items[0];
|
|
58
|
+
}
|
|
59
|
+
return jsonSchema.items;
|
|
60
|
+
})();
|
|
61
|
+
if (itemsSchema) {
|
|
62
|
+
schema.items = jsonSchemaToGenAISchema(itemsSchema);
|
|
34
63
|
}
|
|
35
|
-
if (jsonSchema.minItems
|
|
36
|
-
schema.minItems = jsonSchema.minItems;
|
|
64
|
+
if (typeof jsonSchema.minItems === 'number') {
|
|
65
|
+
schema.minItems = String(jsonSchema.minItems);
|
|
37
66
|
}
|
|
38
|
-
if (jsonSchema.maxItems
|
|
39
|
-
schema.maxItems = jsonSchema.maxItems;
|
|
67
|
+
if (typeof jsonSchema.maxItems === 'number') {
|
|
68
|
+
schema.maxItems = String(jsonSchema.maxItems);
|
|
40
69
|
}
|
|
41
70
|
break;
|
|
71
|
+
}
|
|
42
72
|
case 'object':
|
|
43
73
|
schema.type = Type.OBJECT;
|
|
44
74
|
if (jsonSchema.properties) {
|
|
45
|
-
schema.properties =
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
75
|
+
schema.properties = Object.fromEntries(Object.entries(jsonSchema.properties).map(([key, value]) => [
|
|
76
|
+
key,
|
|
77
|
+
jsonSchemaToGenAISchema(value),
|
|
78
|
+
]));
|
|
49
79
|
}
|
|
50
|
-
if (jsonSchema.required) {
|
|
80
|
+
if (Array.isArray(jsonSchema.required)) {
|
|
51
81
|
schema.required = jsonSchema.required;
|
|
52
82
|
}
|
|
53
|
-
// Note: GenAI Schema doesn't have additionalProperties field
|
|
54
|
-
// We skip it for now
|
|
55
83
|
break;
|
|
56
|
-
default:
|
|
57
|
-
// For unknown types, keep as-is
|
|
58
|
-
schema.type = jsonSchema.type;
|
|
59
84
|
}
|
|
60
85
|
}
|
|
61
|
-
|
|
62
|
-
if (jsonSchema.description) {
|
|
86
|
+
if (typeof jsonSchema.description === 'string') {
|
|
63
87
|
schema.description = jsonSchema.description;
|
|
64
88
|
}
|
|
65
|
-
if (jsonSchema.enum) {
|
|
66
|
-
schema.enum = jsonSchema.enum.map(String);
|
|
89
|
+
if (Array.isArray(jsonSchema.enum)) {
|
|
90
|
+
schema.enum = jsonSchema.enum.map((x) => String(x));
|
|
67
91
|
}
|
|
68
|
-
if (
|
|
92
|
+
if ('default' in jsonSchema) {
|
|
69
93
|
schema.default = jsonSchema.default;
|
|
70
94
|
}
|
|
71
|
-
if (jsonSchema.
|
|
72
|
-
schema.example = jsonSchema.
|
|
73
|
-
}
|
|
74
|
-
if (jsonSchema.nullable) {
|
|
75
|
-
schema.nullable = true;
|
|
95
|
+
if (Array.isArray(jsonSchema.examples) && jsonSchema.examples.length > 0) {
|
|
96
|
+
schema.example = jsonSchema.examples[0];
|
|
76
97
|
}
|
|
77
|
-
|
|
78
|
-
if (jsonSchema.anyOf) {
|
|
98
|
+
if (Array.isArray(jsonSchema.anyOf)) {
|
|
79
99
|
schema.anyOf = jsonSchema.anyOf.map((s) => jsonSchemaToGenAISchema(s));
|
|
80
100
|
}
|
|
81
|
-
else if (jsonSchema.oneOf) {
|
|
101
|
+
else if (Array.isArray(jsonSchema.oneOf)) {
|
|
82
102
|
schema.anyOf = jsonSchema.oneOf.map((s) => jsonSchemaToGenAISchema(s));
|
|
83
103
|
}
|
|
84
|
-
|
|
85
|
-
if (jsonSchema.minimum !== undefined) {
|
|
104
|
+
if (typeof jsonSchema.minimum === 'number') {
|
|
86
105
|
schema.minimum = jsonSchema.minimum;
|
|
87
106
|
}
|
|
88
|
-
if (jsonSchema.maximum
|
|
107
|
+
if (typeof jsonSchema.maximum === 'number') {
|
|
89
108
|
schema.maximum = jsonSchema.maximum;
|
|
90
109
|
}
|
|
91
|
-
if (jsonSchema.minLength
|
|
92
|
-
schema.minLength = jsonSchema.minLength;
|
|
110
|
+
if (typeof jsonSchema.minLength === 'number') {
|
|
111
|
+
schema.minLength = String(jsonSchema.minLength);
|
|
93
112
|
}
|
|
94
|
-
if (jsonSchema.maxLength
|
|
95
|
-
schema.maxLength = jsonSchema.maxLength;
|
|
113
|
+
if (typeof jsonSchema.maxLength === 'number') {
|
|
114
|
+
schema.maxLength = String(jsonSchema.maxLength);
|
|
96
115
|
}
|
|
97
|
-
if (jsonSchema.pattern) {
|
|
116
|
+
if (typeof jsonSchema.pattern === 'string') {
|
|
98
117
|
schema.pattern = jsonSchema.pattern;
|
|
99
118
|
}
|
|
100
119
|
return schema;
|
|
@@ -156,12 +175,13 @@ export function aiToolToCallableTool(tool, name) {
|
|
|
156
175
|
// Execute the tool if it has an execute function
|
|
157
176
|
if (tool.execute) {
|
|
158
177
|
try {
|
|
159
|
-
const
|
|
178
|
+
const args = isRecord(functionCall.args) ? functionCall.args : {};
|
|
179
|
+
const result = await tool.execute(args, {
|
|
160
180
|
toolCallId: functionCall.id || '',
|
|
161
181
|
messages: [],
|
|
162
182
|
});
|
|
163
183
|
// Convert the result to a Part
|
|
164
|
-
|
|
184
|
+
const part = {
|
|
165
185
|
functionResponse: {
|
|
166
186
|
id: functionCall.id,
|
|
167
187
|
name: functionCall.name || toolName,
|
|
@@ -169,11 +189,12 @@ export function aiToolToCallableTool(tool, name) {
|
|
|
169
189
|
output: result,
|
|
170
190
|
},
|
|
171
191
|
},
|
|
172
|
-
}
|
|
192
|
+
};
|
|
193
|
+
parts.push(part);
|
|
173
194
|
}
|
|
174
195
|
catch (error) {
|
|
175
196
|
// Handle errors
|
|
176
|
-
|
|
197
|
+
const part = {
|
|
177
198
|
functionResponse: {
|
|
178
199
|
id: functionCall.id,
|
|
179
200
|
name: functionCall.name || toolName,
|
|
@@ -181,7 +202,8 @@ export function aiToolToCallableTool(tool, name) {
|
|
|
181
202
|
error: error instanceof Error ? error.message : String(error),
|
|
182
203
|
},
|
|
183
204
|
},
|
|
184
|
-
}
|
|
205
|
+
};
|
|
206
|
+
parts.push(part);
|
|
185
207
|
}
|
|
186
208
|
}
|
|
187
209
|
}
|
|
@@ -189,9 +211,6 @@ export function aiToolToCallableTool(tool, name) {
|
|
|
189
211
|
},
|
|
190
212
|
};
|
|
191
213
|
}
|
|
192
|
-
/**
|
|
193
|
-
* Helper to extract schema from AI SDK tool
|
|
194
|
-
*/
|
|
195
214
|
export function extractSchemaFromTool(tool) {
|
|
196
215
|
const inputSchema = tool.inputSchema;
|
|
197
216
|
if (!inputSchema) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { tool } from 'ai';
|
|
2
|
+
import { tool } from './ai-tool.js';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { Type } from '@google/genai';
|
|
5
5
|
import { aiToolToGenAIFunction, aiToolToCallableTool, extractSchemaFromTool, } from './ai-tool-to-genai.js';
|
package/dist/ai-tool.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Minimal tool definition helper used by Kimaki.
|
|
2
|
+
// This replaces the Vercel AI SDK `tool()` helper so Kimaki can define typed
|
|
3
|
+
// tools (Zod input schema + execute) without depending on the full `ai` package.
|
|
4
|
+
export function tool(definition) {
|
|
5
|
+
return definition;
|
|
6
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -15,11 +15,11 @@ import { Events, ChannelType, REST, Routes, SlashCommandBuilder, AttachmentBuild
|
|
|
15
15
|
import path from 'node:path';
|
|
16
16
|
import fs from 'node:fs';
|
|
17
17
|
import * as errore from 'errore';
|
|
18
|
-
import { createLogger, LogPrefix } from './logger.js';
|
|
18
|
+
import { createLogger, formatErrorWithStack, LogPrefix } from './logger.js';
|
|
19
19
|
import { archiveThread, uploadFilesToDiscord, stripMentions } from './discord-utils.js';
|
|
20
20
|
import { spawn, spawnSync, execSync } from 'node:child_process';
|
|
21
21
|
import http from 'node:http';
|
|
22
|
-
import { setDataDir, getDataDir, getLockPort, setDefaultVerbosity, setDefaultMentionMode, getProjectsDir } from './config.js';
|
|
22
|
+
import { setDataDir, getDataDir, getLockPort, setDefaultVerbosity, setDefaultMentionMode, setCritiqueEnabled, getProjectsDir } from './config.js';
|
|
23
23
|
import { sanitizeAgentName } from './commands/agent.js';
|
|
24
24
|
import { showFileUploadButton, } from './commands/file-upload.js';
|
|
25
25
|
import { execAsync } from './worktree-utils.js';
|
|
@@ -1105,6 +1105,7 @@ cli
|
|
|
1105
1105
|
.option('--enable-voice-channels', 'Create voice channels for projects (disabled by default)')
|
|
1106
1106
|
.option('--verbosity <level>', 'Default verbosity for all channels (tools-and-text, text-and-essential-tools, or text-only)')
|
|
1107
1107
|
.option('--mention-mode', 'Bot only responds when @mentioned (default for all channels)')
|
|
1108
|
+
.option('--no-critique', 'Disable automatic diff upload to critique.work in system prompts')
|
|
1108
1109
|
.option('--auto-restart', 'Automatically restart the bot on crash or OOM kill')
|
|
1109
1110
|
.action(async (options) => {
|
|
1110
1111
|
try {
|
|
@@ -1126,6 +1127,10 @@ cli
|
|
|
1126
1127
|
setDefaultMentionMode(true);
|
|
1127
1128
|
cliLogger.log('Default mention mode: enabled (bot only responds when @mentioned)');
|
|
1128
1129
|
}
|
|
1130
|
+
if (options.noCritique) {
|
|
1131
|
+
setCritiqueEnabled(false);
|
|
1132
|
+
cliLogger.log('Critique disabled: diffs will not be auto-uploaded to critique.work');
|
|
1133
|
+
}
|
|
1129
1134
|
if (options.installUrl) {
|
|
1130
1135
|
await initDatabase();
|
|
1131
1136
|
const existingBot = await getBotToken();
|
|
@@ -1147,7 +1152,7 @@ cli
|
|
|
1147
1152
|
});
|
|
1148
1153
|
}
|
|
1149
1154
|
catch (error) {
|
|
1150
|
-
cliLogger.error('Unhandled error:',
|
|
1155
|
+
cliLogger.error('Unhandled error:', formatErrorWithStack(error));
|
|
1151
1156
|
process.exit(EXIT_NO_RESTART);
|
|
1152
1157
|
}
|
|
1153
1158
|
});
|
|
@@ -1992,15 +1997,48 @@ cli
|
|
|
1992
1997
|
cliLogger.error('Failed to connect to OpenCode:', getClient.message);
|
|
1993
1998
|
process.exit(EXIT_NO_RESTART);
|
|
1994
1999
|
}
|
|
2000
|
+
// Try current project first (fast path)
|
|
1995
2001
|
const markdown = new ShareMarkdown(getClient());
|
|
1996
2002
|
const result = await markdown.generate({ sessionID: sessionId });
|
|
1997
|
-
if (result instanceof Error) {
|
|
1998
|
-
|
|
1999
|
-
process.exit(
|
|
2003
|
+
if (!(result instanceof Error)) {
|
|
2004
|
+
process.stdout.write(result);
|
|
2005
|
+
process.exit(0);
|
|
2000
2006
|
}
|
|
2001
|
-
//
|
|
2002
|
-
|
|
2003
|
-
|
|
2007
|
+
// Session not found in current project, search across all projects.
|
|
2008
|
+
// project.list() returns all known projects globally from any OpenCode server,
|
|
2009
|
+
// but session.list/get are scoped to the server's own project. So we try each.
|
|
2010
|
+
cliLogger.log('Session not in current project, searching all projects...');
|
|
2011
|
+
const projectsResponse = await getClient().project.list({});
|
|
2012
|
+
const projects = projectsResponse.data || [];
|
|
2013
|
+
const otherProjects = projects
|
|
2014
|
+
.filter((p) => path.resolve(p.worktree) !== projectDirectory)
|
|
2015
|
+
.filter((p) => {
|
|
2016
|
+
try {
|
|
2017
|
+
fs.accessSync(p.worktree, fs.constants.R_OK);
|
|
2018
|
+
return true;
|
|
2019
|
+
}
|
|
2020
|
+
catch {
|
|
2021
|
+
return false;
|
|
2022
|
+
}
|
|
2023
|
+
})
|
|
2024
|
+
// Sort by most recently created first to find sessions faster
|
|
2025
|
+
.sort((a, b) => b.time.created - a.time.created);
|
|
2026
|
+
for (const project of otherProjects) {
|
|
2027
|
+
const dir = project.worktree;
|
|
2028
|
+
cliLogger.log(`Trying project: ${dir}`);
|
|
2029
|
+
const otherClient = await initializeOpencodeForDirectory(dir);
|
|
2030
|
+
if (otherClient instanceof Error) {
|
|
2031
|
+
continue;
|
|
2032
|
+
}
|
|
2033
|
+
const otherMarkdown = new ShareMarkdown(otherClient());
|
|
2034
|
+
const otherResult = await otherMarkdown.generate({ sessionID: sessionId });
|
|
2035
|
+
if (!(otherResult instanceof Error)) {
|
|
2036
|
+
process.stdout.write(otherResult);
|
|
2037
|
+
process.exit(0);
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
cliLogger.error(`Session ${sessionId} not found in any project`);
|
|
2041
|
+
process.exit(EXIT_NO_RESTART);
|
|
2004
2042
|
}
|
|
2005
2043
|
catch (error) {
|
|
2006
2044
|
cliLogger.error('Error:', error instanceof Error ? error.message : String(error));
|
|
@@ -93,10 +93,7 @@ export async function handlePermissionButton(interaction) {
|
|
|
93
93
|
const response = actionPart.replace('permission_', '');
|
|
94
94
|
const context = pendingPermissionContexts.get(contextHash);
|
|
95
95
|
if (!context) {
|
|
96
|
-
await interaction.
|
|
97
|
-
content: 'This permission request has expired or was already handled.',
|
|
98
|
-
ephemeral: true,
|
|
99
|
-
});
|
|
96
|
+
await interaction.update({ components: [] });
|
|
100
97
|
return;
|
|
101
98
|
}
|
|
102
99
|
await interaction.deferUpdate();
|
package/dist/commands/queue.js
CHANGED
|
@@ -178,7 +178,7 @@ export async function handleQueueCommandCommand({ command, appId }) {
|
|
|
178
178
|
return;
|
|
179
179
|
}
|
|
180
180
|
const commandPayload = { name: commandName, arguments: args };
|
|
181
|
-
const displayText = `/${commandName}
|
|
181
|
+
const displayText = `/${commandName}`;
|
|
182
182
|
// Check if there's an active request running
|
|
183
183
|
const existingController = abortControllers.get(sessionId);
|
|
184
184
|
const hasActiveRequest = Boolean(existingController && !existingController.signal.aborted);
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
// /restart-opencode-server command - Restart the opencode server for the current channel.
|
|
2
2
|
// Used for resolving opencode state issues, internal bugs, refreshing auth state, plugins, etc.
|
|
3
|
+
// Aborts all in-progress sessions in this channel before restarting to avoid orphaned requests.
|
|
3
4
|
import { ChannelType } from 'discord.js';
|
|
4
|
-
import { restartOpencodeServer } from '../opencode.js';
|
|
5
|
+
import { initializeOpencodeForDirectory, restartOpencodeServer } from '../opencode.js';
|
|
5
6
|
import { resolveWorkingDirectory, SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
|
|
6
7
|
import { createLogger, LogPrefix } from '../logger.js';
|
|
8
|
+
import { getAllThreadSessionIds, getThreadIdBySessionId } from '../database.js';
|
|
9
|
+
import { abortControllers } from '../session-handler.js';
|
|
10
|
+
import * as errore from 'errore';
|
|
7
11
|
const logger = createLogger(LogPrefix.OPENCODE);
|
|
8
12
|
export async function handleRestartOpencodeServerCommand({ command, appId }) {
|
|
9
13
|
const channel = command.channel;
|
|
@@ -49,6 +53,49 @@ export async function handleRestartOpencodeServerCommand({ command, appId }) {
|
|
|
49
53
|
}
|
|
50
54
|
// Defer reply since restart may take a moment
|
|
51
55
|
await command.deferReply({ flags: SILENT_MESSAGE_FLAGS });
|
|
56
|
+
// Abort all in-progress sessions in this channel before restarting.
|
|
57
|
+
// Find sessions with active abort controllers, check if their thread belongs
|
|
58
|
+
// to this channel (thread parentId matches, or command was run in the thread itself).
|
|
59
|
+
const parentChannelId = isThread ? channel.parentId : channel.id;
|
|
60
|
+
const activeSessionIds = [...abortControllers.keys()];
|
|
61
|
+
let abortedCount = 0;
|
|
62
|
+
if (activeSessionIds.length > 0) {
|
|
63
|
+
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
64
|
+
const client = !(getClient instanceof Error) ? getClient : null;
|
|
65
|
+
for (const sessionId of activeSessionIds) {
|
|
66
|
+
const threadId = await getThreadIdBySessionId(sessionId);
|
|
67
|
+
if (!threadId) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
// Check if thread belongs to this channel: either the thread IS this channel,
|
|
71
|
+
// or the thread's parent matches the parent channel
|
|
72
|
+
const threadChannel = await errore.tryAsync(() => {
|
|
73
|
+
return command.client.channels.fetch(threadId);
|
|
74
|
+
});
|
|
75
|
+
if (threadChannel instanceof Error || !threadChannel) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const threadParentId = 'parentId' in threadChannel ? threadChannel.parentId : null;
|
|
79
|
+
if (threadId !== channel.id && threadParentId !== parentChannelId) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const controller = abortControllers.get(sessionId);
|
|
83
|
+
if (controller) {
|
|
84
|
+
logger.log(`[RESTART] Aborting session ${sessionId} in thread ${threadId}`);
|
|
85
|
+
controller.abort(new Error('Server restart requested'));
|
|
86
|
+
abortControllers.delete(sessionId);
|
|
87
|
+
abortedCount++;
|
|
88
|
+
}
|
|
89
|
+
if (client) {
|
|
90
|
+
await errore.tryAsync(() => {
|
|
91
|
+
return client().session.abort({ path: { id: sessionId } });
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (abortedCount > 0) {
|
|
97
|
+
logger.log(`[RESTART] Aborted ${abortedCount} active session(s) before restart`);
|
|
98
|
+
}
|
|
52
99
|
logger.log(`[RESTART] Restarting opencode server for directory: ${projectDirectory}`);
|
|
53
100
|
const result = await restartOpencodeServer(projectDirectory);
|
|
54
101
|
if (result instanceof Error) {
|
|
@@ -58,8 +105,9 @@ export async function handleRestartOpencodeServerCommand({ command, appId }) {
|
|
|
58
105
|
});
|
|
59
106
|
return;
|
|
60
107
|
}
|
|
108
|
+
const abortMsg = abortedCount > 0 ? ` (aborted ${abortedCount} active session${abortedCount > 1 ? 's' : ''})` : '';
|
|
61
109
|
await command.editReply({
|
|
62
|
-
content:
|
|
110
|
+
content: `Opencode server **restarted** successfully${abortMsg}`,
|
|
63
111
|
});
|
|
64
112
|
logger.log(`[RESTART] Opencode server restarted for directory: ${projectDirectory}`);
|
|
65
113
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Handles slash commands that map to user-configured commands in opencode.json.
|
|
3
3
|
import { ChannelType } from 'discord.js';
|
|
4
4
|
import { handleOpencodeSession } from '../session-handler.js';
|
|
5
|
-
import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
|
|
5
|
+
import { sendThreadMessage, SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
|
|
6
6
|
import { createLogger, LogPrefix } from '../logger.js';
|
|
7
7
|
import { getChannelDirectory, getThreadSession } from '../database.js';
|
|
8
8
|
import fs from 'node:fs';
|
|
@@ -12,7 +12,7 @@ export const handleUserCommand = async ({ command, appId }) => {
|
|
|
12
12
|
// Strip the -cmd suffix to get the actual OpenCode command name
|
|
13
13
|
const commandName = discordCommandName.replace(/-cmd$/, '');
|
|
14
14
|
const args = command.options.getString('arguments') || '';
|
|
15
|
-
userCommandLogger.log(`Executing /${commandName} (from /${discordCommandName})
|
|
15
|
+
userCommandLogger.log(`Executing /${commandName} (from /${discordCommandName}) argsLength=${args.length}`);
|
|
16
16
|
const channel = command.channel;
|
|
17
17
|
userCommandLogger.log(`Channel info: type=${channel?.type}, id=${channel?.id}, isNull=${channel === null}`);
|
|
18
18
|
const isThread = channel &&
|
|
@@ -95,10 +95,10 @@ export const handleUserCommand = async ({ command, appId }) => {
|
|
|
95
95
|
else if (textChannel) {
|
|
96
96
|
// Running in text channel - create a new thread
|
|
97
97
|
const starterMessage = await textChannel.send({
|
|
98
|
-
content: `**/${commandName}
|
|
98
|
+
content: `**/${commandName}**`,
|
|
99
99
|
flags: SILENT_MESSAGE_FLAGS,
|
|
100
100
|
});
|
|
101
|
-
const threadName = `/${commandName}
|
|
101
|
+
const threadName = `/${commandName}`;
|
|
102
102
|
const newThread = await starterMessage.startThread({
|
|
103
103
|
name: threadName.slice(0, 100),
|
|
104
104
|
autoArchiveDuration: 1440,
|
|
@@ -106,6 +106,10 @@ export const handleUserCommand = async ({ command, appId }) => {
|
|
|
106
106
|
});
|
|
107
107
|
// Add user to thread so it appears in their sidebar
|
|
108
108
|
await newThread.members.add(command.user.id);
|
|
109
|
+
if (args) {
|
|
110
|
+
const argsPreview = args.length > 1800 ? `${args.slice(0, 1800)}\n... truncated` : args;
|
|
111
|
+
await sendThreadMessage(newThread, `Args: ${argsPreview}`);
|
|
112
|
+
}
|
|
109
113
|
await command.editReply(`Started /${commandName} in ${newThread.toString()}`);
|
|
110
114
|
await handleOpencodeSession({
|
|
111
115
|
prompt: '', // Not used when command is set
|
package/dist/config.js
CHANGED
|
@@ -51,6 +51,15 @@ export function getDefaultMentionMode() {
|
|
|
51
51
|
export function setDefaultMentionMode(enabled) {
|
|
52
52
|
defaultMentionMode = enabled;
|
|
53
53
|
}
|
|
54
|
+
// Whether critique (diff upload to critique.work) is enabled in system prompts.
|
|
55
|
+
// Enabled by default, disabled via --no-critique CLI flag.
|
|
56
|
+
let critiqueEnabled = true;
|
|
57
|
+
export function getCritiqueEnabled() {
|
|
58
|
+
return critiqueEnabled;
|
|
59
|
+
}
|
|
60
|
+
export function setCritiqueEnabled(enabled) {
|
|
61
|
+
critiqueEnabled = enabled;
|
|
62
|
+
}
|
|
54
63
|
export const registeredUserCommands = [];
|
|
55
64
|
const DEFAULT_LOCK_PORT = 29988;
|
|
56
65
|
/**
|
package/dist/db.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { PrismaLibSql } from '@prisma/adapter-libsql';
|
|
4
4
|
import { PrismaClient, Prisma } from './generated/client.js';
|
|
5
5
|
import { getDataDir } from './config.js';
|
|
6
|
-
import { createLogger, LogPrefix } from './logger.js';
|
|
6
|
+
import { createLogger, formatErrorWithStack, LogPrefix } from './logger.js';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
9
9
|
const __dirname = path.dirname(__filename);
|
|
@@ -38,14 +38,20 @@ async function initializePrisma() {
|
|
|
38
38
|
dbLogger.log(`Opening database at: ${dbPath}`);
|
|
39
39
|
const adapter = new PrismaLibSql({ url: `file:${dbPath}` });
|
|
40
40
|
const prisma = new PrismaClient({ adapter });
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
41
|
+
try {
|
|
42
|
+
// WAL mode allows concurrent reads while writing instead of blocking.
|
|
43
|
+
// busy_timeout makes SQLite retry for 5s instead of immediately failing with SQLITE_BUSY.
|
|
44
|
+
await prisma.$executeRawUnsafe('PRAGMA journal_mode = WAL');
|
|
45
|
+
await prisma.$executeRawUnsafe('PRAGMA busy_timeout = 5000');
|
|
46
|
+
// Always run migrations - schema.sql uses IF NOT EXISTS so it's idempotent
|
|
47
|
+
dbLogger.log(exists ? 'Existing database, running migrations...' : 'New database, running schema setup...');
|
|
48
|
+
await migrateSchema(prisma);
|
|
49
|
+
dbLogger.log('Schema migration complete');
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
dbLogger.error('Prisma init failed:', formatErrorWithStack(error));
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
49
55
|
prismaInstance = prisma;
|
|
50
56
|
return prisma;
|
|
51
57
|
}
|
package/dist/discord-bot.js
CHANGED
|
@@ -24,7 +24,7 @@ export { ensureKimakiCategory, ensureKimakiAudioCategory, createProjectChannels,
|
|
|
24
24
|
import { ChannelType, Client, Events, GatewayIntentBits, Partials, ThreadAutoArchiveDuration, } from 'discord.js';
|
|
25
25
|
import fs from 'node:fs';
|
|
26
26
|
import * as errore from 'errore';
|
|
27
|
-
import { createLogger, LogPrefix } from './logger.js';
|
|
27
|
+
import { createLogger, formatErrorWithStack, LogPrefix } from './logger.js';
|
|
28
28
|
import { writeHeapSnapshot, startHeapMonitor } from './heap-monitor.js';
|
|
29
29
|
import { setGlobalDispatcher, Agent } from 'undici';
|
|
30
30
|
// Increase connection pool to prevent deadlock when multiple sessions have open SSE streams.
|
|
@@ -700,6 +700,6 @@ export async function startDiscordBot({ token, appId, discordClient, useWorktree
|
|
|
700
700
|
discordLogger.log('Ignoring unhandled rejection during shutdown:', reason);
|
|
701
701
|
return;
|
|
702
702
|
}
|
|
703
|
-
discordLogger.error('Unhandled
|
|
703
|
+
discordLogger.error('Unhandled rejection:', formatErrorWithStack(reason), 'at promise:', promise);
|
|
704
704
|
});
|
|
705
705
|
}
|
package/dist/genai-worker.js
CHANGED
|
@@ -10,7 +10,7 @@ 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, LogPrefix } from './logger.js';
|
|
13
|
+
import { createLogger, formatErrorWithStack, LogPrefix } from './logger.js';
|
|
14
14
|
if (!parentPort) {
|
|
15
15
|
throw new Error('This module must be run as a worker thread');
|
|
16
16
|
}
|
|
@@ -33,8 +33,9 @@ process.on('uncaughtException', (error) => {
|
|
|
33
33
|
process.exit(1);
|
|
34
34
|
});
|
|
35
35
|
process.on('unhandledRejection', (reason, promise) => {
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
const formattedReason = formatErrorWithStack(reason);
|
|
37
|
+
workerLogger.error('Unhandled rejection in worker:', formattedReason, 'at promise:', promise);
|
|
38
|
+
sendError(`Worker unhandled rejection: ${formattedReason}`);
|
|
38
39
|
});
|
|
39
40
|
// Audio configuration
|
|
40
41
|
const AUDIO_CONFIG = {
|
package/dist/logger.js
CHANGED
|
@@ -66,6 +66,16 @@ function formatArg(arg) {
|
|
|
66
66
|
}
|
|
67
67
|
return util.inspect(arg, { colors: true, depth: 4 });
|
|
68
68
|
}
|
|
69
|
+
export function formatErrorWithStack(error) {
|
|
70
|
+
if (error instanceof Error) {
|
|
71
|
+
return error.stack ?? `${error.name}: ${error.message}`;
|
|
72
|
+
}
|
|
73
|
+
if (typeof error === 'string') {
|
|
74
|
+
return error;
|
|
75
|
+
}
|
|
76
|
+
// Keep this stable and safe for unknown values (handles circular structures).
|
|
77
|
+
return util.inspect(error, { colors: false, depth: 4 });
|
|
78
|
+
}
|
|
69
79
|
function writeToFile(level, prefix, args) {
|
|
70
80
|
if (!isDev) {
|
|
71
81
|
return;
|
package/dist/session-handler.js
CHANGED
|
@@ -381,13 +381,19 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
381
381
|
const threadPermissions = pendingPermissions.get(thread.id);
|
|
382
382
|
if (threadPermissions && threadPermissions.size > 0) {
|
|
383
383
|
const clientV2 = getOpencodeClientV2(directory);
|
|
384
|
-
let rejectedCount = 0;
|
|
385
384
|
for (const [permId, pendingPerm] of threadPermissions) {
|
|
386
385
|
sessionLogger.log(`[PERMISSION] Auto-rejecting permission ${permId} due to new message`);
|
|
386
|
+
// Remove the permission buttons from the Discord message
|
|
387
|
+
const removeButtonsResult = await errore.tryAsync(async () => {
|
|
388
|
+
const msg = await thread.messages.fetch(pendingPerm.messageId);
|
|
389
|
+
await msg.edit({ components: [] });
|
|
390
|
+
});
|
|
391
|
+
if (removeButtonsResult instanceof Error) {
|
|
392
|
+
sessionLogger.log(`[PERMISSION] Failed to remove buttons for ${permId}:`, removeButtonsResult);
|
|
393
|
+
}
|
|
387
394
|
if (!clientV2) {
|
|
388
395
|
sessionLogger.log(`[PERMISSION] OpenCode v2 client unavailable for permission ${permId}`);
|
|
389
396
|
cleanupPermissionContext(pendingPerm.contextHash);
|
|
390
|
-
rejectedCount++;
|
|
391
397
|
continue;
|
|
392
398
|
}
|
|
393
399
|
const rejectResult = await errore.tryAsync(() => {
|
|
@@ -400,16 +406,9 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
400
406
|
if (rejectResult instanceof Error) {
|
|
401
407
|
sessionLogger.log(`[PERMISSION] Failed to auto-reject permission ${permId}:`, rejectResult);
|
|
402
408
|
}
|
|
403
|
-
else {
|
|
404
|
-
rejectedCount++;
|
|
405
|
-
}
|
|
406
409
|
cleanupPermissionContext(pendingPerm.contextHash);
|
|
407
410
|
}
|
|
408
411
|
pendingPermissions.delete(thread.id);
|
|
409
|
-
if (rejectedCount > 0) {
|
|
410
|
-
const plural = rejectedCount > 1 ? 's' : '';
|
|
411
|
-
await sendThreadMessage(thread, `⚠️ ${rejectedCount} pending permission request${plural} auto-rejected due to new message`);
|
|
412
|
-
}
|
|
413
412
|
}
|
|
414
413
|
// Answer any pending question tool with the user's message (silently, no thread message)
|
|
415
414
|
const questionAnswered = await cancelPendingQuestion(thread.id, prompt);
|
|
@@ -1029,7 +1028,7 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
1029
1028
|
}
|
|
1030
1029
|
sessionLogger.log(`[QUEUE] Question shown but queue has messages, processing from ${nextMessage.username}`);
|
|
1031
1030
|
const displayText = nextMessage.command
|
|
1032
|
-
? `/${nextMessage.command.name}
|
|
1031
|
+
? `/${nextMessage.command.name}`
|
|
1033
1032
|
: `${nextMessage.prompt.slice(0, 150)}${nextMessage.prompt.length > 150 ? '...' : ''}`;
|
|
1034
1033
|
await sendThreadMessage(thread, `» **${nextMessage.username}:** ${displayText}`);
|
|
1035
1034
|
setImmediate(() => {
|
|
@@ -1205,7 +1204,7 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
1205
1204
|
sessionLogger.error('Failed to fetch provider info for context percentage:', contextResult);
|
|
1206
1205
|
}
|
|
1207
1206
|
const projectInfo = branchName ? `${folderName} ⋅ ${branchName} ⋅ ` : `${folderName} ⋅ `;
|
|
1208
|
-
await sendThreadMessage(thread,
|
|
1207
|
+
await sendThreadMessage(thread, `*${projectInfo}${sessionDuration}${contextInfo}${attachCommand}${modelInfo}${agentInfo}*`, { flags: NOTIFY_MESSAGE_FLAGS });
|
|
1209
1208
|
sessionLogger.log(`DURATION: Session completed in ${sessionDuration}, port ${port}, model ${usedModel}, tokens ${tokensUsedInSession}`);
|
|
1210
1209
|
// Process queued messages after completion
|
|
1211
1210
|
const queue = messageQueue.get(thread.id);
|
|
@@ -1217,7 +1216,7 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
1217
1216
|
sessionLogger.log(`[QUEUE] Processing queued message from ${nextMessage.username}`);
|
|
1218
1217
|
// Show that queued message is being sent
|
|
1219
1218
|
const displayText = nextMessage.command
|
|
1220
|
-
? `/${nextMessage.command.name}
|
|
1219
|
+
? `/${nextMessage.command.name}`
|
|
1221
1220
|
: `${nextMessage.prompt.slice(0, 150)}${nextMessage.prompt.length > 150 ? '...' : ''}`;
|
|
1222
1221
|
await sendThreadMessage(thread, `» **${nextMessage.username}:** ${displayText}`);
|
|
1223
1222
|
// Send the queued message as a new prompt (recursive call)
|