kimaki 0.4.39 → 0.4.41

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 (52) hide show
  1. package/LICENSE +21 -0
  2. package/dist/cli.js +108 -51
  3. package/dist/commands/abort.js +1 -1
  4. package/dist/commands/add-project.js +2 -2
  5. package/dist/commands/agent.js +2 -2
  6. package/dist/commands/fork.js +2 -2
  7. package/dist/commands/model.js +2 -2
  8. package/dist/commands/remove-project.js +2 -2
  9. package/dist/commands/resume.js +2 -2
  10. package/dist/commands/session.js +4 -4
  11. package/dist/commands/share.js +1 -1
  12. package/dist/commands/undo-redo.js +2 -2
  13. package/dist/commands/worktree.js +180 -0
  14. package/dist/database.js +49 -1
  15. package/dist/discord-bot.js +29 -4
  16. package/dist/discord-utils.js +36 -0
  17. package/dist/errors.js +86 -87
  18. package/dist/genai-worker.js +1 -1
  19. package/dist/interaction-handler.js +6 -2
  20. package/dist/markdown.js +5 -1
  21. package/dist/message-formatting.js +2 -2
  22. package/dist/opencode.js +4 -4
  23. package/dist/session-handler.js +2 -2
  24. package/dist/tools.js +3 -3
  25. package/dist/voice-handler.js +3 -3
  26. package/dist/voice.js +4 -4
  27. package/package.json +16 -16
  28. package/src/cli.ts +166 -85
  29. package/src/commands/abort.ts +1 -1
  30. package/src/commands/add-project.ts +2 -2
  31. package/src/commands/agent.ts +2 -2
  32. package/src/commands/fork.ts +2 -2
  33. package/src/commands/model.ts +2 -2
  34. package/src/commands/remove-project.ts +2 -2
  35. package/src/commands/resume.ts +2 -2
  36. package/src/commands/session.ts +4 -4
  37. package/src/commands/share.ts +1 -1
  38. package/src/commands/undo-redo.ts +2 -2
  39. package/src/commands/worktree.ts +243 -0
  40. package/src/database.ts +96 -1
  41. package/src/discord-bot.ts +30 -4
  42. package/src/discord-utils.ts +50 -0
  43. package/src/errors.ts +90 -160
  44. package/src/genai-worker.ts +1 -1
  45. package/src/interaction-handler.ts +7 -2
  46. package/src/markdown.ts +5 -4
  47. package/src/message-formatting.ts +2 -2
  48. package/src/opencode.ts +4 -4
  49. package/src/session-handler.ts +2 -2
  50. package/src/tools.ts +3 -3
  51. package/src/voice-handler.ts +3 -3
  52. package/src/voice.ts +4 -4
package/src/errors.ts CHANGED
@@ -2,190 +2,117 @@
2
2
  // Errors are grouped by category: infrastructure, domain, and validation.
3
3
  // Use errore.matchError() for exhaustive error handling in command handlers.
4
4
 
5
- import * as errore from 'errore'
5
+ import { createTaggedError } from 'errore'
6
6
 
7
7
  // ═══════════════════════════════════════════════════════════════════════════
8
8
  // INFRASTRUCTURE ERRORS - Server, filesystem, external services
9
9
  // ═══════════════════════════════════════════════════════════════════════════
10
10
 
