kimaki 0.14.0 → 0.16.0

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.
Files changed (101) hide show
  1. package/dist/cli-commands/send.js +46 -15
  2. package/dist/cli-commands/session.js +53 -11
  3. package/dist/cli-runner.js +29 -3
  4. package/dist/commands/abort.js +9 -6
  5. package/dist/commands/add-dir.js +1 -1
  6. package/dist/commands/btw.js +27 -16
  7. package/dist/commands/compact.js +1 -1
  8. package/dist/commands/context-usage.js +16 -9
  9. package/dist/commands/fork.js +9 -11
  10. package/dist/commands/login.js +2 -9
  11. package/dist/commands/new-worktree.js +116 -129
  12. package/dist/commands/permissions.js +10 -6
  13. package/dist/commands/remove-project.js +5 -9
  14. package/dist/commands/undo-redo.js +17 -9
  15. package/dist/context-awareness-plugin.js +135 -147
  16. package/dist/discord-bot.js +38 -16
  17. package/dist/discord-command-registration.js +8 -0
  18. package/dist/discord-utils.js +23 -36
  19. package/dist/errors.js +48 -30
  20. package/dist/external-opencode-sync.js +2 -4
  21. package/dist/forum-sync/markdown.js +1 -4
  22. package/dist/genai-worker.js +3 -5
  23. package/dist/hrana-server.js +5 -8
  24. package/dist/html-components.js +2 -4
  25. package/dist/ipc-polling.js +11 -18
  26. package/dist/markdown.js +95 -100
  27. package/dist/memory-overview-plugin.js +45 -50
  28. package/dist/message-formatting.js +4 -9
  29. package/dist/message-preprocessing.js +5 -6
  30. package/dist/openai-auth-plugin.js +1 -0
  31. package/dist/opencode.js +50 -62
  32. package/dist/plugin-logger.js +37 -0
  33. package/dist/plugin-opencode-client.js +11 -0
  34. package/dist/session-handler/agent-utils.js +3 -4
  35. package/dist/session-handler/global-event-listener.js +8 -7
  36. package/dist/session-handler/model-utils.js +7 -10
  37. package/dist/session-handler/opencode-session-event-log.js +5 -7
  38. package/dist/session-handler/thread-session-runtime.js +163 -229
  39. package/dist/skill-filter.js +12 -0
  40. package/dist/skill-filter.test.js +22 -1
  41. package/dist/subagent-rate-limit-plugin.js +18 -20
  42. package/dist/system-message.js +10 -1
  43. package/dist/system-message.test.js +10 -1
  44. package/dist/task-runner.js +4 -8
  45. package/dist/task-schedule.js +21 -34
  46. package/dist/thread-message-queue.e2e.test.js +3 -0
  47. package/dist/voice-handler.js +10 -17
  48. package/dist/voice.js +8 -13
  49. package/dist/worktrees.js +40 -76
  50. package/package.json +8 -8
  51. package/skills/holocron/SKILL.md +8 -0
  52. package/src/cli-commands/send.ts +50 -15
  53. package/src/cli-commands/session.ts +66 -7
  54. package/src/cli-runner.ts +32 -2
  55. package/src/commands/abort.ts +9 -6
  56. package/src/commands/add-dir.ts +1 -1
  57. package/src/commands/btw.ts +34 -24
  58. package/src/commands/compact.ts +1 -1
  59. package/src/commands/context-usage.ts +18 -9
  60. package/src/commands/fork.ts +7 -6
  61. package/src/commands/login.ts +2 -8
  62. package/src/commands/new-worktree.ts +65 -85
  63. package/src/commands/permissions.ts +9 -8
  64. package/src/commands/remove-project.ts +6 -9
  65. package/src/commands/undo-redo.ts +17 -9
  66. package/src/context-awareness-plugin.ts +25 -38
  67. package/src/discord-bot.ts +46 -18
  68. package/src/discord-command-registration.ts +11 -0
  69. package/src/discord-utils.ts +16 -29
  70. package/src/errors.ts +49 -30
  71. package/src/external-opencode-sync.ts +2 -6
  72. package/src/forum-sync/markdown.ts +4 -4
  73. package/src/genai-worker.ts +4 -5
  74. package/src/hrana-server.ts +4 -4
  75. package/src/html-components.ts +2 -6
  76. package/src/ipc-polling.ts +9 -10
  77. package/src/markdown.ts +103 -110
  78. package/src/memory-overview-plugin.ts +9 -13
  79. package/src/message-formatting.ts +5 -9
  80. package/src/message-preprocessing.ts +5 -6
  81. package/src/openai-auth-plugin.ts +1 -0
  82. package/src/opencode.ts +34 -38
  83. package/src/plugin-logger.ts +55 -0
  84. package/src/plugin-opencode-client.ts +11 -0
  85. package/src/queue-advanced-e2e-setup.ts +3 -0
  86. package/src/session-handler/agent-utils.ts +4 -4
  87. package/src/session-handler/global-event-listener.ts +9 -7
  88. package/src/session-handler/model-utils.ts +7 -12
  89. package/src/session-handler/opencode-session-event-log.ts +6 -7
  90. package/src/session-handler/thread-session-runtime.ts +173 -232
  91. package/src/skill-filter.test.ts +28 -1
  92. package/src/skill-filter.ts +21 -0
  93. package/src/subagent-rate-limit-plugin.ts +6 -7
  94. package/src/system-message.test.ts +10 -1
  95. package/src/system-message.ts +10 -1
  96. package/src/task-runner.ts +4 -12
  97. package/src/task-schedule.ts +16 -24
  98. package/src/thread-message-queue.e2e.test.ts +4 -0
  99. package/src/voice-handler.ts +9 -16
  100. package/src/voice.ts +7 -12
  101. package/src/worktrees.ts +52 -106
