kimaki 0.26.0 → 0.28.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.
- package/dist/agent-model.e2e.test.js +250 -164
- package/dist/anthropic-account-identity.js +9 -1
- package/dist/anthropic-auth-plugin.js +10 -13
- package/dist/anthropic-auth-plugin.test.js +17 -0
- package/dist/anthropic-auth-state.js +1 -0
- package/dist/bash-tool-schema-plugin.js +32 -0
- package/dist/bash-tool-schema-plugin.test.js +49 -0
- package/dist/bin.js +73 -19
- package/dist/cli-commands/maintenance.js +46 -7
- package/dist/cli-commands/project.js +11 -27
- package/dist/cli-commands/send.js +30 -1
- package/dist/cli-commands/session.js +322 -90
- package/dist/cli-commands/task.js +34 -15
- package/dist/cli-parsing.test.js +83 -4
- package/dist/cli-runner.js +44 -8
- package/dist/cli-runner.test.js +27 -0
- package/dist/cli-send-thread.e2e.test.js +9 -9
- package/dist/cli.js +32 -0
- package/dist/commands/abort.js +5 -1
- package/dist/commands/action-buttons.js +2 -1
- package/dist/commands/add-project.js +4 -7
- package/dist/commands/agent.js +164 -32
- package/dist/commands/ask-question.js +23 -33
- package/dist/commands/ask-question.test.js +43 -1
- package/dist/commands/btw.js +28 -6
- package/dist/commands/file-upload.js +2 -1
- package/dist/commands/fork-subagent.js +2 -4
- package/dist/commands/fork.js +4 -4
- package/dist/commands/last-sessions.js +17 -10
- package/dist/commands/login.js +6 -0
- package/dist/commands/merge-worktree.js +47 -14
- package/dist/commands/model-variant.js +8 -1
- package/dist/commands/model.js +16 -9
- package/dist/commands/multioauth.js +1 -0
- package/dist/commands/new-worktree.js +170 -102
- package/dist/commands/queue.js +3 -2
- package/dist/commands/resume.js +4 -4
- package/dist/commands/session.js +78 -63
- package/dist/commands/user-command.js +6 -8
- package/dist/commands/worktrees.js +5 -13
- package/dist/config.js +16 -1
- package/dist/cpu-profiler.js +211 -0
- package/dist/cpu-profiler.test.js +160 -0
- package/dist/database.js +152 -3
- package/dist/db.js +33 -0
- package/dist/db.test.js +267 -1
- package/dist/default-channel-provisioning.test.js +148 -0
- package/dist/discord-bot.js +517 -488
- package/dist/discord-command-registration.js +25 -10
- package/dist/discord-command-registration.test.js +17 -0
- package/dist/discord-utils.js +97 -5
- package/dist/discord-utils.test.js +151 -1
- package/dist/errors.js +10 -0
- package/dist/exec-async.js +17 -5
- package/dist/external-directory-permissions.e2e.test.js +11 -7
- package/dist/external-directory-project-deny.e2e.test.js +5 -3
- package/dist/external-opencode-sync.js +94 -17
- package/dist/external-opencode-sync.test.js +206 -0
- package/dist/extract-sdk-error-message.test.js +13 -0
- package/dist/file-edit-log.js +197 -0
- package/dist/file-edit-log.test.js +258 -0
- package/dist/format-tables.js +52 -0
- package/dist/format-tables.test.js +93 -0
- package/dist/gateway-proxy-reconnect.e2e.test.js +28 -8
- package/dist/gateway-proxy.e2e.test.js +18 -18
- package/dist/git-worktree-core.js +182 -17
- package/dist/interaction-handler.js +416 -398
- package/dist/ipc-tools-plugin.js +68 -3
- package/dist/kimaki-opencode-plugin-loading.e2e.test.js +122 -58
- package/dist/kimaki-opencode-plugin.js +9 -3
- package/dist/kimaki-workspace-adaptor.js +33 -10
- package/dist/logger.js +11 -1
- package/dist/markdown.js +3 -1
- package/dist/markdown.test.js +11 -3
- package/dist/message-formatting.js +181 -51
- package/dist/message-formatting.test.js +529 -22
- package/dist/message-preprocessing.js +105 -1
- package/dist/oauth-rotation-shared.js +10 -0
- package/dist/openai-auth-plugin.js +1 -0
- package/dist/openai-auth-state.js +1 -0
- package/dist/opencode-command.js +28 -0
- package/dist/opencode-command.test.js +109 -1
- package/dist/opencode-interrupt-plugin.js +4 -0
- package/dist/opencode-interrupt-plugin.test.js +24 -0
- package/dist/opencode.js +82 -15
- package/dist/project-add.test.js +62 -0
- package/dist/question-abort-clears-dropdown.e2e.test.js +89 -0
- package/dist/question-abort-retry-text-before-dropdown.e2e.test.js +150 -0
- package/dist/question-text-before-dropdown.e2e.test.js +52 -0
- package/dist/queue-advanced-abort.e2e.test.js +8 -10
- package/dist/queue-advanced-action-buttons.e2e.test.js +8 -8
- package/dist/queue-advanced-e2e-setup.js +78 -5
- package/dist/queue-advanced-footer.e2e.test.js +60 -269
- package/dist/queue-advanced-model-switch.e2e.test.js +9 -11
- package/dist/queue-advanced-permissions-typing.e2e.test.js +15 -15
- package/dist/queue-advanced-question.e2e.test.js +32 -27
- package/dist/queue-advanced-typing-interrupt.e2e.test.js +10 -12
- package/dist/queue-advanced-typing.e2e.test.js +7 -9
- package/dist/queue-delete-message.e2e.test.js +3 -3
- package/dist/queue-drain-after-interactive-ui.e2e.test.js +5 -5
- package/dist/queue-interrupt-drain.e2e.test.js +122 -12
- package/dist/queue-question-select-drain.e2e.test.js +34 -132
- package/dist/runtime-lifecycle.e2e.test.js +21 -51
- package/dist/schema.js +35 -1
- package/dist/session-handler/event-stream-state.js +391 -50
- package/dist/session-handler/event-stream-state.test.js +736 -8
- package/dist/session-handler/model-utils.js +172 -5
- package/dist/session-handler/model-utils.test.js +445 -0
- package/dist/session-handler/opencode-session-event-log.js +30 -1
- package/dist/session-handler/thread-ingress.test.js +41 -0
- package/dist/session-handler/thread-runtime-state.js +2 -0
- package/dist/session-handler/thread-session-runtime-summary.test.js +121 -0
- package/dist/session-handler/thread-session-runtime.js +574 -299
- package/dist/session-search.js +123 -4
- package/dist/session-search.test.js +248 -1
- package/dist/session-sleep.test.js +224 -0
- package/dist/session-title-rename.test.js +37 -39
- package/dist/sleep.e2e.test.js +494 -0
- package/dist/startup-time.e2e.test.js +2 -1
- package/dist/store.js +3 -0
- package/dist/system-message.js +102 -16
- package/dist/system-message.test.js +192 -15
- package/dist/task-runner.js +96 -5
- package/dist/task-schedule.js +87 -1
- package/dist/task-schedule.test.js +149 -1
- package/dist/test-utils.js +17 -9
- package/dist/thinking-utils.js +10 -0
- package/dist/thinking-utils.test.js +35 -0
- package/dist/thread-message-queue.e2e.test.js +65 -213
- package/dist/undo-redo.e2e.test.js +17 -9
- package/dist/voice-attachment.js +23 -1
- package/dist/voice-handler.js +21 -4
- package/dist/voice-message.e2e.test.js +144 -30
- package/dist/voice-routing-input.e2e.test.js +195 -0
- package/dist/voice-routing-question.e2e.test.js +125 -0
- package/dist/voice.js +28 -8
- package/dist/voice.test.js +96 -30
- package/dist/worktree-clone-isolation.e2e.test.js +188 -0
- package/dist/worktree-lifecycle.e2e.test.js +55 -36
- package/dist/worktrees.js +226 -443
- package/dist/worktrees.test.js +435 -58
- package/dist/xai-auth-plugin.js +1 -0
- package/dist/xai-auth-state.js +1 -0
- package/package.json +8 -6
- package/skills/bump-holocron-consumers/SKILL.md +137 -0
- package/skills/critique/SKILL.md +4 -4
- package/skills/errore/SKILL.md +60 -218
- package/skills/goke/SKILL.md +150 -10
- package/skills/holocron/SKILL.md +154 -17
- package/skills/npm-package/SKILL.md +55 -35
- package/skills/profano/SKILL.md +17 -2
- package/skills/spiceflow/SKILL.md +39 -3
- package/skills/strada/SKILL.md +39 -0
- package/skills/zele/SKILL.md +19 -0
- package/src/agent-model.e2e.test.ts +287 -186
- package/src/anthropic-account-identity.ts +20 -1
- package/src/anthropic-auth-plugin.test.ts +19 -0
- package/src/anthropic-auth-plugin.ts +12 -16
- package/src/anthropic-auth-state.ts +1 -0
- package/src/bash-tool-schema-plugin.test.ts +55 -0
- package/src/bash-tool-schema-plugin.ts +46 -0
- package/src/bin.ts +78 -26
- package/src/cli-commands/maintenance.ts +59 -13
- package/src/cli-commands/project.ts +14 -35
- package/src/cli-commands/send.ts +31 -1
- package/src/cli-commands/session.ts +374 -114
- package/src/cli-commands/task.ts +34 -15
- package/src/cli-parsing.test.ts +103 -4
- package/src/cli-runner.test.ts +49 -0
- package/src/cli-runner.ts +44 -7
- package/src/cli-send-thread.e2e.test.ts +9 -11
- package/src/cli.ts +53 -0
- package/src/commands/abort.ts +6 -1
- package/src/commands/action-buttons.ts +2 -1
- package/src/commands/add-project.ts +4 -8
- package/src/commands/agent.ts +205 -29
- package/src/commands/ask-question.test.ts +48 -1
- package/src/commands/ask-question.ts +23 -33
- package/src/commands/btw.ts +40 -5
- package/src/commands/file-upload.ts +2 -1
- package/src/commands/fork-subagent.ts +2 -3
- package/src/commands/fork.ts +4 -3
- package/src/commands/last-sessions.ts +10 -4
- package/src/commands/login.ts +6 -0
- package/src/commands/merge-worktree.ts +54 -16
- package/src/commands/model-variant.ts +9 -1
- package/src/commands/model.ts +20 -9
- package/src/commands/multioauth.ts +1 -0
- package/src/commands/new-worktree.ts +138 -55
- package/src/commands/queue.ts +3 -2
- package/src/commands/resume.ts +4 -3
- package/src/commands/session.ts +94 -70
- package/src/commands/user-command.ts +6 -6
- package/src/commands/worktrees.ts +5 -12
- package/src/config.ts +18 -1
- package/src/cpu-profiler.test.ts +183 -0
- package/src/cpu-profiler.ts +293 -0
- package/src/database.ts +235 -3
- package/src/db.test.ts +293 -0
- package/src/db.ts +38 -0
- package/src/default-channel-provisioning.test.ts +155 -0
- package/src/discord-bot.ts +78 -36
- package/src/discord-command-registration.test.ts +20 -0
- package/src/discord-command-registration.ts +42 -12
- package/src/discord-utils.test.ts +176 -0
- package/src/discord-utils.ts +152 -7
- package/src/errors.ts +12 -0
- package/src/exec-async.ts +20 -11
- package/src/external-directory-permissions.e2e.test.ts +11 -7
- package/src/external-directory-project-deny.e2e.test.ts +5 -3
- package/src/external-opencode-sync.test.ts +248 -0
- package/src/external-opencode-sync.ts +122 -19
- package/src/extract-sdk-error-message.test.ts +18 -0
- package/src/file-edit-log.test.ts +287 -0
- package/src/file-edit-log.ts +283 -0
- package/src/format-tables.test.ts +101 -0
- package/src/format-tables.ts +75 -0
- package/src/gateway-proxy-reconnect.e2e.test.ts +38 -8
- package/src/gateway-proxy.e2e.test.ts +18 -17
- package/src/git-worktree-core.ts +247 -20
- package/src/interaction-handler.ts +36 -13
- package/src/ipc-tools-plugin.ts +82 -5
- package/src/kimaki-opencode-plugin-loading.e2e.test.ts +188 -97
- package/src/kimaki-opencode-plugin.ts +9 -3
- package/src/kimaki-workspace-adaptor.ts +40 -11
- package/src/logger.ts +14 -4
- package/src/markdown.test.ts +13 -3
- package/src/markdown.ts +4 -1
- package/src/message-formatting.test.ts +672 -25
- package/src/message-formatting.ts +242 -53
- package/src/message-preprocessing.ts +116 -1
- package/src/oauth-rotation-shared.ts +10 -0
- package/src/openai-auth-plugin.ts +1 -0
- package/src/openai-auth-state.ts +1 -0
- package/src/opencode-command.test.ts +140 -0
- package/src/opencode-command.ts +38 -0
- package/src/opencode-interrupt-plugin.test.ts +32 -0
- package/src/opencode-interrupt-plugin.ts +3 -0
- package/src/opencode.ts +115 -15
- package/src/project-add.test.ts +80 -0
- package/src/question-abort-clears-dropdown.e2e.test.ts +109 -0
- package/src/question-abort-retry-text-before-dropdown.e2e.test.ts +172 -0
- package/src/question-text-before-dropdown.e2e.test.ts +67 -0
- package/src/queue-advanced-abort.e2e.test.ts +8 -9
- package/src/queue-advanced-action-buttons.e2e.test.ts +8 -8
- package/src/queue-advanced-e2e-setup.ts +83 -4
- package/src/queue-advanced-footer.e2e.test.ts +67 -315
- package/src/queue-advanced-model-switch.e2e.test.ts +9 -10
- package/src/queue-advanced-permissions-typing.e2e.test.ts +15 -15
- package/src/queue-advanced-question.e2e.test.ts +36 -30
- package/src/queue-advanced-typing-interrupt.e2e.test.ts +11 -11
- package/src/queue-advanced-typing.e2e.test.ts +8 -8
- package/src/queue-delete-message.e2e.test.ts +3 -3
- package/src/queue-drain-after-interactive-ui.e2e.test.ts +5 -5
- package/src/queue-interrupt-drain.e2e.test.ts +133 -12
- package/src/queue-question-select-drain.e2e.test.ts +36 -161
- package/src/runtime-lifecycle.e2e.test.ts +21 -54
- package/src/schema.sql +15 -2
- package/src/schema.ts +37 -1
- package/src/session-handler/event-stream-state.test.ts +790 -7
- package/src/session-handler/event-stream-state.ts +568 -53
- package/src/session-handler/model-utils.test.ts +537 -0
- package/src/session-handler/model-utils.ts +281 -5
- package/src/session-handler/opencode-session-event-log.ts +31 -1
- package/src/session-handler/thread-ingress.test.ts +54 -0
- package/src/session-handler/thread-runtime-state.ts +3 -0
- package/src/session-handler/thread-session-runtime-summary.test.ts +169 -0
- package/src/session-handler/thread-session-runtime.ts +712 -358
- package/src/session-search.test.ts +281 -0
- package/src/session-search.ts +241 -7
- package/src/session-sleep.test.ts +294 -0
- package/src/session-title-rename.test.ts +54 -48
- package/src/sleep.e2e.test.ts +565 -0
- package/src/startup-time.e2e.test.ts +5 -1
- package/src/store.ts +22 -0
- package/src/system-message.test.ts +226 -15
- package/src/system-message.ts +130 -16
- package/src/task-runner.ts +138 -3
- package/src/task-schedule.test.ts +170 -0
- package/src/task-schedule.ts +141 -1
- package/src/test-utils.ts +21 -10
- package/src/thinking-utils.test.ts +46 -0
- package/src/thinking-utils.ts +21 -0
- package/src/thread-message-queue.e2e.test.ts +65 -245
- package/src/undo-redo.e2e.test.ts +17 -9
- package/src/voice-attachment.ts +29 -1
- package/src/voice-handler.ts +23 -4
- package/src/voice-message.e2e.test.ts +149 -29
- package/src/voice-routing-input.e2e.test.ts +203 -0
- package/src/voice-routing-question.e2e.test.ts +131 -0
- package/src/voice.test.ts +107 -29
- package/src/voice.ts +34 -6
- package/src/worktree-clone-isolation.e2e.test.ts +213 -0
- package/src/worktree-lifecycle.e2e.test.ts +55 -35
- package/src/worktrees.test.ts +505 -64
- package/src/worktrees.ts +257 -603
- package/src/xai-auth-plugin.ts +1 -0
- package/src/xai-auth-state.ts +1 -0
- package/dist/message-finish-field.e2e.test.js +0 -169
- package/dist/subagent-rate-limit-plugin.js +0 -184
- package/src/message-finish-field.e2e.test.ts +0 -196
- package/src/subagent-rate-limit-plugin.ts +0 -228
|
@@ -3,9 +3,13 @@ import { afterEach, describe, expect, test } from 'vitest'
|
|
|
3
3
|
import {
|
|
4
4
|
hasKimakiAdminPermission,
|
|
5
5
|
hasKimakiBotPermission,
|
|
6
|
+
raceDiscordRename,
|
|
7
|
+
resolveFooterMentionUserId,
|
|
6
8
|
resolveGuildMessageMember,
|
|
9
|
+
resolveThreadFooterMentionUserId,
|
|
7
10
|
splitMarkdownForDiscord,
|
|
8
11
|
} from './discord-utils.js'
|
|
12
|
+
import type { ThreadChannel } from 'discord.js'
|
|
9
13
|
import { store } from './store.js'
|
|
10
14
|
|
|
11
15
|
describe('splitMarkdownForDiscord', () => {
|
|
@@ -292,3 +296,175 @@ describe('resolveGuildMessageMember', () => {
|
|
|
292
296
|
await expect(resolveGuildMessageMember(message)).resolves.toBe(null)
|
|
293
297
|
})
|
|
294
298
|
})
|
|
299
|
+
|
|
300
|
+
describe('resolveFooterMentionUserId', () => {
|
|
301
|
+
test('uses the session user when it is not the bot', () => {
|
|
302
|
+
expect(resolveFooterMentionUserId({
|
|
303
|
+
sessionUserId: 'user-1',
|
|
304
|
+
botUserId: 'bot-1',
|
|
305
|
+
threadOwnerId: 'bot-1',
|
|
306
|
+
members: [
|
|
307
|
+
{ id: 'bot-1', bot: true, joinedTimestamp: 1 },
|
|
308
|
+
{ id: 'user-2', bot: false, joinedTimestamp: 2 },
|
|
309
|
+
],
|
|
310
|
+
})).toBe('user-1')
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
test('skips bots and uses the earliest human member when the bot created the thread', () => {
|
|
314
|
+
expect(resolveFooterMentionUserId({
|
|
315
|
+
sessionUserId: 'bot-1',
|
|
316
|
+
botUserId: 'bot-1',
|
|
317
|
+
threadOwnerId: 'bot-1',
|
|
318
|
+
members: [
|
|
319
|
+
{ id: 'bot-1', bot: true, joinedTimestamp: 1 },
|
|
320
|
+
{ id: 'other-bot', bot: true, joinedTimestamp: 2 },
|
|
321
|
+
{ id: 'user-3', bot: false, joinedTimestamp: 4 },
|
|
322
|
+
{ id: 'user-2', bot: false, joinedTimestamp: 3 },
|
|
323
|
+
],
|
|
324
|
+
})).toBe('user-2')
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
test('does not mention anyone when the bot created the thread and no human member exists', () => {
|
|
328
|
+
expect(resolveFooterMentionUserId({
|
|
329
|
+
sessionUserId: 'bot-1',
|
|
330
|
+
botUserId: 'bot-1',
|
|
331
|
+
threadOwnerId: 'bot-1',
|
|
332
|
+
members: [
|
|
333
|
+
{ id: 'bot-1', bot: true, joinedTimestamp: 1 },
|
|
334
|
+
{ id: 'other-bot', bot: true, joinedTimestamp: 2 },
|
|
335
|
+
],
|
|
336
|
+
})).toBeUndefined()
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
test('uses the first human member when the bot created the thread and no session user is set', () => {
|
|
340
|
+
expect(resolveFooterMentionUserId({
|
|
341
|
+
sessionUserId: undefined,
|
|
342
|
+
botUserId: 'bot-1',
|
|
343
|
+
threadOwnerId: 'bot-1',
|
|
344
|
+
members: [
|
|
345
|
+
{ id: 'bot-1', bot: true, joinedTimestamp: 1 },
|
|
346
|
+
{ id: 'user-2', bot: false, joinedTimestamp: 2 },
|
|
347
|
+
],
|
|
348
|
+
})).toBe('user-2')
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
test('does not mention a member whose bot flag is unknown', () => {
|
|
352
|
+
expect(resolveFooterMentionUserId({
|
|
353
|
+
sessionUserId: undefined,
|
|
354
|
+
botUserId: 'bot-1',
|
|
355
|
+
threadOwnerId: 'bot-1',
|
|
356
|
+
members: [
|
|
357
|
+
{ id: 'bot-1', bot: true, joinedTimestamp: 1 },
|
|
358
|
+
{ id: 'maybe-user', joinedTimestamp: 2 },
|
|
359
|
+
],
|
|
360
|
+
})).toBeUndefined()
|
|
361
|
+
})
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
function fakeThread({
|
|
365
|
+
ownerId,
|
|
366
|
+
botUserId,
|
|
367
|
+
members,
|
|
368
|
+
onFetch,
|
|
369
|
+
}: {
|
|
370
|
+
ownerId: string
|
|
371
|
+
botUserId: string
|
|
372
|
+
members: Array<{ id: string; bot?: boolean; joinedTimestamp?: number | null }>
|
|
373
|
+
onFetch?: () => Promise<Map<string, { id: string; user?: { bot?: boolean }; joinedTimestamp?: number | null }>>
|
|
374
|
+
}): ThreadChannel {
|
|
375
|
+
const cache = new Map(members.map((member) => [
|
|
376
|
+
member.id,
|
|
377
|
+
{
|
|
378
|
+
id: member.id,
|
|
379
|
+
user: member.bot === undefined ? undefined : { bot: member.bot },
|
|
380
|
+
joinedTimestamp: member.joinedTimestamp ?? null,
|
|
381
|
+
},
|
|
382
|
+
]))
|
|
383
|
+
return {
|
|
384
|
+
ownerId,
|
|
385
|
+
client: { user: { id: botUserId } },
|
|
386
|
+
members: {
|
|
387
|
+
cache,
|
|
388
|
+
fetch: onFetch ?? (() => {
|
|
389
|
+
throw new Error('should not fetch thread members')
|
|
390
|
+
}),
|
|
391
|
+
},
|
|
392
|
+
} as ThreadChannel
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
describe('resolveThreadFooterMentionUserId', () => {
|
|
396
|
+
test('does not fetch when a human session user is already known', async () => {
|
|
397
|
+
await expect(resolveThreadFooterMentionUserId({
|
|
398
|
+
sessionUserId: 'user-1',
|
|
399
|
+
thread: fakeThread({
|
|
400
|
+
ownerId: 'bot-1',
|
|
401
|
+
botUserId: 'bot-1',
|
|
402
|
+
members: [{ id: 'bot-1', bot: true }],
|
|
403
|
+
}),
|
|
404
|
+
})).resolves.toBe('user-1')
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
test('does not fetch when the thread owner is not the bot', async () => {
|
|
408
|
+
await expect(resolveThreadFooterMentionUserId({
|
|
409
|
+
sessionUserId: undefined,
|
|
410
|
+
thread: fakeThread({
|
|
411
|
+
ownerId: 'user-1',
|
|
412
|
+
botUserId: 'bot-1',
|
|
413
|
+
members: [{ id: 'user-1', bot: false }],
|
|
414
|
+
}),
|
|
415
|
+
})).resolves.toBeUndefined()
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
test('uses cached humans without fetching', async () => {
|
|
419
|
+
await expect(resolveThreadFooterMentionUserId({
|
|
420
|
+
sessionUserId: undefined,
|
|
421
|
+
thread: fakeThread({
|
|
422
|
+
ownerId: 'bot-1',
|
|
423
|
+
botUserId: 'bot-1',
|
|
424
|
+
members: [
|
|
425
|
+
{ id: 'bot-1', bot: true, joinedTimestamp: 1 },
|
|
426
|
+
{ id: 'user-2', bot: false, joinedTimestamp: 2 },
|
|
427
|
+
],
|
|
428
|
+
}),
|
|
429
|
+
})).resolves.toBe('user-2')
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
test('fetches only when the bot owns the thread and no human is cached', async () => {
|
|
433
|
+
let fetched = false
|
|
434
|
+
const thread = fakeThread({
|
|
435
|
+
ownerId: 'bot-1',
|
|
436
|
+
botUserId: 'bot-1',
|
|
437
|
+
members: [{ id: 'bot-1', bot: true, joinedTimestamp: 1 }],
|
|
438
|
+
onFetch: async () => {
|
|
439
|
+
fetched = true
|
|
440
|
+
return new Map([
|
|
441
|
+
['bot-1', { id: 'bot-1', user: { bot: true }, joinedTimestamp: 1 }],
|
|
442
|
+
['user-2', { id: 'user-2', user: { bot: false }, joinedTimestamp: 2 }],
|
|
443
|
+
])
|
|
444
|
+
},
|
|
445
|
+
})
|
|
446
|
+
await expect(resolveThreadFooterMentionUserId({
|
|
447
|
+
sessionUserId: undefined,
|
|
448
|
+
thread,
|
|
449
|
+
})).resolves.toBe('user-2')
|
|
450
|
+
expect(fetched).toBe(true)
|
|
451
|
+
})
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
describe('raceDiscordRename', () => {
|
|
455
|
+
test('returns timeout when rename hangs', async () => {
|
|
456
|
+
const result = await raceDiscordRename({
|
|
457
|
+
rename: new Promise(() => {}),
|
|
458
|
+
timeoutMs: 20,
|
|
459
|
+
})
|
|
460
|
+
expect(result).toBe('timeout')
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
test('returns rename result when it finishes first', async () => {
|
|
464
|
+
const result = await raceDiscordRename({
|
|
465
|
+
rename: Promise.resolve('ok'),
|
|
466
|
+
timeoutMs: 1000,
|
|
467
|
+
})
|
|
468
|
+
expect(result).toBe('ok')
|
|
469
|
+
})
|
|
470
|
+
})
|
package/src/discord-utils.ts
CHANGED
|
@@ -21,6 +21,11 @@ import type { OpencodeClient } from '@opencode-ai/sdk/v2'
|
|
|
21
21
|
import { discordApiUrl } from './discord-urls.js'
|
|
22
22
|
import { Lexer } from 'marked'
|
|
23
23
|
import { splitTablesFromMarkdown } from './format-tables.js'
|
|
24
|
+
import {
|
|
25
|
+
sessionPartContent,
|
|
26
|
+
shouldLeadWithBlankLine,
|
|
27
|
+
type SessionChunk,
|
|
28
|
+
} from './message-formatting.js'
|
|
24
29
|
import { getChannelDirectory, getThreadWorktreeOrWorkspace } from './database.js'
|
|
25
30
|
import { DiscordOperationError } from './errors.js'
|
|
26
31
|
import { limitHeadingDepth } from './limit-heading-depth.js'
|
|
@@ -254,12 +259,6 @@ export async function archiveThread({
|
|
|
254
259
|
if (updateResult instanceof Error) {
|
|
255
260
|
discordLogger.warn(`[archive-thread] ${updateResult.message}`)
|
|
256
261
|
}
|
|
257
|
-
|
|
258
|
-
const abortResult = await client.session.abort({ sessionID: sessionId })
|
|
259
|
-
.catch((e) => new Error('Failed to abort session', { cause: e }))
|
|
260
|
-
if (abortResult instanceof Error) {
|
|
261
|
-
discordLogger.warn(`[archive-thread] ${abortResult.message}`)
|
|
262
|
-
}
|
|
263
262
|
}
|
|
264
263
|
|
|
265
264
|
if (archiveDelay > 0) {
|
|
@@ -275,6 +274,27 @@ export async function archiveThread({
|
|
|
275
274
|
})
|
|
276
275
|
}
|
|
277
276
|
|
|
277
|
+
export const DISCORD_THREAD_RENAME_TIMEOUT_MS = 3000
|
|
278
|
+
|
|
279
|
+
// Discord rename can hang on the 3rd call (~2 per 10 min). Race so it never blocks.
|
|
280
|
+
export async function raceDiscordRename<T>({
|
|
281
|
+
rename,
|
|
282
|
+
timeoutMs = DISCORD_THREAD_RENAME_TIMEOUT_MS,
|
|
283
|
+
}: {
|
|
284
|
+
rename: Promise<T>
|
|
285
|
+
timeoutMs?: number
|
|
286
|
+
}): Promise<T | 'timeout'> {
|
|
287
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs)
|
|
288
|
+
return Promise.race([
|
|
289
|
+
rename,
|
|
290
|
+
new Promise<'timeout'>((resolve) => {
|
|
291
|
+
timeoutSignal.addEventListener('abort', () => {
|
|
292
|
+
resolve('timeout')
|
|
293
|
+
})
|
|
294
|
+
}),
|
|
295
|
+
])
|
|
296
|
+
}
|
|
297
|
+
|
|
278
298
|
/**
|
|
279
299
|
* Add a user as a thread member so the thread shows up in their Discord left
|
|
280
300
|
* sidebar. Discord only lists threads you are a member of, so a scheduled
|
|
@@ -312,6 +332,91 @@ export async function ensureThreadMember({
|
|
|
312
332
|
if (addMemberResult instanceof Error) return addMemberResult
|
|
313
333
|
}
|
|
314
334
|
|
|
335
|
+
export type FooterMentionMember = {
|
|
336
|
+
id: string
|
|
337
|
+
bot?: boolean
|
|
338
|
+
joinedTimestamp?: number | null
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function resolveFooterMentionUserId({
|
|
342
|
+
sessionUserId,
|
|
343
|
+
botUserId,
|
|
344
|
+
threadOwnerId,
|
|
345
|
+
members,
|
|
346
|
+
}: {
|
|
347
|
+
sessionUserId: string | undefined
|
|
348
|
+
botUserId: string | undefined
|
|
349
|
+
threadOwnerId: string | undefined
|
|
350
|
+
members: FooterMentionMember[]
|
|
351
|
+
}): string | undefined {
|
|
352
|
+
if (sessionUserId && sessionUserId !== botUserId) return sessionUserId
|
|
353
|
+
if (!botUserId || threadOwnerId !== botUserId) return undefined
|
|
354
|
+
const humans = members
|
|
355
|
+
.filter((member) => member.id && member.id !== botUserId && member.bot === false)
|
|
356
|
+
.sort((a, b) => {
|
|
357
|
+
const aJoined = a.joinedTimestamp ?? Number.POSITIVE_INFINITY
|
|
358
|
+
const bJoined = b.joinedTimestamp ?? Number.POSITIVE_INFINITY
|
|
359
|
+
if (aJoined !== bJoined) return aJoined - bJoined
|
|
360
|
+
return a.id.localeCompare(b.id)
|
|
361
|
+
})
|
|
362
|
+
return humans[0]?.id
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function footerMentionMembersFromCache(thread: ThreadChannel): FooterMentionMember[] {
|
|
366
|
+
return [...thread.members.cache.values()].map((member) => ({
|
|
367
|
+
id: member.id,
|
|
368
|
+
bot: member.user?.bot,
|
|
369
|
+
joinedTimestamp: member.joinedTimestamp ?? null,
|
|
370
|
+
}))
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export async function resolveThreadFooterMentionUserId({
|
|
374
|
+
sessionUserId,
|
|
375
|
+
thread,
|
|
376
|
+
}: {
|
|
377
|
+
sessionUserId: string | undefined
|
|
378
|
+
thread: ThreadChannel
|
|
379
|
+
}): Promise<string | undefined> {
|
|
380
|
+
const botUserId = thread.client.user?.id
|
|
381
|
+
if (sessionUserId && sessionUserId !== botUserId) return sessionUserId
|
|
382
|
+
if (!botUserId || thread.ownerId !== botUserId) return undefined
|
|
383
|
+
|
|
384
|
+
const cached = footerMentionMembersFromCache(thread)
|
|
385
|
+
if (cached.some((member) => member.bot === false && member.id !== botUserId)) {
|
|
386
|
+
return resolveFooterMentionUserId({
|
|
387
|
+
sessionUserId,
|
|
388
|
+
botUserId,
|
|
389
|
+
threadOwnerId: thread.ownerId ?? undefined,
|
|
390
|
+
members: cached,
|
|
391
|
+
})
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const fetched = await thread.members.fetch().catch((e) => {
|
|
395
|
+
return new DiscordOperationError({ operation: 'fetchThreadMembers', cause: e })
|
|
396
|
+
})
|
|
397
|
+
if (fetched instanceof Error) {
|
|
398
|
+
discordLogger.warn(`[FOOTER] Failed to fetch thread members: ${fetched.message}`)
|
|
399
|
+
return resolveFooterMentionUserId({
|
|
400
|
+
sessionUserId,
|
|
401
|
+
botUserId,
|
|
402
|
+
threadOwnerId: thread.ownerId ?? undefined,
|
|
403
|
+
members: cached,
|
|
404
|
+
})
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const members = [...fetched.values()].map((member) => ({
|
|
408
|
+
id: member.id,
|
|
409
|
+
bot: member.user?.bot,
|
|
410
|
+
joinedTimestamp: member.joinedTimestamp ?? null,
|
|
411
|
+
}))
|
|
412
|
+
return resolveFooterMentionUserId({
|
|
413
|
+
sessionUserId,
|
|
414
|
+
botUserId,
|
|
415
|
+
threadOwnerId: thread.ownerId ?? undefined,
|
|
416
|
+
members,
|
|
417
|
+
})
|
|
418
|
+
}
|
|
419
|
+
|
|
315
420
|
/** Remove Discord mentions from text so they don't appear in thread titles */
|
|
316
421
|
export function stripMentions(text: string): string {
|
|
317
422
|
return text
|
|
@@ -611,7 +716,7 @@ export function splitMarkdownForDiscord({
|
|
|
611
716
|
}
|
|
612
717
|
|
|
613
718
|
export async function sendThreadMessage(
|
|
614
|
-
thread: ThreadChannel,
|
|
719
|
+
thread: ThreadChannel | TextChannel,
|
|
615
720
|
content: string,
|
|
616
721
|
options?: { flags?: number },
|
|
617
722
|
): Promise<Message> {
|
|
@@ -675,6 +780,46 @@ export async function sendThreadMessage(
|
|
|
675
780
|
return firstMessage!
|
|
676
781
|
}
|
|
677
782
|
|
|
783
|
+
export async function sendSessionPartMessage(
|
|
784
|
+
thread: ThreadChannel,
|
|
785
|
+
content: string,
|
|
786
|
+
options?: {
|
|
787
|
+
leadWithBlankLine?: boolean
|
|
788
|
+
flags?: number
|
|
789
|
+
},
|
|
790
|
+
): Promise<Message> {
|
|
791
|
+
return sendThreadMessage(
|
|
792
|
+
thread,
|
|
793
|
+
sessionPartContent({
|
|
794
|
+
content,
|
|
795
|
+
leadWithBlankLine: options?.leadWithBlankLine === true,
|
|
796
|
+
}),
|
|
797
|
+
{ flags: options?.flags },
|
|
798
|
+
)
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
export async function sendSessionPartBatches({
|
|
802
|
+
thread,
|
|
803
|
+
batches,
|
|
804
|
+
}: {
|
|
805
|
+
thread: ThreadChannel
|
|
806
|
+
batches: SessionChunk[]
|
|
807
|
+
}) {
|
|
808
|
+
const sent: Array<{ partIds: string[]; message: Message }> = []
|
|
809
|
+
let previousKind: SessionChunk['kind'] | undefined
|
|
810
|
+
for (const batch of batches) {
|
|
811
|
+
const message = await sendSessionPartMessage(thread, batch.content, {
|
|
812
|
+
leadWithBlankLine: shouldLeadWithBlankLine({
|
|
813
|
+
previousKind,
|
|
814
|
+
nextKind: batch.kind,
|
|
815
|
+
}),
|
|
816
|
+
})
|
|
817
|
+
previousKind = batch.kind
|
|
818
|
+
sent.push({ partIds: batch.partIds, message })
|
|
819
|
+
}
|
|
820
|
+
return sent
|
|
821
|
+
}
|
|
822
|
+
|
|
678
823
|
export function isThreadChannelType(type: number): boolean {
|
|
679
824
|
return [
|
|
680
825
|
ChannelType.PublicThread,
|
package/src/errors.ts
CHANGED
|
@@ -24,6 +24,12 @@ export class ServerNotReadyError extends errore.createTaggedError({
|
|
|
24
24
|
'OpenCode client for directory "$directory" is not available because the shared server is not ready',
|
|
25
25
|
}) {}
|
|
26
26
|
|
|
27
|
+
export class OpencodeIncompatibleVersionError extends errore.createTaggedError({
|
|
28
|
+
name: 'OpencodeIncompatibleVersionError',
|
|
29
|
+
message:
|
|
30
|
+
'Kimaki is not compatible with OpenCode version $version. Install an OpenCode 1.x release.',
|
|
31
|
+
}) {}
|
|
32
|
+
|
|
27
33
|
export class ApiKeyMissingError extends errore.createTaggedError({
|
|
28
34
|
name: 'ApiKeyMissingError',
|
|
29
35
|
message: '$service API key is required',
|
|
@@ -117,6 +123,11 @@ export class OpenCodeSdkError extends errore.createTaggedError({
|
|
|
117
123
|
message: 'OpenCode SDK call failed: $operation',
|
|
118
124
|
}) {}
|
|
119
125
|
|
|
126
|
+
export class InvalidModelError extends errore.createTaggedError({
|
|
127
|
+
name: 'InvalidModelError',
|
|
128
|
+
message: 'Invalid model "$model": $reason',
|
|
129
|
+
}) {}
|
|
130
|
+
|
|
120
131
|
export class FilesystemOperationError extends errore.createTaggedError({
|
|
121
132
|
name: 'FilesystemOperationError',
|
|
122
133
|
message: 'Filesystem operation failed: $operation',
|
|
@@ -214,6 +225,7 @@ export type OpenCodeErrors =
|
|
|
214
225
|
| DirectoryNotAccessibleError
|
|
215
226
|
| ServerStartError
|
|
216
227
|
| ServerNotReadyError
|
|
228
|
+
| OpencodeIncompatibleVersionError
|
|
217
229
|
|
|
218
230
|
export type SessionErrors =
|
|
219
231
|
| SessionNotFoundError
|
package/src/exec-async.ts
CHANGED
|
@@ -1,32 +1,41 @@
|
|
|
1
|
-
import { exec } from 'node:child_process'
|
|
1
|
+
import { exec, execFile } from 'node:child_process'
|
|
2
2
|
import { promisify } from 'node:util'
|
|
3
3
|
|
|
4
4
|
const DEFAULT_EXEC_TIMEOUT_MS = 10_000
|
|
5
5
|
|
|
6
6
|
const _execAsync = promisify(exec)
|
|
7
|
+
const _execFileAsync = promisify(execFile)
|
|
7
8
|
|
|
8
9
|
export function execAsync(
|
|
9
|
-
command: string,
|
|
10
|
+
command: string | { command: string; args: string[] },
|
|
10
11
|
options?: Parameters<typeof _execAsync>[1],
|
|
11
12
|
): Promise<{ stdout: string; stderr: string }> {
|
|
12
|
-
const timeoutMs =
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
const timeoutMs = options?.timeout || DEFAULT_EXEC_TIMEOUT_MS
|
|
14
|
+
const childPromise =
|
|
15
|
+
typeof command === 'string'
|
|
16
|
+
? _execAsync(command, options)
|
|
17
|
+
: _execFileAsync(command.command, command.args, options)
|
|
18
|
+
const execPromise = childPromise.then(({ stdout, stderr }) => {
|
|
19
|
+
return {
|
|
20
|
+
stdout: typeof stdout === 'string' ? stdout : stdout.toString(),
|
|
21
|
+
stderr: typeof stderr === 'string' ? stderr : stderr.toString(),
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
const commandLabel = typeof command === 'string'
|
|
25
|
+
? command
|
|
26
|
+
: [command.command, ...command.args].join(' ')
|
|
18
27
|
let timer: ReturnType<typeof setTimeout> | undefined
|
|
19
28
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
20
29
|
timer = setTimeout(() => {
|
|
21
|
-
const pid =
|
|
30
|
+
const pid = childPromise.child?.pid
|
|
22
31
|
if (pid) {
|
|
23
32
|
try {
|
|
24
33
|
process.kill(-pid, 'SIGTERM')
|
|
25
34
|
} catch {
|
|
26
|
-
|
|
35
|
+
childPromise.child?.kill('SIGTERM')
|
|
27
36
|
}
|
|
28
37
|
}
|
|
29
|
-
reject(new Error(`Command timed out after ${timeoutMs}ms: ${
|
|
38
|
+
reject(new Error(`Command timed out after ${timeoutMs}ms: ${commandLabel}`))
|
|
30
39
|
}, timeoutMs)
|
|
31
40
|
})
|
|
32
41
|
return Promise.race([execPromise, timeoutPromise]).finally(() => {
|
|
@@ -81,18 +81,22 @@ describe('external directory permissions', () => {
|
|
|
81
81
|
"--- from: user (external-directory-tester)
|
|
82
82
|
EXTERNAL_DIRECTORY_PROBE_MARKER first
|
|
83
83
|
--- from: assistant (TestBot)
|
|
84
|
-
*using deterministic-provider/deterministic-v2*
|
|
85
|
-
|
|
84
|
+
> *using deterministic-provider/deterministic-v2*
|
|
85
|
+
> reading external directory
|
|
86
|
+
|
|
86
87
|
┣ read *probe.txt*
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
|
|
89
|
+
> external-directory-probe-done
|
|
90
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>
|
|
89
91
|
--- from: user (external-directory-tester)
|
|
90
92
|
EXTERNAL_DIRECTORY_PROBE_MARKER followup
|
|
91
93
|
--- from: assistant (TestBot)
|
|
92
|
-
|
|
94
|
+
> reading external directory
|
|
95
|
+
|
|
93
96
|
┣ read *probe.txt*
|
|
94
|
-
|
|
95
|
-
|
|
97
|
+
|
|
98
|
+
> external-directory-probe-done
|
|
99
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>"
|
|
96
100
|
`)
|
|
97
101
|
expect(text).not.toContain('Permission Required')
|
|
98
102
|
})
|
|
@@ -67,10 +67,12 @@ describe('external directory project deny', () => {
|
|
|
67
67
|
"--- from: user (external-directory-deny-tester)
|
|
68
68
|
EXTERNAL_DIRECTORY_PROBE_MARKER denied
|
|
69
69
|
--- from: assistant (TestBot)
|
|
70
|
-
*using deterministic-provider/deterministic-v2*
|
|
71
|
-
|
|
70
|
+
> *using deterministic-provider/deterministic-v2*
|
|
71
|
+
> reading external directory
|
|
72
|
+
|
|
72
73
|
┣ read *probe.txt*
|
|
73
|
-
|
|
74
|
+
|
|
75
|
+
> external-directory-probe-denied"
|
|
74
76
|
`)
|
|
75
77
|
|
|
76
78
|
// A deny is silent: it must not fall back to asking the user.
|