11
- export class DirectoryNotAccessibleError extends errore.TaggedError('DirectoryNotAccessibleError')<{
12
- directory: string
13
- message: string
14
- }>() {
15
- constructor(args: { directory: string }) {
16
- super({ ...args, message: `Directory does not exist or is not accessible: ${args.directory}` })
17
- }
18
- }
19
-
20
- export class ServerStartError extends errore.TaggedError('ServerStartError')<{
21
- port: number
22
- reason: string
23
- message: string
24
- }>() {
25
- constructor(args: { port: number; reason: string }) {
26
- super({ ...args, message: `Server failed to start on port ${args.port}: ${args.reason}` })
27
- }
28
- }
29
-
30
- export class ServerNotFoundError extends errore.TaggedError('ServerNotFoundError')<{
31
- directory: string
32
- message: string
33
- }>() {
34
- constructor(args: { directory: string }) {
35
- super({ ...args, message: `OpenCode server not found for directory: ${args.directory}` })
36
- }
37
- }
38
-
39
- export class ServerNotReadyError extends errore.TaggedError('ServerNotReadyError')<{
40
- directory: string
41
- message: string
42
- }>() {
43
- constructor(args: { directory: string }) {
44
- super({
45
- ...args,
46
- message: `OpenCode server for directory "${args.directory}" is in an error state (no client available)`,
47
- })
48
- }
49
- }
50
-
51
- export class ApiKeyMissingError extends errore.TaggedError('ApiKeyMissingError')<{
52
- service: string
53
- message: string
54
- }>() {
55
- constructor(args: { service: string }) {
56
- super({ ...args, message: `${args.service} API key is required` })
57
- }
58
- }
11
+ export class DirectoryNotAccessibleError extends createTaggedError({
12
+ name: 'DirectoryNotAccessibleError',
13
+ message: 'Directory does not exist or is not accessible: $directory',
14
+ }) {}
15
+
16
+ export class ServerStartError extends createTaggedError({
17
+ name: 'ServerStartError',
18
+ message: 'Server failed to start on port $port: $reason',
19
+ }) {}
20
+
21
+ export class ServerNotFoundError extends createTaggedError({
22
+ name: 'ServerNotFoundError',
23
+ message: 'OpenCode server not found for directory: $directory',
24
+ }) {}
25
+
26
+ export class ServerNotReadyError extends createTaggedError({
27
+ name: 'ServerNotReadyError',
28
+ message: 'OpenCode server for directory "$directory" is in an error state (no client available)',
29
+ }) {}
30
+
31
+ export class ApiKeyMissingError extends createTaggedError({
32
+ name: 'ApiKeyMissingError',
33
+ message: '$service API key is required',
34
+ }) {}
59
35
 
60
36
  // ═══════════════════════════════════════════════════════════════════════════
61
37
  // DOMAIN ERRORS - Sessions, messages, transcription
62
38
  // ═══════════════════════════════════════════════════════════════════════════
63
39
 
64
- export class SessionNotFoundError extends errore.TaggedError('SessionNotFoundError')<{
65
- sessionId: string
66
- message: string
67
- }>() {
68
- constructor(args: { sessionId: string }) {
69
- super({ ...args, message: `Session ${args.sessionId} not found` })
70
- }
71
- }
72
-
73
- export class SessionCreateError extends errore.TaggedError('SessionCreateError')<{
74
- message: string
75
- cause?: unknown
76
- }>() {}
77
-
78
- export class MessagesNotFoundError extends errore.TaggedError('MessagesNotFoundError')<{
79
- sessionId: string
80
- message: string
81
- }>() {
82
- constructor(args: { sessionId: string }) {
83
- super({ ...args, message: `No messages found for session ${args.sessionId}` })
84
- }
85
- }
86
-
87
- export class TranscriptionError extends errore.TaggedError('TranscriptionError')<{
88
- reason: string
89
- message: string
90
- cause?: unknown
91
- }>() {
92
- constructor(args: { reason: string; cause?: unknown }) {
93
- super({ ...args, message: `Transcription failed: ${args.reason}` })
94
- }
95
- }
96
-
97
- export class GrepSearchError extends errore.TaggedError('GrepSearchError')<{
98
- pattern: string
99
- message: string
100
- cause?: unknown
101
- }>() {
102
- constructor(args: { pattern: string; cause?: unknown }) {
103
- super({ ...args, message: `Grep search failed for pattern: ${args.pattern}` })
104
- }
105
- }
106
-
107
- export class GlobSearchError extends errore.TaggedError('GlobSearchError')<{
108
- pattern: string
109
- message: string
110
- cause?: unknown
111
- }>() {
112
- constructor(args: { pattern: string; cause?: unknown }) {
113
- super({ ...args, message: `Glob search failed for pattern: ${args.pattern}` })
114
- }
115
- }
40
+ export class SessionNotFoundError extends createTaggedError({
41
+ name: 'SessionNotFoundError',
42
+ message: 'Session $sessionId not found',
43
+ }) {}
44
+
45
+ export class SessionCreateError extends createTaggedError({
46
+ name: 'SessionCreateError',
47
+ message: '$message',
48
+ }) {}
49
+
50
+ export class MessagesNotFoundError extends createTaggedError({
51
+ name: 'MessagesNotFoundError',
52
+ message: 'No messages found for session $sessionId',
53
+ }) {}
54
+
55
+ export class TranscriptionError extends createTaggedError({
56
+ name: 'TranscriptionError',
57
+ message: 'Transcription failed: $reason',
58
+ }) {}
59
+
60
+ export class GrepSearchError extends createTaggedError({
61
+ name: 'GrepSearchError',
62
+ message: 'Grep search failed for pattern: $pattern',
63
+ }) {}
64
+
65
+ export class GlobSearchError extends createTaggedError({
66
+ name: 'GlobSearchError',
67
+ message: 'Glob search failed for pattern: $pattern',
68
+ }) {}
116
69
 
