kimaki 0.4.39 → 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.
Files changed (51) hide show
  1. package/dist/cli.js +19 -21
  2. package/dist/commands/abort.js +1 -1
  3. package/dist/commands/add-project.js +2 -2
  4. package/dist/commands/agent.js +2 -2
  5. package/dist/commands/fork.js +2 -2
  6. package/dist/commands/model.js +2 -2
  7. package/dist/commands/remove-project.js +2 -2
  8. package/dist/commands/resume.js +2 -2
  9. package/dist/commands/session.js +4 -4
  10. package/dist/commands/share.js +1 -1
  11. package/dist/commands/undo-redo.js +2 -2
  12. package/dist/commands/worktree.js +180 -0
  13. package/dist/database.js +49 -1
  14. package/dist/discord-bot.js +29 -4
  15. package/dist/discord-utils.js +36 -0
  16. package/dist/errors.js +86 -87
  17. package/dist/genai-worker.js +1 -1
  18. package/dist/interaction-handler.js +6 -2
  19. package/dist/markdown.js +5 -1
  20. package/dist/message-formatting.js +2 -2
  21. package/dist/opencode.js +4 -4
  22. package/dist/session-handler.js +2 -2
  23. package/dist/tools.js +3 -3
  24. package/dist/voice-handler.js +3 -3
  25. package/dist/voice.js +4 -4
  26. package/package.json +4 -3
  27. package/src/cli.ts +20 -30
  28. package/src/commands/abort.ts +1 -1
  29. package/src/commands/add-project.ts +2 -2
  30. package/src/commands/agent.ts +2 -2
  31. package/src/commands/fork.ts +2 -2
  32. package/src/commands/model.ts +2 -2
  33. package/src/commands/remove-project.ts +2 -2
  34. package/src/commands/resume.ts +2 -2
  35. package/src/commands/session.ts +4 -4
  36. package/src/commands/share.ts +1 -1
  37. package/src/commands/undo-redo.ts +2 -2
  38. package/src/commands/worktree.ts +243 -0
  39. package/src/database.ts +96 -1
  40. package/src/discord-bot.ts +30 -4
  41. package/src/discord-utils.ts +50 -0
  42. package/src/errors.ts +90 -160
  43. package/src/genai-worker.ts +1 -1
  44. package/src/interaction-handler.ts +7 -2
  45. package/src/markdown.ts +5 -4
  46. package/src/message-formatting.ts +2 -2
  47. package/src/opencode.ts +4 -4
  48. package/src/session-handler.ts +2 -2
  49. package/src/tools.ts +3 -3
  50. package/src/voice-handler.ts +3 -3
  51. package/src/voice.ts +4 -4
package/dist/errors.js CHANGED
@@ -1,110 +1,109 @@
1
1
  // TaggedError definitions for type-safe error handling with errore.
2
2
  // Errors are grouped by category: infrastructure, domain, and validation.
3
3
  // Use errore.matchError() for exhaustive error handling in command handlers.
4
- import * as errore from 'errore';
4
+ import { createTaggedError } from 'errore';
5
5
  // ═══════════════════════════════════════════════════════════════════════════
6
6
  // INFRASTRUCTURE ERRORS - Server, filesystem, external services
7
7
  // ═══════════════════════════════════════════════════════════════════════════
