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/cli.js
CHANGED
|
@@ -9,7 +9,9 @@ import { getChannelsWithDescriptions, createDiscordClient, getDatabase, startDis
|
|
|
9
9
|
import { Events, ChannelType, REST, Routes, SlashCommandBuilder, AttachmentBuilder, } from 'discord.js';
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import fs from 'node:fs';
|
|
12
|
+
import * as errore from 'errore';
|
|
12
13
|
import { createLogger } from './logger.js';
|
|
14
|
+
import { uploadFilesToDiscord } from './discord-utils.js';
|
|
13
15
|
import { spawn, spawnSync, execSync } from 'node:child_process';
|
|
14
16
|
import http from 'node:http';
|
|
15
17
|
import { setDataDir, getDataDir, getLockPort } from './config.js';
|
|
@@ -135,7 +137,7 @@ async function registerCommands({ token, appId, userCommands = [], agents = [],
|
|
|
135
137
|
})
|
|
136
138
|
.toJSON(),
|
|
137
139
|
new SlashCommandBuilder()
|
|
138
|
-
.setName('session')
|
|
140
|
+
.setName('new-session')
|
|
139
141
|
.setDescription('Start a new OpenCode session')
|
|
140
142
|
.addStringOption((option) => {
|
|
141
143
|
option.setName('prompt').setDescription('Prompt content for the session').setRequired(true);
|
|
@@ -157,6 +159,17 @@ async function registerCommands({ token, appId, userCommands = [], agents = [],
|
|
|
157
159
|
return option;
|
|
158
160
|
})
|
|
159
161
|
.toJSON(),
|
|
162
|
+
new SlashCommandBuilder()
|
|
163
|
+
.setName('new-worktree')
|
|
164
|
+
.setDescription('Create a new git worktree and start a session thread')
|
|
165
|
+
.addStringOption((option) => {
|
|
166
|
+
option
|
|
167
|
+
.setName('name')
|
|
168
|
+
.setDescription('Name for the worktree (will be formatted: lowercase, spaces to dashes)')
|
|
169
|
+
.setRequired(true);
|
|
170
|
+
return option;
|
|
171
|
+
})
|
|
172
|
+
.toJSON(),
|
|
160
173
|
new SlashCommandBuilder()
|
|
161
174
|
.setName('add-project')
|
|
162
175
|
.setDescription('Create Discord channels for a new OpenCode project')
|
|
@@ -256,8 +269,8 @@ async function registerCommands({ token, appId, userCommands = [], agents = [],
|
|
|
256
269
|
.toJSON());
|
|
257
270
|
}
|
|
258
271
|
// Add agent-specific quick commands like /plan-agent, /build-agent
|
|
259
|
-
// Filter to primary/all mode agents (same as /agent command shows)
|
|
260
|
-
const primaryAgents = agents.filter((a) => a.mode === 'primary' || a.mode === 'all');
|
|
272
|
+
// Filter to primary/all mode agents (same as /agent command shows), excluding hidden agents
|
|
273
|
+
const primaryAgents = agents.filter((a) => (a.mode === 'primary' || a.mode === 'all') && !a.hidden);
|
|
261
274
|
for (const agent of primaryAgents) {
|
|
262
275
|
const sanitizedName = sanitizeAgentName(agent.name);
|
|
263
276
|
const commandName = `${sanitizedName}-agent`;
|
|
@@ -432,7 +445,12 @@ async function run({ restart, addChannels }) {
|
|
|
432
445
|
// This is the biggest startup bottleneck (can take 1-30 seconds to spawn and wait for ready)
|
|
433
446
|
const currentDir = process.cwd();
|
|
434
447
|
s.start('Starting OpenCode server...');
|
|
435
|
-
const opencodePromise = initializeOpencodeForDirectory(currentDir)
|
|
448
|
+
const opencodePromise = initializeOpencodeForDirectory(currentDir).then((result) => {
|
|
449
|
+
if (result instanceof Error) {
|
|
450
|
+
throw new Error(result.message);
|
|
451
|
+
}
|
|
452
|
+
return result;
|
|
453
|
+
});
|
|
436
454
|
s.message('Connecting to Discord...');
|
|
437
455
|
const discordClient = await createDiscordClient();
|
|
438
456
|
const guilds = [];
|
|
@@ -735,25 +753,11 @@ cli
|
|
|
735
753
|
}
|
|
736
754
|
const s = spinner();
|
|
737
755
|
s.start(`Uploading ${resolvedFiles.length} file(s)...`);
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
}));
|
|
744
|
-
formData.append('files[0]', new Blob([buffer]), path.basename(file));
|
|
745
|
-
const response = await fetch(`https://discord.com/api/v10/channels/${threadRow.thread_id}/messages`, {
|
|
746
|
-
method: 'POST',
|
|
747
|
-
headers: {
|
|
748
|
-
Authorization: `Bot ${botRow.token}`,
|
|
749
|
-
},
|
|
750
|
-
body: formData,
|
|
751
|
-
});
|
|
752
|
-
if (!response.ok) {
|
|
753
|
-
const error = await response.text();
|
|
754
|
-
throw new Error(`Discord API error: ${response.status} - ${error}`);
|
|
755
|
-
}
|
|
756
|
-
}
|
|
756
|
+
await uploadFilesToDiscord({
|
|
757
|
+
threadId: threadRow.thread_id,
|
|
758
|
+
botToken: botRow.token,
|
|
759
|
+
files: resolvedFiles,
|
|
760
|
+
});
|
|
757
761
|
s.stop(`Uploaded ${resolvedFiles.length} file(s)!`);
|
|
758
762
|
note(`Files uploaded to Discord thread!\n\nFiles: ${resolvedFiles.map((f) => path.basename(f)).join(', ')}`, 'ā
Success');
|
|
759
763
|
process.exit(0);
|
package/dist/commands/abort.js
CHANGED
|
@@ -5,6 +5,7 @@ import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
|
5
5
|
import { resolveTextChannel, getKimakiMetadata, SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
|
|
6
6
|
import { abortControllers } from '../session-handler.js';
|
|
7
7
|
import { createLogger } from '../logger.js';
|
|
8
|
+
import * as errore from 'errore';
|
|
8
9
|
const logger = createLogger('ABORT');
|
|
9
10
|
export async function handleAbortCommand({ command }) {
|
|
10
11
|
const channel = command.channel;
|
|
@@ -51,13 +52,21 @@ export async function handleAbortCommand({ command }) {
|
|
|
51
52
|
return;
|
|
52
53
|
}
|
|
53
54
|
const sessionId = row.session_id;
|
|
55
|
+
const existingController = abortControllers.get(sessionId);
|
|
56
|
+
if (existingController) {
|
|
57
|
+
existingController.abort(new Error('User requested abort'));
|
|
58
|
+
abortControllers.delete(sessionId);
|
|
59
|
+
}
|
|
60
|
+
const getClient = await initializeOpencodeForDirectory(directory);
|
|
61
|
+
if (getClient instanceof Error) {
|
|
62
|
+
await command.reply({
|
|
63
|
+
content: `Failed to abort: ${getClient.message}`,
|
|
64
|
+
ephemeral: true,
|
|
65
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
66
|
+
});
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
54
69
|
try {
|
|
55
|
-
const existingController = abortControllers.get(sessionId);
|
|
56
|
-
if (existingController) {
|
|
57
|
-
existingController.abort(new Error('User requested abort'));
|
|
58
|
-
abortControllers.delete(sessionId);
|
|
59
|
-
}
|
|
60
|
-
const getClient = await initializeOpencodeForDirectory(directory);
|
|
61
70
|
await getClient().session.abort({
|
|
62
71
|
path: { id: sessionId },
|
|
63
72
|
});
|
|
@@ -6,6 +6,7 @@ import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
|
6
6
|
import { createProjectChannels } from '../channel-management.js';
|
|
7
7
|
import { createLogger } from '../logger.js';
|
|
8
8
|
import { abbreviatePath } from '../utils.js';
|
|
9
|
+
import * as errore from 'errore';
|
|
9
10
|
const logger = createLogger('ADD-PROJECT');
|
|
10
11
|
export async function handleAddProjectCommand({ command, appId }) {
|
|
11
12
|
await command.deferReply({ ephemeral: false });
|
|
@@ -18,6 +19,10 @@ export async function handleAddProjectCommand({ command, appId }) {
|
|
|
18
19
|
try {
|
|
19
20
|
const currentDir = process.cwd();
|
|
20
21
|
const getClient = await initializeOpencodeForDirectory(currentDir);
|
|
22
|
+
if (getClient instanceof Error) {
|
|
23
|
+
await command.editReply(getClient.message);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
21
26
|
const projectsResponse = await getClient().project.list({});
|
|
22
27
|
if (!projectsResponse.data) {
|
|
23
28
|
await command.editReply('Failed to fetch projects');
|
|
@@ -60,6 +65,10 @@ export async function handleAddProjectAutocomplete({ interaction, appId, }) {
|
|
|
60
65
|
try {
|
|
61
66
|
const currentDir = process.cwd();
|
|
62
67
|
const getClient = await initializeOpencodeForDirectory(currentDir);
|
|
68
|
+
if (getClient instanceof Error) {
|
|
69
|
+
await interaction.respond([]);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
63
72
|
const projectsResponse = await getClient().project.list({});
|
|
64
73
|
if (!projectsResponse.data) {
|
|
65
74
|
await interaction.respond([]);
|
package/dist/commands/agent.js
CHANGED
|
@@ -6,6 +6,7 @@ import { getDatabase, setChannelAgent, setSessionAgent, clearSessionModel, runMo
|
|
|
6
6
|
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
7
7
|
import { resolveTextChannel, getKimakiMetadata } from '../discord-utils.js';
|
|
8
8
|
import { createLogger } from '../logger.js';
|
|
9
|
+
import * as errore from 'errore';
|
|
9
10
|
const agentLogger = createLogger('AGENT');
|
|
10
11
|
const pendingAgentContexts = new Map();
|
|
11
12
|
/**
|
|
@@ -102,6 +103,10 @@ export async function handleAgentCommand({ interaction, appId, }) {
|
|
|
102
103
|
}
|
|
103
104
|
try {
|
|
104
105
|
const getClient = await initializeOpencodeForDirectory(context.dir);
|
|
106
|
+
if (getClient instanceof Error) {
|
|
107
|
+
await interaction.editReply({ content: getClient.message });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
105
110
|
const agentsResponse = await getClient().app.agents({
|
|
106
111
|
query: { directory: context.dir },
|
|
107
112
|
});
|
|
@@ -110,7 +115,10 @@ export async function handleAgentCommand({ interaction, appId, }) {
|
|
|
110
115
|
return;
|
|
111
116
|
}
|
|
112
117
|
const agents = agentsResponse.data
|
|
113
|
-
.filter((
|
|
118
|
+
.filter((agent) => {
|
|
119
|
+
const hidden = agent.hidden;
|
|
120
|
+
return (agent.mode === 'primary' || agent.mode === 'all') && !hidden;
|
|
121
|
+
})
|
|
114
122
|
.slice(0, 25);
|
|
115
123
|
if (agents.length === 0) {
|
|
116
124
|
await interaction.editReply({ content: 'No primary agents available' });
|
|
@@ -202,6 +210,10 @@ export async function handleQuickAgentCommand({ command, appId, }) {
|
|
|
202
210
|
}
|
|
203
211
|
try {
|
|
204
212
|
const getClient = await initializeOpencodeForDirectory(context.dir);
|
|
213
|
+
if (getClient instanceof Error) {
|
|
214
|
+
await command.editReply({ content: getClient.message });
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
205
217
|
const agentsResponse = await getClient().app.agents({
|
|
206
218
|
query: { directory: context.dir },
|
|
207
219
|
});
|
package/dist/commands/fork.js
CHANGED
|
@@ -5,6 +5,7 @@ import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
|
5
5
|
import { resolveTextChannel, getKimakiMetadata, sendThreadMessage } from '../discord-utils.js';
|
|
6
6
|
import { collectLastAssistantParts } from '../message-formatting.js';
|
|
7
7
|
import { createLogger } from '../logger.js';
|
|
8
|
+
import * as errore from 'errore';
|
|
8
9
|
const sessionLogger = createLogger('SESSION');
|
|
9
10
|
const forkLogger = createLogger('FORK');
|
|
10
11
|
export async function handleForkCommand(interaction) {
|
|
@@ -50,8 +51,14 @@ export async function handleForkCommand(interaction) {
|
|
|
50
51
|
// Defer reply before API calls to avoid 3-second timeout
|
|
51
52
|
await interaction.deferReply({ ephemeral: true });
|
|
52
53
|
const sessionId = row.session_id;
|
|
54
|
+
const getClient = await initializeOpencodeForDirectory(directory);
|
|
55
|
+
if (getClient instanceof Error) {
|
|
56
|
+
await interaction.editReply({
|
|
57
|
+
content: `Failed to load messages: ${getClient.message}`,
|
|
58
|
+
});
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
53
61
|
try {
|
|
54
|
-
const getClient = await initializeOpencodeForDirectory(directory);
|
|
55
62
|
const messagesResponse = await getClient().session.messages({
|
|
56
63
|
path: { id: sessionId },
|
|
57
64
|
});
|
|
@@ -120,8 +127,12 @@ export async function handleForkSelectMenu(interaction) {
|
|
|
120
127
|
return;
|
|
121
128
|
}
|
|
122
129
|
await interaction.deferReply({ ephemeral: false });
|
|
130
|
+
const getClient = await initializeOpencodeForDirectory(directory);
|
|
131
|
+
if (getClient instanceof Error) {
|
|
132
|
+
await interaction.editReply(`Failed to fork session: ${getClient.message}`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
123
135
|
try {
|
|
124
|
-
const getClient = await initializeOpencodeForDirectory(directory);
|
|
125
136
|
const forkResponse = await getClient().session.fork({
|
|
126
137
|
path: { id: sessionId },
|
|
127
138
|
body: { messageID: selectedMessageId },
|
package/dist/commands/model.js
CHANGED
|
@@ -6,6 +6,7 @@ import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
|
6
6
|
import { resolveTextChannel, getKimakiMetadata } from '../discord-utils.js';
|
|
7
7
|
import { abortAndRetrySession } from '../session-handler.js';
|
|
8
8
|
import { createLogger } from '../logger.js';
|
|
9
|
+
import * as errore from 'errore';
|
|
9
10
|
const modelLogger = createLogger('MODEL');
|
|
10
11
|
// Store context by hash to avoid customId length limits (Discord max: 100 chars)
|
|
11
12
|
const pendingModelContexts = new Map();
|
|
@@ -77,6 +78,10 @@ export async function handleModelCommand({ interaction, appId, }) {
|
|
|
77
78
|
}
|
|
78
79
|
try {
|
|
79
80
|
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
81
|
+
if (getClient instanceof Error) {
|
|
82
|
+
await interaction.editReply({ content: getClient.message });
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
80
85
|
const providersResponse = await getClient().provider.list({
|
|
81
86
|
query: { directory: projectDirectory },
|
|
82
87
|
});
|
|
@@ -162,6 +167,13 @@ export async function handleProviderSelectMenu(interaction) {
|
|
|
162
167
|
}
|
|
163
168
|
try {
|
|
164
169
|
const getClient = await initializeOpencodeForDirectory(context.dir);
|
|
170
|
+
if (getClient instanceof Error) {
|
|
171
|
+
await interaction.editReply({
|
|
172
|
+
content: getClient.message,
|
|
173
|
+
components: [],
|
|
174
|
+
});
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
165
177
|
const providersResponse = await getClient().provider.list({
|
|
166
178
|
query: { directory: context.dir },
|
|
167
179
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// /remove-project command - Remove Discord channels for a project.
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import * as errore from 'errore';
|
|
3
4
|
import { getDatabase } from '../database.js';
|
|
4
5
|
import { createLogger } from '../logger.js';
|
|
5
6
|
import { abbreviatePath } from '../utils.js';
|
|
@@ -25,20 +26,27 @@ export async function handleRemoveProjectCommand({ command, appId }) {
|
|
|
25
26
|
const deletedChannels = [];
|
|
26
27
|
const failedChannels = [];
|
|
27
28
|
for (const { channel_id, channel_type } of channels) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
const channel = await errore.tryAsync({
|
|
30
|
+
try: () => guild.channels.fetch(channel_id),
|
|
31
|
+
catch: (e) => e,
|
|
32
|
+
});
|
|
33
|
+
if (channel instanceof Error) {
|
|
34
|
+
logger.error(`Failed to fetch channel ${channel_id}:`, channel);
|
|
35
|
+
failedChannels.push(`${channel_type}: ${channel_id}`);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (channel) {
|
|
39
|
+
try {
|
|
31
40
|
await channel.delete(`Removed by /remove-project command`);
|
|
32
41
|
deletedChannels.push(`${channel_type}: ${channel_id}`);
|
|
33
42
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
43
|
+
catch (error) {
|
|
44
|
+
logger.error(`Failed to delete channel ${channel_id}:`, error);
|
|
45
|
+
failedChannels.push(`${channel_type}: ${channel_id}`);
|
|
37
46
|
}
|
|
38
47
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
failedChannels.push(`${channel_type}: ${channel_id}`);
|
|
48
|
+
else {
|
|
49
|
+
deletedChannels.push(`${channel_type}: ${channel_id} (already deleted)`);
|
|
42
50
|
}
|
|
43
51
|
}
|
|
44
52
|
// Remove from database
|
|
@@ -76,14 +84,16 @@ export async function handleRemoveProjectAutocomplete({ interaction, appId, }) {
|
|
|
76
84
|
// Filter to only channels that exist in this guild
|
|
77
85
|
const projectsInGuild = [];
|
|
78
86
|
for (const { directory, channel_id } of allChannels) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
catch {
|
|
87
|
+
const channel = await errore.tryAsync({
|
|
88
|
+
try: () => guild.channels.fetch(channel_id),
|
|
89
|
+
catch: (e) => e,
|
|
90
|
+
});
|
|
91
|
+
if (channel instanceof Error) {
|
|
86
92
|
// Channel not in this guild, skip
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (channel) {
|
|
96
|
+
projectsInGuild.push({ directory, channelId: channel_id });
|
|
87
97
|
}
|
|
88
98
|
}
|
|
89
99
|
const projects = projectsInGuild
|
package/dist/commands/resume.js
CHANGED
|
@@ -7,6 +7,7 @@ import { sendThreadMessage, resolveTextChannel, getKimakiMetadata } from '../dis
|
|
|
7
7
|
import { extractTagsArrays } from '../xml.js';
|
|
8
8
|
import { collectLastAssistantParts } from '../message-formatting.js';
|
|
9
9
|
import { createLogger } from '../logger.js';
|
|
10
|
+
import * as errore from 'errore';
|
|
10
11
|
const logger = createLogger('RESUME');
|
|
11
12
|
export async function handleResumeCommand({ command, appId }) {
|
|
12
13
|
await command.deferReply({ ephemeral: false });
|
|
@@ -41,6 +42,10 @@ export async function handleResumeCommand({ command, appId }) {
|
|
|
41
42
|
}
|
|
42
43
|
try {
|
|
43
44
|
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
45
|
+
if (getClient instanceof Error) {
|
|
46
|
+
await command.editReply(getClient.message);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
44
49
|
const sessionResponse = await getClient().session.get({
|
|
45
50
|
path: { id: sessionId },
|
|
46
51
|
});
|
|
@@ -111,6 +116,10 @@ export async function handleResumeAutocomplete({ interaction, appId, }) {
|
|
|
111
116
|
}
|
|
112
117
|
try {
|
|
113
118
|
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
119
|
+
if (getClient instanceof Error) {
|
|
120
|
+
await interaction.respond([]);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
114
123
|
const sessionsResponse = await getClient().session.list();
|
|
115
124
|
if (!sessionsResponse.data) {
|
|
116
125
|
await interaction.respond([]);
|
package/dist/commands/session.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// /session command - Start a new OpenCode session.
|
|
1
|
+
// /new-session command - Start a new OpenCode session.
|
|
2
2
|
import { ChannelType } from 'discord.js';
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
@@ -8,6 +8,7 @@ import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
|
|
|
8
8
|
import { extractTagsArrays } from '../xml.js';
|
|
9
9
|
import { handleOpencodeSession } from '../session-handler.js';
|
|
10
10
|
import { createLogger } from '../logger.js';
|
|
11
|
+
import * as errore from 'errore';
|
|
11
12
|
const logger = createLogger('SESSION');
|
|
12
13
|
export async function handleSessionCommand({ command, appId }) {
|
|
13
14
|
await command.deferReply({ ephemeral: false });
|
|
@@ -44,6 +45,10 @@ export async function handleSessionCommand({ command, appId }) {
|
|
|
44
45
|
}
|
|
45
46
|
try {
|
|
46
47
|
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
48
|
+
if (getClient instanceof Error) {
|
|
49
|
+
await command.editReply(getClient.message);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
47
52
|
const files = filesString
|
|
48
53
|
.split(',')
|
|
49
54
|
.map((f) => f.trim())
|
|
@@ -102,6 +107,10 @@ async function handleAgentAutocomplete({ interaction, appId }) {
|
|
|
102
107
|
}
|
|
103
108
|
try {
|
|
104
109
|
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
110
|
+
if (getClient instanceof Error) {
|
|
111
|
+
await interaction.respond([]);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
105
114
|
const agentsResponse = await getClient().app.agents({
|
|
106
115
|
query: { directory: projectDirectory },
|
|
107
116
|
});
|
|
@@ -165,6 +174,10 @@ export async function handleSessionAutocomplete({ interaction, appId, }) {
|
|
|
165
174
|
}
|
|
166
175
|
try {
|
|
167
176
|
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
177
|
+
if (getClient instanceof Error) {
|
|
178
|
+
await interaction.respond([]);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
168
181
|
const response = await getClient().find.files({
|
|
169
182
|
query: {
|
|
170
183
|
query: currentQuery || '',
|
package/dist/commands/share.js
CHANGED
|
@@ -4,6 +4,7 @@ import { getDatabase } from '../database.js';
|
|
|
4
4
|
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
5
5
|
import { resolveTextChannel, getKimakiMetadata, SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
|
|
6
6
|
import { createLogger } from '../logger.js';
|
|
7
|
+
import * as errore from 'errore';
|
|
7
8
|
const logger = createLogger('SHARE');
|
|
8
9
|
export async function handleShareCommand({ command }) {
|
|
9
10
|
const channel = command.channel;
|
|
@@ -50,8 +51,16 @@ export async function handleShareCommand({ command }) {
|
|
|
50
51
|
return;
|
|
51
52
|
}
|
|
52
53
|
const sessionId = row.session_id;
|
|
54
|
+
const getClient = await initializeOpencodeForDirectory(directory);
|
|
55
|
+
if (getClient instanceof Error) {
|
|
56
|
+
await command.reply({
|
|
57
|
+
content: `Failed to share session: ${getClient.message}`,
|
|
58
|
+
ephemeral: true,
|
|
59
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
60
|
+
});
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
53
63
|
try {
|
|
54
|
-
const getClient = await initializeOpencodeForDirectory(directory);
|
|
55
64
|
const response = await getClient().session.share({
|
|
56
65
|
path: { id: sessionId },
|
|
57
66
|
});
|
|
@@ -4,6 +4,7 @@ import { getDatabase } from '../database.js';
|
|
|
4
4
|
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
5
5
|
import { resolveTextChannel, getKimakiMetadata, SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
|
|
6
6
|
import { createLogger } from '../logger.js';
|
|
7
|
+
import * as errore from 'errore';
|
|
7
8
|
const logger = createLogger('UNDO-REDO');
|
|
8
9
|
export async function handleUndoCommand({ command }) {
|
|
9
10
|
const channel = command.channel;
|
|
@@ -50,9 +51,13 @@ export async function handleUndoCommand({ command }) {
|
|
|
50
51
|
return;
|
|
51
52
|
}
|
|
52
53
|
const sessionId = row.session_id;
|
|
54
|
+
await command.deferReply({ flags: SILENT_MESSAGE_FLAGS });
|
|
55
|
+
const getClient = await initializeOpencodeForDirectory(directory);
|
|
56
|
+
if (getClient instanceof Error) {
|
|
57
|
+
await command.editReply(`Failed to undo: ${getClient.message}`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
53
60
|
try {
|
|
54
|
-
await command.deferReply({ flags: SILENT_MESSAGE_FLAGS });
|
|
55
|
-
const getClient = await initializeOpencodeForDirectory(directory);
|
|
56
61
|
// Fetch messages to find the last assistant message
|
|
57
62
|
const messagesResponse = await getClient().session.messages({
|
|
58
63
|
path: { id: sessionId },
|
|
@@ -133,9 +138,13 @@ export async function handleRedoCommand({ command }) {
|
|
|
133
138
|
return;
|
|
134
139
|
}
|
|
135
140
|
const sessionId = row.session_id;
|
|
141
|
+
await command.deferReply({ flags: SILENT_MESSAGE_FLAGS });
|
|
142
|
+
const getClient = await initializeOpencodeForDirectory(directory);
|
|
143
|
+
if (getClient instanceof Error) {
|
|
144
|
+
await command.editReply(`Failed to redo: ${getClient.message}`);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
136
147
|
try {
|
|
137
|
-
await command.deferReply({ flags: SILENT_MESSAGE_FLAGS });
|
|
138
|
-
const getClient = await initializeOpencodeForDirectory(directory);
|
|
139
148
|
// Check if session has reverted state
|
|
140
149
|
const sessionResponse = await getClient().session.get({
|
|
141
150
|
path: { id: sessionId },
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// Worktree management command: /new-worktree
|
|
2
|
+
// Uses OpenCode SDK v2 to create worktrees with kimaki- prefix
|
|
3
|
+
// Creates thread immediately, then worktree in background so user can type
|
|
4
|
+
import { ChannelType } from 'discord.js';
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import { createPendingWorktree, setWorktreeReady, setWorktreeError, } from '../database.js';
|
|
7
|
+
import { initializeOpencodeForDirectory, getOpencodeClientV2 } from '../opencode.js';
|
|
8
|
+
import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
|
|
9
|
+
import { extractTagsArrays } from '../xml.js';
|
|
10
|
+
import { createLogger } from '../logger.js';
|
|
11
|
+
import * as errore from 'errore';
|
|
12
|
+
const logger = createLogger('WORKTREE');
|
|
13
|
+
class WorktreeError extends Error {
|
|
14
|
+
constructor(message, options) {
|
|
15
|
+
super(message, options);
|
|
16
|
+
this.name = 'WorktreeError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Format worktree name: lowercase, spaces to dashes, remove special chars, add kimaki- prefix.
|
|
21
|
+
* "My Feature" ā "kimaki-my-feature"
|
|
22
|
+
*/
|
|
23
|
+
function formatWorktreeName(name) {
|
|
24
|
+
const formatted = name
|
|
25
|
+
.toLowerCase()
|
|
26
|
+
.trim()
|
|
27
|
+
.replace(/\s+/g, '-')
|
|
28
|
+
.replace(/[^a-z0-9-]/g, '');
|
|
29
|
+
return `kimaki-${formatted}`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Get project directory from channel topic.
|
|
33
|
+
*/
|
|
34
|
+
function getProjectDirectoryFromChannel(channel, appId) {
|
|
35
|
+
if (!channel.topic) {
|
|
36
|
+
return new WorktreeError('This channel has no topic configured');
|
|
37
|
+
}
|
|
38
|
+
const extracted = extractTagsArrays({
|
|
39
|
+
xml: channel.topic,
|
|
40
|
+
tags: ['kimaki.directory', 'kimaki.app'],
|
|
41
|
+
});
|
|
42
|
+
const projectDirectory = extracted['kimaki.directory']?.[0]?.trim();
|
|
43
|
+
const channelAppId = extracted['kimaki.app']?.[0]?.trim();
|
|
44
|
+
if (channelAppId && channelAppId !== appId) {
|
|
45
|
+
return new WorktreeError('This channel is not configured for this bot');
|
|
46
|
+
}
|
|
47
|
+
if (!projectDirectory) {
|
|
48
|
+
return new WorktreeError('This channel is not configured with a project directory');
|
|
49
|
+
}
|
|
50
|
+
if (!fs.existsSync(projectDirectory)) {
|
|
51
|
+
return new WorktreeError(`Directory does not exist: ${projectDirectory}`);
|
|
52
|
+
}
|
|
53
|
+
return projectDirectory;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Create worktree in background and update starter message when done.
|
|
57
|
+
*/
|
|
58
|
+
async function createWorktreeInBackground({ thread, starterMessage, worktreeName, projectDirectory, clientV2, }) {
|
|
59
|
+
// Create worktree using SDK v2
|
|
60
|
+
logger.log(`Creating worktree "${worktreeName}" for project ${projectDirectory}`);
|
|
61
|
+
const worktreeResult = await errore.tryAsync({
|
|
62
|
+
try: async () => {
|
|
63
|
+
const response = await clientV2.worktree.create({
|
|
64
|
+
directory: projectDirectory,
|
|
65
|
+
worktreeCreateInput: {
|
|
66
|
+
name: worktreeName,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
if (response.error) {
|
|
70
|
+
throw new Error(`SDK error: ${JSON.stringify(response.error)}`);
|
|
71
|
+
}
|
|
72
|
+
if (!response.data) {
|
|
73
|
+
throw new Error('No worktree data returned from SDK');
|
|
74
|
+
}
|
|
75
|
+
return response.data;
|
|
76
|
+
},
|
|
77
|
+
catch: (e) => new WorktreeError('Failed to create worktree', { cause: e }),
|
|
78
|
+
});
|
|
79
|
+
if (errore.isError(worktreeResult)) {
|
|
80
|
+
const errorMsg = worktreeResult.message;
|
|
81
|
+
logger.error('[NEW-WORKTREE] Error:', worktreeResult.cause);
|
|
82
|
+
setWorktreeError({ threadId: thread.id, errorMessage: errorMsg });
|
|
83
|
+
await starterMessage.edit(`š³ **Worktree: ${worktreeName}**\nā ${errorMsg}`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// Success - update database and edit starter message
|
|
87
|
+
setWorktreeReady({ threadId: thread.id, worktreeDirectory: worktreeResult.directory });
|
|
88
|
+
await starterMessage.edit(`š³ **Worktree: ${worktreeName}**\n` +
|
|
89
|
+
`š \`${worktreeResult.directory}\`\n` +
|
|
90
|
+
`šæ Branch: \`${worktreeResult.branch}\``);
|
|
91
|
+
}
|
|
92
|
+
export async function handleNewWorktreeCommand({ command, appId, }) {
|
|
93
|
+
await command.deferReply({ ephemeral: false });
|
|
94
|
+
const rawName = command.options.getString('name', true);
|
|
95
|
+
const worktreeName = formatWorktreeName(rawName);
|
|
96
|
+
if (worktreeName === 'kimaki-') {
|
|
97
|
+
await command.editReply('Invalid worktree name. Please use letters, numbers, and spaces.');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const channel = command.channel;
|
|
101
|
+
if (!channel || channel.type !== ChannelType.GuildText) {
|
|
102
|
+
await command.editReply('This command can only be used in text channels');
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const textChannel = channel;
|
|
106
|
+
const projectDirectory = getProjectDirectoryFromChannel(textChannel, appId);
|
|
107
|
+
if (errore.isError(projectDirectory)) {
|
|
108
|
+
await command.editReply(projectDirectory.message);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
// Initialize opencode and check if worktree already exists
|
|
112
|
+
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
113
|
+
if (errore.isError(getClient)) {
|
|
114
|
+
await command.editReply(`Failed to initialize OpenCode: ${getClient.message}`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const clientV2 = getOpencodeClientV2(projectDirectory);
|
|
118
|
+
if (!clientV2) {
|
|
119
|
+
await command.editReply('Failed to get OpenCode client');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
// Check if worktree with this name already exists
|
|
123
|
+
// SDK returns array of directory paths like "~/.opencode/worktree/abc/kimaki-my-feature"
|
|
124
|
+
const listResult = await errore.tryAsync({
|
|
125
|
+
try: async () => {
|
|
126
|
+
const response = await clientV2.worktree.list({ directory: projectDirectory });
|
|
127
|
+
return response.data || [];
|
|
128
|
+
},
|
|
129
|
+
catch: (e) => new WorktreeError('Failed to list worktrees', { cause: e }),
|
|
130
|
+
});
|
|
131
|
+
if (errore.isError(listResult)) {
|
|
132
|
+
await command.editReply(listResult.message);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
// Check if any worktree path ends with our name
|
|
136
|
+
const existingWorktree = listResult.find((dir) => dir.endsWith(`/${worktreeName}`));
|
|
137
|
+
if (existingWorktree) {
|
|
138
|
+
await command.editReply(`Worktree \`${worktreeName}\` already exists at \`${existingWorktree}\``);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
// Create thread immediately so user can start typing
|
|
142
|
+
const result = await errore.tryAsync({
|
|
143
|
+
try: async () => {
|
|
144
|
+
const starterMessage = await textChannel.send({
|
|
145
|
+
content: `š³ **Creating worktree: ${worktreeName}**\nā³ Setting up...`,
|
|
146
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
147
|
+
});
|
|
148
|
+
const thread = await starterMessage.startThread({
|
|
149
|
+
name: `worktree: ${worktreeName}`,
|
|
150
|
+
autoArchiveDuration: 1440,
|
|
151
|
+
reason: 'Worktree session',
|
|
152
|
+
});
|
|
153
|
+
return { thread, starterMessage };
|
|
154
|
+
},
|
|
155
|
+
catch: (e) => new WorktreeError('Failed to create thread', { cause: e }),
|
|
156
|
+
});
|
|
157
|
+
if (errore.isError(result)) {
|
|
158
|
+
logger.error('[NEW-WORKTREE] Error:', result.cause);
|
|
159
|
+
await command.editReply(result.message);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const { thread, starterMessage } = result;
|
|
163
|
+
// Store pending worktree in database
|
|
164
|
+
createPendingWorktree({
|
|
165
|
+
threadId: thread.id,
|
|
166
|
+
worktreeName,
|
|
167
|
+
projectDirectory,
|
|
168
|
+
});
|
|
169
|
+
await command.editReply(`Creating worktree in ${thread.toString()}`);
|
|
170
|
+
// Create worktree in background (don't await)
|
|
171
|
+
createWorktreeInBackground({
|
|
172
|
+
thread,
|
|
173
|
+
starterMessage,
|
|
174
|
+
worktreeName,
|
|
175
|
+
projectDirectory,
|
|
176
|
+
clientV2,
|
|
177
|
+
}).catch((e) => {
|
|
178
|
+
logger.error('[NEW-WORKTREE] Background error:', e);
|
|
179
|
+
});
|
|
180
|
+
}
|