117
70
  // ═══════════════════════════════════════════════════════════════════════════
118
71
  // VALIDATION ERRORS - Input validation, format checks
119
72
  // ═══════════════════════════════════════════════════════════════════════════
120
73
 
121
- export class InvalidAudioFormatError extends errore.TaggedError('InvalidAudioFormatError')<{
122
- message: string
123
- }>() {
124
- constructor() {
125
- super({ message: 'Invalid audio format' })
126
- }
127
- }
128
-
129
- export class EmptyTranscriptionError extends errore.TaggedError('EmptyTranscriptionError')<{
130
- message: string
131
- }>() {
132
- constructor() {
133
- super({ message: 'Model returned empty transcription' })
134
- }
135
- }
136
-
137
- export class NoResponseContentError extends errore.TaggedError('NoResponseContentError')<{
138
- message: string
139
- }>() {
140
- constructor() {
141
- super({ message: 'No response content from model' })
142
- }
143
- }
144
-
145
- export class NoToolResponseError extends errore.TaggedError('NoToolResponseError')<{
146
- message: string
147
- }>() {
148
- constructor() {
149
- super({ message: 'No valid tool responses' })
150
- }
151
- }
74
+ export class InvalidAudioFormatError extends createTaggedError({
75
+ name: 'InvalidAudioFormatError',
76
+ message: 'Invalid audio format',
77
+ }) {}
78
+
79
+ export class EmptyTranscriptionError extends createTaggedError({
80
+ name: 'EmptyTranscriptionError',
81
+ message: 'Model returned empty transcription',
82
+ }) {}
83
+
84
+ export class NoResponseContentError extends createTaggedError({
85
+ name: 'NoResponseContentError',
86
+ message: 'No response content from model',
87
+ }) {}
88
+
89
+ export class NoToolResponseError extends createTaggedError({
90
+ name: 'NoToolResponseError',
91
+ message: 'No valid tool responses',
92
+ }) {}
152
93
 
153
94
  // ═══════════════════════════════════════════════════════════════════════════
154
95
  // NETWORK ERRORS - Fetch and HTTP
155
96
  // ═══════════════════════════════════════════════════════════════════════════
156
97
 
157
- export class FetchError extends errore.TaggedError('FetchError')<{
158
- url: string
159
- message: string
160
- cause?: unknown
161
- }>() {
162
- constructor(args: { url: string; cause?: unknown }) {
163
- const causeMsg = args.cause instanceof Error ? args.cause.message : String(args.cause)
164
- super({ ...args, message: `Fetch failed for ${args.url}: ${causeMsg}` })
165
- }
166
- }
98
+ export class FetchError extends createTaggedError({
99
+ name: 'FetchError',
100
+ message: 'Fetch failed for $url',
101
+ }) {}
167
102
 
168
103
  // ═══════════════════════════════════════════════════════════════════════════
169
104
  // API ERRORS - External service responses
170
105
  // ═══════════════════════════════════════════════════════════════════════════
171
106
 