@@ -3,6 +3,7 @@
3
3
  // Creates thread immediately, then worktree in background so user can type
4
4
  import { ChannelType, REST, } from 'discord.js';
5
5
  import fs from 'node:fs';
6
+ import { OpenCodeSdkError } from '../errors.js';
6
7
  import { createPendingWorktree, setWorktreeReady, setWorktreeError, getChannelDirectory, getThreadSession, setThreadSession, } from '../database.js';
7
8
  import { SILENT_MESSAGE_FLAGS, reactToThread, resolveProjectDirectoryFromAutocomplete, resolveTextChannel, sendThreadMessage, } from '../discord-utils.js';
8
9
  import { createLogger, LogPrefix } from '../logger.js';
@@ -149,72 +150,67 @@ async function getProjectDirectoryFromChannel(channel) {
149
150
  * Never throws — all internal errors are caught and returned as Error values.
150
151
  */
151
152
  export async function createWorktreeInBackground({ thread, starterMessage, worktreeName, projectDirectory, baseBranch, rest, }) {
152
- return errore.tryAsync({
153
- try: async () => {
154
- logger.log(`Creating worktree "${worktreeName}" for project ${projectDirectory}${baseBranch ? ` from ${baseBranch}` : ''}`);
155
- await createPendingWorktree({
156
- threadId: thread.id,
157
- worktreeName,
158
- projectDirectory,
159
- });
160
- // Serialize status message edits so onProgress can't overwrite the
161
- // final success/error edit even if Discord's API is slow.
162
- let editChain = Promise.resolve();
163
- const editStatus = (content) => {
164
- editChain = editChain
165
- .then(async () => {
166
- await starterMessage?.edit(content);
167
- })
168
- .catch(() => { });
169
- };
170
- const worktreeResult = await createWorktreeWithSubmodules({
171
- directory: projectDirectory,
172
- name: worktreeName,
173
- baseBranch,
174
- onProgress: (phase) => {
175
- editStatus(`🌳 **Worktree: ${worktreeName}**\n${phase}`);
176
- },
177
- });
178
- if (worktreeResult instanceof Error) {
179
- const errorMsg = worktreeResult.message;
180
- logger.error('[WORKTREE] Creation failed:', worktreeResult);
181
- await setWorktreeError({ threadId: thread.id, errorMessage: errorMsg });
182
- editStatus(`🌳 **Worktree: ${worktreeName}**\n❌ ${errorMsg}`);
183
- await editChain;
184
- return worktreeResult;
185
- }
186
- // Success - update database and edit starter message
187
- await setWorktreeReady({
188
- threadId: thread.id,
189
- worktreeDirectory: worktreeResult.directory,
190
- });
191
- // React with tree emoji to mark as worktree thread
192
- await reactToThread({
193
- rest,
194
- threadId: thread.id,
195
- channelId: thread.parentId || undefined,
196
- emoji: '🌳',
197
- });
198
- editStatus(`🌳 **Worktree: ${worktreeName}**\n` +
199
- `📁 \`${worktreeResult.directory}\`\n` +
200
- `🌿 Branch: \`${worktreeResult.branch}\``);
153
+ return (async () => {
154
+ logger.log(`Creating worktree "${worktreeName}" for project ${projectDirectory}${baseBranch ? ` from ${baseBranch}` : ''}`);
155
+ // Serialize status message edits so onProgress can't overwrite the
156
+ // final success/error edit even if Discord's API is slow.
157
+ let editChain = Promise.resolve();
158
+ const editStatus = (content) => {
159
+ editChain = editChain
160
+ .then(async () => {
161
+ await starterMessage?.edit(content);
162
+ })
163
+ .catch(() => { });
164
+ };
165
+ // DB pending entry must complete before git creation so error paths
166
+ // (setWorktreeError) can find the row reliably
167
+ await createPendingWorktree({
168
+ threadId: thread.id,
169
+ worktreeName,
170
+ projectDirectory,
171
+ });
172
+ const worktreeResult = await createWorktreeWithSubmodules({
173
+ directory: projectDirectory,
174
+ name: worktreeName,
175
+ baseBranch,
176
+ onProgress: (phase) => {
177
+ editStatus(`🌳 **Worktree: ${worktreeName}**\n${phase}`);
178
+ },
179
+ });
180
+ if (worktreeResult instanceof Error) {
181
+ const errorMsg = worktreeResult.message;
182
+ logger.error('[WORKTREE] Creation failed:', worktreeResult);
183
+ await setWorktreeError({ threadId: thread.id, errorMessage: errorMsg });
184
+ editStatus(`🌳 **Worktree: ${worktreeName}**\n❌ ${errorMsg}`);
201
185
  await editChain;
202
- return worktreeResult.directory;
203
- },
204
- catch: (e) => {
205
- logger.error('[WORKTREE] Unexpected error in createWorktreeInBackground:', e);
206
- return new Error(`Worktree creation failed: ${e instanceof Error ? e.message : String(e)}`, { cause: e });
207
- },
186
+ return worktreeResult;
187
+ }
188
+ // DB ready update is critical; reaction is best-effort
189
+ await setWorktreeReady({
190
+ threadId: thread.id,
191
+ worktreeDirectory: worktreeResult.directory,
192
+ });
193
+ void reactToThread({
194
+ rest,
195
+ threadId: thread.id,
196
+ channelId: thread.parentId || undefined,
197
+ emoji: '🌳',
198
+ }).catch(() => { });
199
+ editStatus(`🌳 **Worktree: ${worktreeName}**\n` +
200
+ `📁 \`${worktreeResult.directory}\`\n` +
201
+ `🌿 Branch: \`${worktreeResult.branch}\``);
202
+ await editChain;
203
+ return worktreeResult.directory;
204
+ })().catch((e) => {
205
+ logger.error('[WORKTREE] Unexpected error in createWorktreeInBackground:', e);
206
+ return new Error(`Worktree creation failed: ${e instanceof Error ? e.message : String(e)}`, { cause: e });
208
207
  });
209
208
  }
210
209
  async function findExistingWorktreePath({ projectDirectory, worktreeName, }) {
211
- const listResult = await errore.tryAsync({
212
- try: () => execAsync('git worktree list --porcelain', { cwd: projectDirectory }),
213
- catch: (e) => new WorktreeError('Failed to list worktrees', { cause: e }),
214
- });
215
- if (errore.isError(listResult)) {
210
+ const listResult = await execAsync('git worktree list --porcelain', { cwd: projectDirectory })
211
+ .catch((e) => new WorktreeError('Failed to list worktrees', { cause: e }));
212
+ if (listResult instanceof Error)
216
213
  return listResult;
217
- }
218
214
  const lines = listResult.stdout.split('\n');
219
215
  let currentPath = '';
220
216
  const branchRef = `refs/heads/${worktreeName}`;
@@ -268,18 +264,15 @@ export async function handleNewWorktreeCommand({ command, appId, }) {
268
264
  await command.editReply(projectDirectory.message);
269
265
  return;
270
266
  }
271
- const baseBranch = await resolveRequestedWorktreeBaseRef({
272
- projectDirectory,
273
- rawBaseBranch,
274
- });
267
+ // Parallelize: base branch validation and existing worktree check are independent
268
+ const [baseBranch, existingWorktree] = await Promise.all([
269
+ resolveRequestedWorktreeBaseRef({ projectDirectory, rawBaseBranch }),
270
+ findExistingWorktreePath({ projectDirectory, worktreeName }),
271
+ ]);
275
272
  if (baseBranch instanceof Error) {
276
273
  await command.editReply(`Invalid base branch: \`${rawBaseBranch}\``);
277
274
  return;
278
275
  }
279
- const existingWorktree = await findExistingWorktreePath({
280
- projectDirectory,
281
- worktreeName,
282
- });
283
276
  if (errore.isError(existingWorktree)) {
284
277
  await command.editReply(existingWorktree.message);
285
278
  return;
@@ -289,30 +282,29 @@ export async function handleNewWorktreeCommand({ command, appId, }) {
289
282
  return;
290
283
  }
291
284
  // Create thread immediately so user can start typing
292
- const result = await errore.tryAsync({
293
- try: async () => {
294
- const starterMessage = await channel.send({
295
- content: worktreeCreatingMessage(worktreeName),
296
- flags: SILENT_MESSAGE_FLAGS,
297
- });
298
- const thread = await starterMessage.startThread({
299
- name: `${WORKTREE_PREFIX}worktree: ${worktreeName}`,
300
- autoArchiveDuration: 1440,
301
- reason: 'Worktree session',
302
- });
303
- // Add user to thread so it appears in their sidebar
304
- await thread.members.add(command.user.id);
305
- return { thread, starterMessage };
306
- },
307
- catch: (e) => new WorktreeError('Failed to create thread', { cause: e }),
308
- });
309
- if (errore.isError(result)) {
285
+ const result = await (async () => {
286
+ const starterMessage = await channel.send({
287
+ content: worktreeCreatingMessage(worktreeName),
288
+ flags: SILENT_MESSAGE_FLAGS,
289
+ });
290
+ const thread = await starterMessage.startThread({
291
+ name: `${WORKTREE_PREFIX}worktree: ${worktreeName}`,
292
+ autoArchiveDuration: 1440,
293
+ reason: 'Worktree session',
294
+ });
295
+ // Parallelize: member add and editReply are independent
296
+ await Promise.all([
297
+ thread.members.add(command.user.id),
298
+ command.editReply(`Creating worktree in ${thread.toString()}`),
299
+ ]);
300
+ return { thread, starterMessage };
301
+ })().catch((e) => new WorktreeError('Failed to create thread', { cause: e }));
302
+ if (result instanceof Error) {
310
303
  logger.error('[NEW-WORKTREE] Error:', result.cause);
311
304
  await command.editReply(result.message);
312
305
  return;
313
306
  }
314
307
  const { thread, starterMessage } = result;
315
- await command.editReply(`Creating worktree in ${thread.toString()}`);
316
308
  // Create worktree in background (don't await)
317
309
  void createWorktreeInBackground({
318
310
  thread,
@@ -353,18 +345,18 @@ async function handleWorktreeInThread({ command, thread, appId, }) {
353
345
  await command.editReply(projectDirectory.message);
354
346
  return;
355
347
  }
356
- const baseBranch = await resolveRequestedWorktreeBaseRef({
357
- projectDirectory,
358
- rawBaseBranch,
359
- });
348
+ // Parallelize: base branch validation, existing worktree check, and parent channel
349
+ // resolve are all independent. resolveTextChannel fetches the parent from Discord
350
+ // cache/API which can overlap with the git operations.
351
+ const [baseBranch, existingWorktreePath, textChannel] = await Promise.all([
352
+ resolveRequestedWorktreeBaseRef({ projectDirectory, rawBaseBranch }),
353
+ findExistingWorktreePath({ projectDirectory, worktreeName }),
354
+ resolveTextChannel(thread),
355
+ ]);
360
356
  if (baseBranch instanceof Error) {
361
357
  await command.editReply(`Invalid base branch: \`${rawBaseBranch}\``);
362
358
  return;
363
359
  }
364
- const existingWorktreePath = await findExistingWorktreePath({
365
- projectDirectory,
366
- worktreeName,
367
- });
368
360
  if (errore.isError(existingWorktreePath)) {
369
361
  await command.editReply(existingWorktreePath.message);
370
362
  return;
@@ -373,33 +365,33 @@ async function handleWorktreeInThread({ command, thread, appId, }) {
373
365
  await command.editReply(`Worktree \`${worktreeName}\` already exists at \`${existingWorktreePath}\``);
374
366
  return;
375
367
  }
376
- const textChannel = await resolveTextChannel(thread);
377
368
  if (!textChannel) {
378
369
  await command.editReply('Could not resolve parent text channel');
379
370
  return;
380
371
  }
381
- const threadResult = await errore.tryAsync({
382
- try: async () => {
383
- const worktreeThread = await textChannel.threads.create({
384
- name: `${WORKTREE_PREFIX}worktree: ${worktreeName}`.slice(0, 100),
385
- autoArchiveDuration: 1440,
386
- reason: `Worktree fork from thread ${thread.id}`,
387
- });
388
- await worktreeThread.members.add(command.user.id);
389
- const statusMessage = await worktreeThread.send({
372
+ const threadResult = await (async () => {
373
+ const worktreeThread = await textChannel.threads.create({
374
+ name: `${WORKTREE_PREFIX}worktree: ${worktreeName}`.slice(0, 100),
375
+ autoArchiveDuration: 1440,
376
+ reason: `Worktree fork from thread ${thread.id}`,
377
+ });
378
+ // Parallelize: member add and status message send are independent
379
+ const [, statusMessage] = await Promise.all([
380
+ worktreeThread.members.add(command.user.id),
381
+ worktreeThread.send({
390
382
  content: worktreeCreatingMessage(worktreeName),
391
383
  flags: SILENT_MESSAGE_FLAGS,
392
- });
393
- return { worktreeThread, statusMessage };
394
- },
395
- catch: (e) => new WorktreeError('Failed to create worktree thread', { cause: e }),
396
- });
384
+ }),
385
+ ]);
386
+ return { worktreeThread, statusMessage };
387
+ })().catch((e) => new WorktreeError('Failed to create worktree thread', { cause: e }));
397
388
  if (threadResult instanceof Error) {
398
389
  await command.editReply(threadResult.message);
399
390
  return;
400
391
  }
401
392
  const { worktreeThread, statusMessage } = threadResult;
402
- await command.editReply(`Creating worktree in ${worktreeThread.toString()}`);
393
+ // Fire-and-forget: don't block background worktree creation on editReply
394
+ void command.editReply(`Creating worktree in ${worktreeThread.toString()}`).catch(() => { });
403
395
  void createWorktreeInBackground({
404
396
  thread: worktreeThread,
405
397
  starterMessage: statusMessage,
@@ -409,9 +401,8 @@ async function handleWorktreeInThread({ command, thread, appId, }) {
409
401
  rest: command.client.rest,
410
402
  })
411
403
  .then(async (result) => {
412
- if (result instanceof Error) {
404
+ if (result instanceof Error)
413
405
  return;
414
- }
415
406
  const sourceSessionId = await getThreadSession(thread.id);
416
407
  if (!sourceSessionId) {
417
408
  await sendThreadMessage(worktreeThread, 'Worktree is ready. Send a message here to start a fresh session in this checkout.');
@@ -425,12 +416,10 @@ async function handleWorktreeInThread({ command, thread, appId, }) {
425
416
  await sendThreadMessage(worktreeThread, `✗ Worktree is ready, but failed to initialize OpenCode for context reuse: ${getClient.message}`);
426
417
  return;
427
418
  }
428
- const forkResponse = await errore.tryAsync(() => {
429
- return getClient().session.fork({
430
- sessionID: sourceSessionId,
431
- directory: result,
432
- });
433
- });
419
+ const forkResponse = await getClient().session.fork({
420
+ sessionID: sourceSessionId,
421
+ directory: result,
422
+ }).catch((e) => new OpenCodeSdkError({ operation: 'session.fork', cause: e }));
434
423
  if (forkResponse instanceof Error) {
435
424
  logger.error('[NEW-WORKTREE] Failed to fork session into worktree:', forkResponse);
436
425
  void notifyError(forkResponse, 'Failed to fork session into worktree');
@@ -445,16 +434,14 @@ async function handleWorktreeInThread({ command, thread, appId, }) {
445
434
  await sendThreadMessage(worktreeThread, `✗ Worktree is ready, but failed to reuse session context there: ${error.message}`);
446
435
  return;
447
436
  }
448
- const permissionResponse = await errore.tryAsync(() => {
449
- return getClient().session.update({
450
- sessionID: forkedSession.id,
437
+ const permissionResponse = await getClient().session.update({
438
+ sessionID: forkedSession.id,
439
+ directory: result,
440
+ permission: buildSessionPermissions({
451
441
  directory: result,
452
- permission: buildSessionPermissions({
453
- directory: result,
454
- originalRepoDirectory: projectDirectory,
455
- }),
456
- });
457
- });
442
+ originalRepoDirectory: projectDirectory,
443
+ }),
444
+ }).catch((e) => new OpenCodeSdkError({ operation: 'session.update', cause: e }));
458
445
  if (permissionResponse instanceof Error || permissionResponse.error) {
459
446
  const error = permissionResponse instanceof Error
460
447
  ? permissionResponse
@@ -1,6 +1,11 @@
1
1
  // Permission button handler - Shows buttons for permission requests.
2
2
  // When OpenCode asks for permission, this module renders 3 buttons:
3
3
  // Accept, Accept Always, and Deny.
4
+ //
5
+ // The `directory` stored in PendingPermissionContext is the session directory
6
+ // (sdkDirectory), which equals the worktree path for worktree threads.
7
+ // This is used for both getOpencodeClient() (so the client header matches)
8
+ // and for explicit `directory` params in SDK calls.
4
9
  import { ButtonBuilder, ButtonStyle, ActionRowBuilder, MessageFlags, } from 'discord.js';
5
10
  import crypto from 'node:crypto';
6
11
  import { getOpencodeClient } from '../opencode.js';
@@ -77,13 +82,12 @@ function takePendingPermissionContext(contextHash) {
77
82
  * Displays 3 buttons in a row: Accept, Accept Always, Deny.
78
83
  * Returns the message ID and context hash for tracking.
79
84
  */
80
- export async function showPermissionButtons({ thread, permission, directory, permissionDirectory, subtaskLabel, }) {
85
+ export async function showPermissionButtons({ thread, permission, directory, subtaskLabel, }) {
81
86
  const contextHash = crypto.randomBytes(8).toString('hex');
82
87
  const context = {
83
88
  permission,
84
89
  requestIds: [permission.id],
85
90
  directory,
86
- permissionDirectory,
87
91
  thread,
88
92
  contextHash,
89
93
  };
@@ -111,7 +115,7 @@ export async function showPermissionButtons({ thread, permission, directory, per
111
115
  await Promise.all(requestIds.map((requestId) => {
112
116
  return client.permission.reply({
113
117
  requestID: requestId,
114
- directory: ctx.permissionDirectory,
118
+ directory: ctx.directory,
115
119
  reply: 'reject',
116
120
  message: timeoutFeedback,
117
121
  });
@@ -207,7 +211,7 @@ export async function cancelPendingPermission(threadId) {
207
211
  const result = await Promise.all(requestIds.map((requestId) => {
208
212
  return client.permission.reply({
209
213
  requestID: requestId,
210
- directory: pendingContext.permissionDirectory,
214
+ directory: pendingContext.directory,
211
215
  reply: 'reject',
212
216
  });
213
217
  })).then(() => {
@@ -266,7 +270,7 @@ export async function handlePermissionButton(interaction) {
266
270
  await Promise.all(requestIds.map((requestId) => {
267
271
  return permClient.permission.reply({
268
272
  requestID: requestId,
269
- directory: context.permissionDirectory,
273
+ directory: context.directory,
270
274
  reply: response,
271
275
  });
272
276
  }));
@@ -274,7 +278,7 @@ export async function handlePermissionButton(interaction) {
274
278
  const resumed = await resumeSessionIfIdleAfterPermission({
275
279
  client: permClient,
276
280
  sessionId: context.permission.sessionID,
277
- directory: context.permissionDirectory,
281
+ directory: context.directory,
278
282
  });
279
283
  if (resumed instanceof Error) {
280
284
  logger.error('Failed to resume idle session after permission:', resumed);
@@ -1,7 +1,7 @@
1
1
  // /remove-project command - Remove Discord channels for a project.
2
2
  import path from 'node:path';
3
- import * as errore from 'errore';
4
3
  import { findChannelsByDirectory, deleteChannelDirectoriesByDirectory, getAllTextChannelDirectories, } from '../database.js';
4
+ import { DiscordOperationError } from '../errors.js';
5
5
  import { createLogger, LogPrefix } from '../logger.js';
6
6
  import { abbreviatePath } from '../utils.js';
7
7
  const logger = createLogger(LogPrefix.REMOVE_PROJECT);
@@ -23,10 +23,8 @@ export async function handleRemoveProjectCommand({ command, appId, }) {
23
23
  const deletedChannels = [];
24
24
  const failedChannels = [];
25
25
  for (const { channel_id, channel_type } of channels) {
26
- const channel = await errore.tryAsync({
27
- try: () => guild.channels.fetch(channel_id),
28
- catch: (e) => e,
29
- });
26
+ const channel = await guild.channels.fetch(channel_id)
27
+ .catch((e) => new DiscordOperationError({ operation: 'fetchChannel', cause: e }));
30
28
  if (channel instanceof Error) {
31
29
  logger.error(`Failed to fetch channel ${channel_id}:`, channel);
32
30
  failedChannels.push(`${channel_type}: ${channel_id}`);
@@ -80,10 +78,8 @@ export async function handleRemoveProjectAutocomplete({ interaction, appId, }) {
80
78
  // Filter to only channels that exist in this guild
81
79
  const projectsInGuild = [];
82
80
  for (const { directory, channel_id } of allChannels) {
83
- const channel = await errore.tryAsync({
84
- try: () => guild.channels.fetch(channel_id),
85
- catch: (e) => e,
86
- });
81
+ const channel = await guild.channels.fetch(channel_id)
82
+ .catch((e) => new DiscordOperationError({ operation: 'fetchChannel', cause: e }));
87
83
  if (channel instanceof Error) {
88
84
  // Channel not in this guild, skip
89
85
  continue;
@@ -1,7 +1,7 @@
1
1
  // Undo/Redo commands - /undo, /redo
2
2
  import { ChannelType, MessageFlags, } from 'discord.js';
3
3
  import { getThreadSession } from '../database.js';
4
- import { initializeOpencodeForDirectory } from '../opencode.js';
4
+ import { getOpencodeClient, initializeOpencodeForDirectory } from '../opencode.js';
5
5
  import { resolveWorkingDirectory, SILENT_MESSAGE_FLAGS, } from '../discord-utils.js';
6
6
  import { createLogger, LogPrefix } from '../logger.js';
7
7
  const logger = createLogger(LogPrefix.UNDO_REDO);
@@ -59,13 +59,17 @@ export async function handleUndoCommand({ command, }) {
59
59
  return;
60
60
  }
61
61
  await command.deferReply();
62
- const getClient = await initializeOpencodeForDirectory(projectDirectory);
63
- if (getClient instanceof Error) {
64
- await command.editReply(`Failed to undo: ${getClient.message}`);
62
+ const serverResult = await initializeOpencodeForDirectory(projectDirectory);
63
+ if (serverResult instanceof Error) {
64
+ await command.editReply(`Failed to undo: ${serverResult.message}`);
65
65
  return;
66
66
  }
67
67
  try {
68
- const client = getClient();
68
+ const client = getOpencodeClient(workingDirectory);
69
+ if (!client) {
70
+ await command.editReply('Failed to get OpenCode client');
71
+ return;
72
+ }
69
73
  // Fetch session to check existing revert state
70
74
  const sessionResponse = await client.session.get({
71
75
  sessionID: sessionId,
@@ -210,13 +214,17 @@ export async function handleRedoCommand({ command, }) {
210
214
  return;
211
215
  }
212
216
  await command.deferReply();
213
- const getClient = await initializeOpencodeForDirectory(projectDirectory);
214
- if (getClient instanceof Error) {
215
- await command.editReply(`Failed to redo: ${getClient.message}`);
217
+ const serverResult = await initializeOpencodeForDirectory(projectDirectory);
218
+ if (serverResult instanceof Error) {
219
+ await command.editReply(`Failed to redo: ${serverResult.message}`);
216
220
  return;
217
221
  }
218
222
  try {
219
- const client = getClient();
223
+ const client = getOpencodeClient(workingDirectory);
224
+ if (!client) {
225
+ await command.editReply('Failed to get OpenCode client');
226
+ return;
227
+ }
220
228
  // Fetch session to check existing revert state
221
229
  const sessionResponse = await client.session.get({
222
230
  sessionID: sessionId,