8
- export class DirectoryNotAccessibleError extends errore.TaggedError('DirectoryNotAccessibleError')() {
9
- constructor(args) {
10
- super({ ...args, message: `Directory does not exist or is not accessible: ${args.directory}` });
11
- }
12
- }
13
- export class ServerStartError extends errore.TaggedError('ServerStartError')() {
14
- constructor(args) {
15
- super({ ...args, message: `Server failed to start on port ${args.port}: ${args.reason}` });
16
- }
17
- }
18
- export class ServerNotFoundError extends errore.TaggedError('ServerNotFoundError')() {
19
- constructor(args) {
20
- super({ ...args, message: `OpenCode server not found for directory: ${args.directory}` });
21
- }
22
- }
23
- export class ServerNotReadyError extends errore.TaggedError('ServerNotReadyError')() {
24
- constructor(args) {
25
- super({
26
- ...args,
27
- message: `OpenCode server for directory "${args.directory}" is in an error state (no client available)`,
28
- });
29
- }
30
- }
31
- export class ApiKeyMissingError extends errore.TaggedError('ApiKeyMissingError')() {
32
- constructor(args) {
33
- super({ ...args, message: `${args.service} API key is required` });
34
- }
8
+ export class DirectoryNotAccessibleError extends createTaggedError({
9
+ name: 'DirectoryNotAccessibleError',
10
+ message: 'Directory does not exist or is not accessible: $directory',
11
+ }) {
12
+ }
13
+ export class ServerStartError extends createTaggedError({
14
+ name: 'ServerStartError',
15
+ message: 'Server failed to start on port $port: $reason',
16
+ }) {
17
+ }
18
+ export class ServerNotFoundError extends createTaggedError({
19
+ name: 'ServerNotFoundError',
20
+ message: 'OpenCode server not found for directory: $directory',
21
+ }) {
22
+ }
23
+ export class ServerNotReadyError extends createTaggedError({
24
+ name: 'ServerNotReadyError',
25
+ message: 'OpenCode server for directory "$directory" is in an error state (no client available)',
26
+ }) {
27
+ }
28
+ export class ApiKeyMissingError extends createTaggedError({
29
+ name: 'ApiKeyMissingError',
30
+ message: '$service API key is required',
31
+ }) {
35
32
  }
36
33
  // ═══════════════════════════════════════════════════════════════════════════
37
34
  // DOMAIN ERRORS - Sessions, messages, transcription
38
35
  // ═══════════════════════════════════════════════════════════════════════════
39
- export class SessionNotFoundError extends errore.TaggedError('SessionNotFoundError')() {
40
- constructor(args) {
41
- super({ ...args, message: `Session ${args.sessionId} not found` });
42
- }
43
- }
44
- export class SessionCreateError extends errore.TaggedError('SessionCreateError')() {
45
- }
46
- export class MessagesNotFoundError extends errore.TaggedError('MessagesNotFoundError')() {
47
- constructor(args) {
48
- super({ ...args, message: `No messages found for session ${args.sessionId}` });
49
- }
50
- }
51
- export class TranscriptionError extends errore.TaggedError('TranscriptionError')() {
52
- constructor(args) {
53
- super({ ...args, message: `Transcription failed: ${args.reason}` });
54
- }
55
- }
56
- export class GrepSearchError extends errore.TaggedError('GrepSearchError')() {
57
- constructor(args) {
58
- super({ ...args, message: `Grep search failed for pattern: ${args.pattern}` });
59
- }
60
- }
61
- export class GlobSearchError extends errore.TaggedError('GlobSearchError')() {
62
- constructor(args) {
63
- super({ ...args, message: `Glob search failed for pattern: ${args.pattern}` });
64
- }
36
+ export class SessionNotFoundError extends createTaggedError({
37
+ name: 'SessionNotFoundError',
38
+ message: 'Session $sessionId not found',
39
+ }) {
40
+ }
41
+ export class SessionCreateError extends createTaggedError({
42
+ name: 'SessionCreateError',
43
+ message: '$message',
44
+ }) {
45
+ }
46
+ export class MessagesNotFoundError extends createTaggedError({
47
+ name: 'MessagesNotFoundError',
48
+ message: 'No messages found for session $sessionId',
49
+ }) {
50
+ }
51
+ export class TranscriptionError extends createTaggedError({
52
+ name: 'TranscriptionError',
53
+ message: 'Transcription failed: $reason',
54
+ }) {
55
+ }
56
+ export class GrepSearchError extends createTaggedError({
57
+ name: 'GrepSearchError',
58
+ message: 'Grep search failed for pattern: $pattern',
59
+ }) {
60
+ }
61
+ export class GlobSearchError extends createTaggedError({
62
+ name: 'GlobSearchError',
63
+ message: 'Glob search failed for pattern: $pattern',
64
+ }) {
65
65
  }
66
66
  // ═══════════════════════════════════════════════════════════════════════════
67
67
  // VALIDATION ERRORS - Input validation, format checks
68
68
  // ═══════════════════════════════════════════════════════════════════════════
69
- export class InvalidAudioFormatError extends errore.TaggedError('InvalidAudioFormatError')() {
70
- constructor() {
71
- super({ message: 'Invalid audio format' });
72
- }
73
- }
74
- export class EmptyTranscriptionError extends errore.TaggedError('EmptyTranscriptionError')() {
75
- constructor() {
76
- super({ message: 'Model returned empty transcription' });
77
- }
78
- }
79
- export class NoResponseContentError extends errore.TaggedError('NoResponseContentError')() {
80
- constructor() {
81
- super({ message: 'No response content from model' });
82
- }
83
- }
84
- export class NoToolResponseError extends errore.TaggedError('NoToolResponseError')() {
85
- constructor() {
86
- super({ message: 'No valid tool responses' });
87
- }
69
+ export class InvalidAudioFormatError extends createTaggedError({
70
+ name: 'InvalidAudioFormatError',
71
+ message: 'Invalid audio format',
72
+ }) {
73
+ }
74
+ export class EmptyTranscriptionError extends createTaggedError({
75
+ name: 'EmptyTranscriptionError',
76
+ message: 'Model returned empty transcription',
77
+ }) {
78
+ }
79
+ export class NoResponseContentError extends createTaggedError({
80
+ name: 'NoResponseContentError',
81
+ message: 'No response content from model',
82
+ }) {
83
+ }
84
+ export class NoToolResponseError extends createTaggedError({
85
+ name: 'NoToolResponseError',
86
+ message: 'No valid tool responses',
87
+ }) {
88
88
  }
89
89
  // ═══════════════════════════════════════════════════════════════════════════
90
90
  // NETWORK ERRORS - Fetch and HTTP
91
91
  // ═══════════════════════════════════════════════════════════════════════════
92
- export class FetchError extends errore.TaggedError('FetchError')() {
93
- constructor(args) {
94
- const causeMsg = args.cause instanceof Error ? args.cause.message : String(args.cause);
95
- super({ ...args, message: `Fetch failed for ${args.url}: ${causeMsg}` });
96
- }
92
+ export class FetchError extends createTaggedError({
93
+ name: 'FetchError',
94
+ message: 'Fetch failed for $url',
95
+ }) {
97
96
  }
98
97
  // ═══════════════════════════════════════════════════════════════════════════
99
98
  // API ERRORS - External service responses
100
99
  // ═══════════════════════════════════════════════════════════════════════════
101
- export class DiscordApiError extends errore.TaggedError('DiscordApiError')() {
102
- constructor(args) {
103
- super({ ...args, message: `Discord API error: ${args.status}${args.body ? ` - ${args.body}` : ''}` });
104
- }
105
- }
106
- export class OpenCodeApiError extends errore.TaggedError('OpenCodeApiError')() {
107
- constructor(args) {
108
- super({ ...args, message: `OpenCode API error (${args.status})${args.body ? `: ${args.body}` : ''}` });
109
- }
100
+ export class DiscordApiError extends createTaggedError({
101
+ name: 'DiscordApiError',
102
+ message: 'Discord API error: $status $body',
103
+ }) {
104
+ }
105
+ export class OpenCodeApiError extends createTaggedError({
106
+ name: 'OpenCodeApiError',
107
+ message: 'OpenCode API error ($status): $body',
108
+ }) {
110
109
  }
@@ -103,7 +103,7 @@ async function createAssistantAudioLogStream(guildId, channelId) {
103
103
  try: () => mkdir(audioDir, { recursive: true }),
104
104
  catch: (e) => e,
105
105
  });
106
- if (errore.isError(mkdirError)) {
106
+ if (mkdirError instanceof Error) {
107
107
  workerLogger.error(`Failed to create audio log directory:`, mkdirError.message);
108
108
  return null;
109
109
  }
@@ -3,6 +3,7 @@
3
3
  // and manages autocomplete, select menu interactions for the bot.
4
4
  import { Events } from 'discord.js';
5
5
  import { handleSessionCommand, handleSessionAutocomplete } from './commands/session.js';
6
+ import { handleNewWorktreeCommand } from './commands/worktree.js';
6
7
  import { handleResumeCommand, handleResumeAutocomplete } from './commands/resume.js';
7
8
  import { handleAddProjectCommand, handleAddProjectAutocomplete } from './commands/add-project.js';
8
9
  import { handleRemoveProjectCommand, handleRemoveProjectAutocomplete, } from './commands/remove-project.js';
@@ -30,7 +31,7 @@ export function registerInteractionHandler({ discordClient, appId, }) {
30
31
  : 'other'}`);
31
32
  if (interaction.isAutocomplete()) {
32
33
  switch (interaction.commandName) {
33
- case 'session':
34
+ case 'new-session':
34
35
  await handleSessionAutocomplete({ interaction, appId });
35
36
  return;
36
37
  case 'resume':
@@ -50,9 +51,12 @@ export function registerInteractionHandler({ discordClient, appId, }) {
50
51
  if (interaction.isChatInputCommand()) {
51
52
  interactionLogger.log(`[COMMAND] Processing: ${interaction.commandName}`);
52
53
  switch (interaction.commandName) {
53
- case 'session':
54
+ case 'new-session':
54
55
  await handleSessionCommand({ command: interaction, appId });
55
56
  return;
57
+ case 'new-worktree':
58
+ await handleNewWorktreeCommand({ command: interaction, appId });
59
+ return;
56
60
  case 'resume':
57
61
  await handleResumeCommand({ command: interaction, appId });
58
62
  return;
package/dist/markdown.js CHANGED
@@ -3,13 +3,17 @@
3
3
  // user messages, assistant responses, tool calls, and reasoning blocks.
4
4
  // Uses errore for type-safe error handling.
5
5
  import * as errore from 'errore';
6
+ import { createTaggedError } from 'errore';
6
7
  import * as yaml from 'js-yaml';
7
8
  import { formatDateTime } from './utils.js';
8
9
  import { extractNonXmlContent } from './xml.js';
9
10
  import { createLogger } from './logger.js';
10
11
  import { SessionNotFoundError, MessagesNotFoundError } from './errors.js';
11
12
  // Generic error for unexpected exceptions in async operations
12
- class UnexpectedError extends errore.TaggedError('UnexpectedError')() {
13
+ class UnexpectedError extends createTaggedError({
14
+ name: 'UnexpectedError',
15
+ message: '$message',
16
+ }) {
13
17
  }
14
18
  const markdownLogger = createLogger('MARKDOWN');
15
19
  export class ShareMarkdown {
@@ -62,7 +62,7 @@ export async function getTextAttachments(message) {
62
62
  try: () => fetch(attachment.url),
63
63
  catch: (e) => new FetchError({ url: attachment.url, cause: e }),
64
64
  });
65
- if (errore.isError(response)) {
65
+ if (response instanceof Error) {
66
66
  return `<attachment filename="${attachment.name}" error="${response.message}" />`;
67
67
  }
68
68
  if (!response.ok) {
@@ -90,7 +90,7 @@ export async function getFileAttachments(message) {
90
90
  try: () => fetch(attachment.url),
91
91
  catch: (e) => new FetchError({ url: attachment.url, cause: e }),
92
92
  });
93
- if (errore.isError(response)) {
93
+ if (response instanceof Error) {
94
94
  logger.error(`Error downloading attachment ${attachment.name}:`, response.message);
95
95
  return null;
96
96
  }
package/dist/opencode.js CHANGED
@@ -43,7 +43,7 @@ async function waitForServer(port, maxAttempts = 30) {
43
43
  try: () => fetch(endpoint),
44
44
  catch: (e) => new FetchError({ url: endpoint, cause: e }),
45
45
  });
46
- if (errore.isError(response)) {
46
+ if (response instanceof Error) {
47
47
  // Connection refused or other transient errors - continue polling
48
48
  opencodeLogger.debug(`Server polling attempt failed: ${response.message}`);
49
49
  continue;
@@ -80,7 +80,7 @@ export async function initializeOpencodeForDirectory(directory) {
80
80
  },
81
81
  catch: () => new DirectoryNotAccessibleError({ directory }),
82
82
  });
83
- if (errore.isError(accessCheck)) {
83
+ if (accessCheck instanceof Error) {
84
84
  return accessCheck;
85
85
  }
86
86
  const port = await getOpenPort();
@@ -125,7 +125,7 @@ export async function initializeOpencodeForDirectory(directory) {
125
125
  serverRetryCount.set(directory, retryCount + 1);
126
126
  opencodeLogger.log(`Restarting server for directory: ${directory} (attempt ${retryCount + 1}/5)`);
127
127
  initializeOpencodeForDirectory(directory).then((result) => {
128
- if (errore.isError(result)) {
128
+ if (result instanceof Error) {
129
129
  opencodeLogger.error(`Failed to restart opencode server:`, result);
130
130
  }
131
131
  });
@@ -139,7 +139,7 @@ export async function initializeOpencodeForDirectory(directory) {
139
139
  }
140
140
  });
141
141
  const waitResult = await waitForServer(port);
142
- if (errore.isError(waitResult)) {
142
+ if (waitResult instanceof Error) {
143
143
  // Dump buffered logs on failure
144
144
  opencodeLogger.error(`Server failed to start for ${directory}:`);
145
145
  for (const line of logBuffer) {
@@ -49,7 +49,7 @@ export async function abortAndRetrySession({ sessionId, thread, projectDirectory
49
49
  controller.abort('model-change');
50
50
  // Also call the API abort endpoint
51
51
  const getClient = await initializeOpencodeForDirectory(projectDirectory);
52
- if (errore.isError(getClient)) {
52
+ if (getClient instanceof Error) {
53
53
  sessionLogger.error(`[ABORT+RETRY] Failed to initialize OpenCode client:`, getClient.message);
54
54
  return false;
55
55
  }
@@ -98,7 +98,7 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
98
98
  const directory = projectDirectory || process.cwd();
99
99
  sessionLogger.log(`Using directory: ${directory}`);
100
100
  const getClient = await initializeOpencodeForDirectory(directory);
101
- if (errore.isError(getClient)) {
101
+ if (getClient instanceof Error) {
102
102
  await sendThreadMessage(thread, `✗ ${getClient.message}`);
103
103
  return;
104
104
  }
package/dist/tools.js CHANGED
@@ -15,7 +15,7 @@ import pc from 'picocolors';
15
15
  import { initializeOpencodeForDirectory, getOpencodeSystemMessage } from './discord-bot.js';
16
16
  export async function getTools({ onMessageCompleted, directory, }) {
17
17
  const getClient = await initializeOpencodeForDirectory(directory);
18
- if (errore.isError(getClient)) {
18
+ if (getClient instanceof Error) {
19
19
  throw new Error(getClient.message);
20
20
  }
21
21
  const client = getClient();
@@ -248,7 +248,7 @@ export async function getTools({ onMessageCompleted, directory, }) {
248
248
  sessionID: sessionId,
249
249
  lastAssistantOnly: true,
250
250
  });
251
- if (errore.isError(markdownResult)) {
251
+ if (markdownResult instanceof Error) {
252
252
  throw new Error(markdownResult.message);
253
253
  }
254
254
  return {
@@ -261,7 +261,7 @@ export async function getTools({ onMessageCompleted, directory, }) {
261
261
  const markdownResult = await markdownRenderer.generate({
262
262
  sessionID: sessionId,
263
263
  });
264
- if (errore.isError(markdownResult)) {
264
+ if (markdownResult instanceof Error) {
265
265
  throw new Error(markdownResult.message);
266
266
  }
267
267
  const messages = await getClient().session.messages({
@@ -326,7 +326,7 @@ export async function processVoiceAttachment({ message, thread, projectDirectory
326
326
  try: () => fetch(audioAttachment.url),
327
327
  catch: (e) => new FetchError({ url: audioAttachment.url, cause: e }),
328
328
  });
329
- if (errore.isError(audioResponse)) {
329
+ if (audioResponse instanceof Error) {
330
330
  voiceLogger.error(`Failed to download audio attachment:`, audioResponse.message);
331
331
  await sendThreadMessage(thread, `⚠️ Failed to download audio: ${audioResponse.message}`);
332
332
  return null;
@@ -367,7 +367,7 @@ export async function processVoiceAttachment({ message, thread, projectDirectory
367
367
  currentSessionContext,
368
368
  lastSessionContext,
369
369
  });
370
- if (errore.isError(transcription)) {
370
+ if (transcription instanceof Error) {
371
371
  const errMsg = errore.matchError(transcription, {
372
372
  ApiKeyMissingError: (e) => e.message,
373
373
  InvalidAudioFormatError: (e) => e.message,
@@ -398,7 +398,7 @@ export async function processVoiceAttachment({ message, thread, projectDirectory
398
398
  if (renamed === null) {
399
399
  voiceLogger.log(`Thread name update timed out`);
400
400
  }
401
- else if (errore.isError(renamed)) {
401
+ else if (renamed instanceof Error) {
402
402
  voiceLogger.log(`Could not update thread name:`, renamed.message);
403
403
  }
404
404
  else {
package/dist/voice.js CHANGED
@@ -103,7 +103,7 @@ function createToolRunner({ directory }) {
103
103
  voiceLogger.log(`Grep search: "${pattern}"`);
104
104
  const result = await runGrep({ pattern, directory });
105
105
  const output = (() => {
106
- if (errore.isError(result)) {
106
+ if (result instanceof Error) {
107
107
  voiceLogger.error('grep search failed:', result);
108
108
  return 'grep search failed';
109
109
  }
@@ -117,7 +117,7 @@ function createToolRunner({ directory }) {
117
117
  voiceLogger.log(`Glob search: "${pattern}"`);
118
118
  const result = await runGlob({ pattern, directory });
119
119
  const output = (() => {
120
- if (errore.isError(result)) {
120
+ if (result instanceof Error) {
121
121
  voiceLogger.error('glob search failed:', result);
122
122
  return 'glob search failed';
123
123
  }
@@ -145,7 +145,7 @@ export async function runTranscriptionLoop({ genAI, model, initialContents, tool
145
145
  }),
146
146
  catch: (e) => new TranscriptionError({ reason: `API call failed: ${String(e)}`, cause: e }),
147
147
  });
148
- if (errore.isError(initialResponse)) {
148
+ if (initialResponse instanceof Error) {
149
149
  return initialResponse;
150
150
  }
151
151
  let response = initialResponse;
@@ -233,7 +233,7 @@ export async function runTranscriptionLoop({ genAI, model, initialContents, tool
233
233
  }),
234
234
  catch: (e) => new TranscriptionError({ reason: `API call failed: ${String(e)}`, cause: e }),
235
235
  });
236
- if (errore.isError(nextResponse)) {
236
+ if (nextResponse instanceof Error) {
237
237
  return nextResponse;
238
238
  }
239
239
  response = nextResponse;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "kimaki",
3
3
  "module": "index.ts",
4
4
  "type": "module",
5
- "version": "0.4.39",
5
+ "version": "0.4.40",
6
6
  "scripts": {
7
7
  "dev": "tsx --env-file .env src/cli.ts",
8
8
  "prepublishOnly": "pnpm tsc",
@@ -34,18 +34,19 @@
34
34
  "@clack/prompts": "^0.11.0",
35
35
  "@discordjs/voice": "^0.19.0",
36
36
  "@google/genai": "^1.34.0",
37
- "@opencode-ai/sdk": "^1.1.12",
37
+ "@opencode-ai/sdk": "^1.1.31",
38
38
  "@purinton/resampler": "^1.0.4",
39
39
  "ai": "^5.0.114",
40
40
  "better-sqlite3": "^12.3.0",
41
41
  "cac": "^6.7.14",
42
42
  "discord.js": "^14.16.3",
43
43
  "domhandler": "^5.0.3",
44
- "errore": "^0.5.2",
44
+ "errore": "workspace:^",
45
45
  "glob": "^13.0.0",
46
46
  "htmlparser2": "^10.0.0",
47
47
  "js-yaml": "^4.1.0",
48
48
  "marked": "^16.3.0",
49
+ "mime": "^4.1.0",
49
50
  "picocolors": "^1.1.1",
50
51
  "pretty-ms": "^9.3.0",
51
52
  "ripgrep-js": "^3.0.0",
package/src/cli.ts CHANGED
@@ -43,6 +43,7 @@ import fs from 'node:fs'
43
43
  import * as errore from 'errore'
44
44
 
45
45
  import { createLogger } from './logger.js'
46
+ import { uploadFilesToDiscord } from './discord-utils.js'
46
47
  import { spawn, spawnSync, execSync, type ExecSyncOptions } from 'node:child_process'
47
48
  import http from 'node:http'
48
49
  import { setDataDir, getDataDir, getLockPort } from './config.js'
@@ -211,7 +212,7 @@ async function registerCommands({
211
212
  })
212
213
  .toJSON(),
213
214
  new SlashCommandBuilder()
214
- .setName('session')
215
+ .setName('new-session')
215
216
  .setDescription('Start a new OpenCode session')
216
217
  .addStringOption((option) => {
217
218
  option.setName('prompt').setDescription('Prompt content for the session').setRequired(true)
@@ -236,6 +237,18 @@ async function registerCommands({
236
237
  return option
237
238
  })
238
239
  .toJSON(),
240
+ new SlashCommandBuilder()
241
+ .setName('new-worktree')
242
+ .setDescription('Create a new git worktree and start a session thread')
243
+ .addStringOption((option) => {
244
+ option
245
+ .setName('name')
246
+ .setDescription('Name for the worktree (will be formatted: lowercase, spaces to dashes)')
247
+ .setRequired(true)
248
+
249
+ return option
250
+ })
251
+ .toJSON(),
239
252
  new SlashCommandBuilder()
240
253
  .setName('add-project')
241
254
  .setDescription('Create Discord channels for a new OpenCode project')
@@ -582,7 +595,7 @@ async function run({ restart, addChannels }: CliOptions) {
582
595
  const currentDir = process.cwd()
583
596
  s.start('Starting OpenCode server...')
584
597
  const opencodePromise = initializeOpencodeForDirectory(currentDir).then((result) => {
585
- if (errore.isError(result)) {
598
+ if (result instanceof Error) {
586
599
  throw new Error(result.message)
587
600
  }
588
601
  return result
@@ -998,34 +1011,11 @@ cli
998
1011
  const s = spinner()
999
1012
  s.start(`Uploading ${resolvedFiles.length} file(s)...`)
1000
1013
 
1001
- for (const file of resolvedFiles) {
1002
- const buffer = fs.readFileSync(file)
1003
-
1004
- const formData = new FormData()
1005
- formData.append(
1006
- 'payload_json',
1007
- JSON.stringify({
1008
- attachments: [{ id: 0, filename: path.basename(file) }],
1009
- }),
1010
- )
1011
- formData.append('files[0]', new Blob([buffer]), path.basename(file))
1012
-
1013
- const response = await fetch(
1014
- `https://discord.com/api/v10/channels/${threadRow.thread_id}/messages`,
1015
- {
1016
- method: 'POST',
1017
- headers: {
1018
- Authorization: `Bot ${botRow.token}`,
1019
- },
1020
- body: formData,
1021
- },
1022
- )
1023
-
1024
- if (!response.ok) {
1025
- const error = await response.text()
1026
- throw new Error(`Discord API error: ${response.status} - ${error}`)
1027
- }
1028
- }
1014
+ await uploadFilesToDiscord({
1015
+ threadId: threadRow.thread_id,
1016
+ botToken: botRow.token,
1017
+ files: resolvedFiles,
1018
+ })
1029
1019
 
1030
1020
  s.stop(`Uploaded ${resolvedFiles.length} file(s)!`)
1031
1021
 
@@ -72,7 +72,7 @@ export async function handleAbortCommand({ command }: CommandContext): Promise<v
72
72
  }
73
73
 
74
74
  const getClient = await initializeOpencodeForDirectory(directory)
75
- if (errore.isError(getClient)) {
75
+ if (getClient instanceof Error) {
76
76
  await command.reply({
77
77
  content: `Failed to abort: ${getClient.message}`,
78
78
  ephemeral: true,
@@ -26,7 +26,7 @@ export async function handleAddProjectCommand({ command, appId }: CommandContext
26
26
  try {
27
27
  const currentDir = process.cwd()
28
28
  const getClient = await initializeOpencodeForDirectory(currentDir)
29
- if (errore.isError(getClient)) {
29
+ if (getClient instanceof Error) {
30
30
  await command.editReply(getClient.message)
31
31
  return
32
32
  }
@@ -94,7 +94,7 @@ export async function handleAddProjectAutocomplete({
94
94
  try {
95
95
  const currentDir = process.cwd()
96
96
  const getClient = await initializeOpencodeForDirectory(currentDir)
97
- if (errore.isError(getClient)) {
97
+ if (getClient instanceof Error) {
98
98
  await interaction.respond([])
99
99
  return
100
100
  }
@@ -162,7 +162,7 @@ export async function handleAgentCommand({
162
162
 
163
163
  try {
164
164
  const getClient = await initializeOpencodeForDirectory(context.dir)
165
- if (errore.isError(getClient)) {
165
+ if (getClient instanceof Error) {
166
166
  await interaction.editReply({ content: getClient.message })
167
167
  return
168
168
  }
@@ -297,7 +297,7 @@ export async function handleQuickAgentCommand({
297
297
 
298
298
  try {
299
299
  const getClient = await initializeOpencodeForDirectory(context.dir)
300
- if (errore.isError(getClient)) {
300
+ if (getClient instanceof Error) {
301
301
  await command.editReply({ content: getClient.message })
302
302
  return
303
303
  }
@@ -73,7 +73,7 @@ export async function handleForkCommand(interaction: ChatInputCommandInteraction
73
73
  const sessionId = row.session_id
74
74
 
75
75
  const getClient = await initializeOpencodeForDirectory(directory)
76
- if (errore.isError(getClient)) {
76
+ if (getClient instanceof Error) {
77
77
  await interaction.editReply({
78
78
  content: `Failed to load messages: ${getClient.message}`,
79
79
  })
@@ -171,7 +171,7 @@ export async function handleForkSelectMenu(
171
171
  await interaction.deferReply({ ephemeral: false })
172
172
 
173
173
  const getClient = await initializeOpencodeForDirectory(directory)
174
- if (errore.isError(getClient)) {
174
+ if (getClient instanceof Error) {
175
175
  await interaction.editReply(`Failed to fork session: ${getClient.message}`)
176
176
  return
177
177
  }
@@ -129,7 +129,7 @@ export async function handleModelCommand({
129
129
 
130
130
  try {
131
131
  const getClient = await initializeOpencodeForDirectory(projectDirectory)
132
- if (errore.isError(getClient)) {
132
+ if (getClient instanceof Error) {
133
133
  await interaction.editReply({ content: getClient.message })
134
134
  return
135
135
  }
@@ -237,7 +237,7 @@ export async function handleProviderSelectMenu(
237
237
 
238
238
  try {
239
239
  const getClient = await initializeOpencodeForDirectory(context.dir)
240
- if (errore.isError(getClient)) {
240
+ if (getClient instanceof Error) {
241
241
  await interaction.editReply({
242
242
  content: getClient.message,
243
243
  components: [],
@@ -42,7 +42,7 @@ export async function handleRemoveProjectCommand({ command, appId }: CommandCont
42
42
  catch: (e) => e as Error,
43
43
  })
44
44
 
45
- if (errore.isError(channel)) {
45
+ if (channel instanceof Error) {
46
46
  logger.error(`Failed to fetch channel ${channel_id}:`, channel)
47
47
  failedChannels.push(`${channel_type}: ${channel_id}`)
48
48
  continue
@@ -116,7 +116,7 @@ export async function handleRemoveProjectAutocomplete({
116
116
  try: () => guild.channels.fetch(channel_id),
117
117
  catch: (e) => e as Error,
118
118
  })
119
- if (errore.isError(channel)) {
119
+ if (channel instanceof Error) {
120
120
  // Channel not in this guild, skip
121
121
  continue
122
122
  }