172
- export class DiscordApiError extends errore.TaggedError('DiscordApiError')<{
173
- status: number
174
- message: string
175
- }>() {
176
- constructor(args: { status: number; body?: string }) {
177
- super({ ...args, message: `Discord API error: ${args.status}${args.body ? ` - ${args.body}` : ''}` })
178
- }
179
- }
180
-
181
- export class OpenCodeApiError extends errore.TaggedError('OpenCodeApiError')<{
182
- status: number
183
- message: string
184
- }>() {
185
- constructor(args: { status: number; body?: string }) {
186
- super({ ...args, message: `OpenCode API error (${args.status})${args.body ? `: ${args.body}` : ''}` })
187
- }
188
- }
107
+ export class DiscordApiError extends createTaggedError({
108
+ name: 'DiscordApiError',
109
+ message: 'Discord API error: $status $body',
110
+ }) {}
111
+
112
+ export class OpenCodeApiError extends createTaggedError({
113
+ name: 'OpenCodeApiError',
114
+ message: 'OpenCode API error ($status): $body',
115
+ }) {}
189
116
 
190
117
  // ═══════════════════════════════════════════════════════════════════════════
191
118
  // UNION TYPES - For function signatures
@@ -205,4 +132,7 @@ export type OpenCodeErrors =
205
132
  | ServerNotFoundError
206
133
  | ServerNotReadyError
207
134
 
208
- export type SessionErrors = SessionNotFoundError | MessagesNotFoundError | OpenCodeApiError
135
+ export type SessionErrors =
136
+ | SessionNotFoundError
137
+ | MessagesNotFoundError
138
+ | OpenCodeApiError
@@ -132,7 +132,7 @@ async function createAssistantAudioLogStream(
132
132
  try: () => mkdir(audioDir, { recursive: true }),
133
133
  catch: (e) => e as Error,
134
134
  })
135
- if (errore.isError(mkdirError)) {
135
+ if (mkdirError instanceof Error) {
136
136
  workerLogger.error(`Failed to create audio log directory:`, mkdirError.message)
137
137
  return null
138
138
  }
@@ -4,6 +4,7 @@
4
4
 
5
5
  import { Events, type Client, type Interaction } from 'discord.js'
6
6
  import { handleSessionCommand, handleSessionAutocomplete } from './commands/session.js'
7
+ import { handleNewWorktreeCommand } from './commands/worktree.js'
7
8
  import { handleResumeCommand, handleResumeAutocomplete } from './commands/resume.js'
8
9
  import { handleAddProjectCommand, handleAddProjectAutocomplete } from './commands/add-project.js'
9
10
  import {
@@ -52,7 +53,7 @@ export function registerInteractionHandler({
52
53
 
53
54
  if (interaction.isAutocomplete()) {
54
55
  switch (interaction.commandName) {
55
- case 'session':
56
+ case 'new-session':
56
57
  await handleSessionAutocomplete({ interaction, appId })
57
58
  return
58
59
 
@@ -78,10 +79,14 @@ export function registerInteractionHandler({
78
79
  interactionLogger.log(`[COMMAND] Processing: ${interaction.commandName}`)
79
80
 
80
81
  switch (interaction.commandName) {
81
- case 'session':
82
+ case 'new-session':
82
83
  await handleSessionCommand({ command: interaction, appId })
83
84
  return
84
85
 
86
+ case 'new-worktree':
87
+ await handleNewWorktreeCommand({ command: interaction, appId })
88
+ return
89
+
85
90
  case 'resume':
86
91
  await handleResumeCommand({ command: interaction, appId })
87
92
  return
package/src/markdown.ts CHANGED
@@ -5,6 +5,7 @@
5
5
 
6
6
  import type { OpencodeClient } from '@opencode-ai/sdk'
7
7
  import * as errore from 'errore'
8
+ import { createTaggedError } from 'errore'
8
9
  import * as yaml from 'js-yaml'
9
10
  import { formatDateTime } from './utils.js'
10
11
  import { extractNonXmlContent } from './xml.js'
@@ -12,10 +13,10 @@ import { createLogger } from './logger.js'
12
13
  import { SessionNotFoundError, MessagesNotFoundError } from './errors.js'
13
14
 
14
15
  // Generic error for unexpected exceptions in async operations
15
- class UnexpectedError extends errore.TaggedError('UnexpectedError')<{
16
- message: string
17
- cause?: unknown
18
- }>() {}
16
+ class UnexpectedError extends createTaggedError({
17
+ name: 'UnexpectedError',
18
+ message: '$message',
19
+ }) {}
19
20
 
20
21
  const markdownLogger = createLogger('MARKDOWN')
21
22
 
@@ -93,7 +93,7 @@ export async function getTextAttachments(message: Message): Promise<string> {
93
93
  try: () => fetch(attachment.url),
94
94
  catch: (e) => new FetchError({ url: attachment.url, cause: e }),
95
95
  })
96
- if (errore.isError(response)) {
96
+ if (response instanceof Error) {
97
97
  return `<attachment filename="${attachment.name}" error="${response.message}" />`
98
98
  }
99
99
  if (!response.ok) {
@@ -128,7 +128,7 @@ export async function getFileAttachments(message: Message): Promise<FilePartInpu
128
128
  try: () => fetch(attachment.url),
129
129
  catch: (e) => new FetchError({ url: attachment.url, cause: e }),
130
130
  })
131
- if (errore.isError(response)) {
131
+ if (response instanceof Error) {
132
132
  logger.error(`Error downloading attachment ${attachment.name}:`, response.message)
133
133
  return null
134
134
  }
package/src/opencode.ts CHANGED
@@ -66,7 +66,7 @@ async function waitForServer(port: number, maxAttempts = 30): Promise<ServerStar
66
66
  try: () => fetch(endpoint),
67
67
  catch: (e) => new FetchError({ url: endpoint, cause: e }),
68
68
  })
69
- if (errore.isError(response)) {
69
+ if (response instanceof Error) {
70
70
  // Connection refused or other transient errors - continue polling
71
71
  opencodeLogger.debug(`Server polling attempt failed: ${response.message}`)
72
72
  continue
@@ -107,7 +107,7 @@ export async function initializeOpencodeForDirectory(directory: string): Promise
107
107
  },
108
108
  catch: () => new DirectoryNotAccessibleError({ directory }),
109
109
  })
110
- if (errore.isError(accessCheck)) {
110
+ if (accessCheck instanceof Error) {
111
111
  return accessCheck
112
112
  }
113
113
 
@@ -164,7 +164,7 @@ export async function initializeOpencodeForDirectory(directory: string): Promise
164
164
  `Restarting server for directory: ${directory} (attempt ${retryCount + 1}/5)`,
165
165
  )
166
166
  initializeOpencodeForDirectory(directory).then((result) => {
167
- if (errore.isError(result)) {
167
+ if (result instanceof Error) {
168
168
  opencodeLogger.error(`Failed to restart opencode server:`, result)
169
169
  }
170
170
  })
@@ -177,7 +177,7 @@ export async function initializeOpencodeForDirectory(directory: string): Promise
177
177
  })
178
178
 
179
179
  const waitResult = await waitForServer(port)
180
- if (errore.isError(waitResult)) {
180
+ if (waitResult instanceof Error) {
181
181
  // Dump buffered logs on failure
182
182
  opencodeLogger.error(`Server failed to start for ${directory}:`)
183
183
  for (const line of logBuffer) {
@@ -105,7 +105,7 @@ export async function abortAndRetrySession({
105
105
 
106
106
  // Also call the API abort endpoint
107
107
  const getClient = await initializeOpencodeForDirectory(projectDirectory)
108
- if (errore.isError(getClient)) {
108
+ if (getClient instanceof Error) {
109
109
  sessionLogger.error(`[ABORT+RETRY] Failed to initialize OpenCode client:`, getClient.message)
110
110
  return false
111
111
  }
@@ -188,7 +188,7 @@ export async function handleOpencodeSession({
188
188
  sessionLogger.log(`Using directory: ${directory}`)
189
189
 
190
190
  const getClient = await initializeOpencodeForDirectory(directory)
191
- if (errore.isError(getClient)) {
191
+ if (getClient instanceof Error) {
192
192
  await sendThreadMessage(thread, `✗ ${getClient.message}`)
193
193
  return
194
194
  }
package/src/tools.ts CHANGED
@@ -36,7 +36,7 @@ export async function getTools({
36
36
  }) => void
37
37
  }) {
38
38
  const getClient = await initializeOpencodeForDirectory(directory)
39
- if (errore.isError(getClient)) {
39
+ if (getClient instanceof Error) {
40
40
  throw new Error(getClient.message)
41
41
  }
42
42
  const client = getClient()
@@ -298,7 +298,7 @@ export async function getTools({
298
298
  sessionID: sessionId,
299
299
  lastAssistantOnly: true,
300
300
  })
301
- if (errore.isError(markdownResult)) {
301
+ if (markdownResult instanceof Error) {
302
302
  throw new Error(markdownResult.message)
303
303
  }
304
304
 
@@ -311,7 +311,7 @@ export async function getTools({
311
311
  const markdownResult = await markdownRenderer.generate({
312
312
  sessionID: sessionId,
313
313
  })
314
- if (errore.isError(markdownResult)) {
314
+ if (markdownResult instanceof Error) {
315
315
  throw new Error(markdownResult.message)
316
316
  }
317
317
 
@@ -450,7 +450,7 @@ export async function processVoiceAttachment({
450
450
  try: () => fetch(audioAttachment.url),
451
451
  catch: (e) => new FetchError({ url: audioAttachment.url, cause: e }),
452
452
  })
453
- if (errore.isError(audioResponse)) {
453
+ if (audioResponse instanceof Error) {
454
454
  voiceLogger.error(`Failed to download audio attachment:`, audioResponse.message)
455
455
  await sendThreadMessage(thread, `⚠️ Failed to download audio: ${audioResponse.message}`)
456
456
  return null
@@ -498,7 +498,7 @@ export async function processVoiceAttachment({
498
498
  lastSessionContext,
499
499
  })
500
500
 
501
- if (errore.isError(transcription)) {
501
+ if (transcription instanceof Error) {
502
502
  const errMsg = errore.matchError(transcription, {
503
503
  ApiKeyMissingError: (e) => e.message,
504
504
  InvalidAudioFormatError: (e) => e.message,
@@ -532,7 +532,7 @@ export async function processVoiceAttachment({
532
532
  ])
533
533
  if (renamed === null) {
534
534
  voiceLogger.log(`Thread name update timed out`)
535
- } else if (errore.isError(renamed)) {
535
+ } else if (renamed instanceof Error) {
536
536
  voiceLogger.log(`Could not update thread name:`, renamed.message)
537
537
  } else {
538
538
  voiceLogger.log(`Updated thread name to: "${threadName}"`)
package/src/voice.ts CHANGED
@@ -145,7 +145,7 @@ function createToolRunner({ directory }: { directory?: string }): TranscriptionT
145
145
  voiceLogger.log(`Grep search: "${pattern}"`)
146
146
  const result = await runGrep({ pattern, directory })
147
147
  const output = (() => {
148
- if (errore.isError(result)) {
148
+ if (result instanceof Error) {
149
149
  voiceLogger.error('grep search failed:', result)
150
150
  return 'grep search failed'
151
151
  }
@@ -160,7 +160,7 @@ function createToolRunner({ directory }: { directory?: string }): TranscriptionT
160
160
  voiceLogger.log(`Glob search: "${pattern}"`)
161
161
  const result = await runGlob({ pattern, directory })
162
162
  const output = (() => {
163
- if (errore.isError(result)) {
163
+ if (result instanceof Error) {
164
164
  voiceLogger.error('glob search failed:', result)
165
165
  return 'glob search failed'
166
166
  }
@@ -214,7 +214,7 @@ export async function runTranscriptionLoop({
214
214
  catch: (e) => new TranscriptionError({ reason: `API call failed: ${String(e)}`, cause: e }),
215
215
  })
216
216
 
217
- if (errore.isError(initialResponse)) {
217
+ if (initialResponse instanceof Error) {
218
218
  return initialResponse
219
219
  }
220
220
 
@@ -323,7 +323,7 @@ export async function runTranscriptionLoop({
323
323
  catch: (e) => new TranscriptionError({ reason: `API call failed: ${String(e)}`, cause: e }),
324
324
  })
325
325
 
326
- if (errore.isError(nextResponse)) {
326
+ if (nextResponse instanceof Error) {
327
327
  return nextResponse
328
328
  }
329
329