@brianli/kimaki 0.4.72-brianli.1
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/bin.js +2 -0
- package/dist/ai-tool-to-genai.js +233 -0
- package/dist/ai-tool-to-genai.test.js +267 -0
- package/dist/ai-tool.js +6 -0
- package/dist/bin.js +87 -0
- package/dist/bot-token.js +121 -0
- package/dist/bot-token.test.js +134 -0
- package/dist/channel-management.js +101 -0
- package/dist/cli-parsing.test.js +89 -0
- package/dist/cli.js +2529 -0
- package/dist/commands/abort.js +82 -0
- package/dist/commands/action-buttons.js +257 -0
- package/dist/commands/add-project.js +114 -0
- package/dist/commands/agent.js +291 -0
- package/dist/commands/ask-question.js +223 -0
- package/dist/commands/compact.js +120 -0
- package/dist/commands/context-usage.js +140 -0
- package/dist/commands/create-new-project.js +118 -0
- package/dist/commands/diff.js +128 -0
- package/dist/commands/file-upload.js +275 -0
- package/dist/commands/fork.js +217 -0
- package/dist/commands/gemini-apikey.js +70 -0
- package/dist/commands/login.js +490 -0
- package/dist/commands/mention-mode.js +51 -0
- package/dist/commands/merge-worktree.js +124 -0
- package/dist/commands/model.js +694 -0
- package/dist/commands/permissions.js +163 -0
- package/dist/commands/queue.js +217 -0
- package/dist/commands/remove-project.js +115 -0
- package/dist/commands/restart-opencode-server.js +116 -0
- package/dist/commands/resume.js +159 -0
- package/dist/commands/run-command.js +79 -0
- package/dist/commands/session-id.js +78 -0
- package/dist/commands/session.js +192 -0
- package/dist/commands/share.js +80 -0
- package/dist/commands/types.js +2 -0
- package/dist/commands/undo-redo.js +159 -0
- package/dist/commands/unset-model.js +152 -0
- package/dist/commands/upgrade.js +42 -0
- package/dist/commands/user-command.js +148 -0
- package/dist/commands/verbosity.js +60 -0
- package/dist/commands/worktree-settings.js +50 -0
- package/dist/commands/worktree.js +299 -0
- package/dist/condense-memory.js +33 -0
- package/dist/config.js +110 -0
- package/dist/database.js +1050 -0
- package/dist/db.js +159 -0
- package/dist/db.test.js +49 -0
- package/dist/discord-api.js +28 -0
- package/dist/discord-auth.js +231 -0
- package/dist/discord-auth.test.js +80 -0
- package/dist/discord-bot.js +997 -0
- package/dist/discord-utils.js +560 -0
- package/dist/discord-utils.test.js +115 -0
- package/dist/errors.js +167 -0
- package/dist/escape-backticks.test.js +429 -0
- package/dist/format-tables.js +122 -0
- package/dist/format-tables.test.js +199 -0
- package/dist/forum-sync/config.js +79 -0
- package/dist/forum-sync/discord-operations.js +154 -0
- package/dist/forum-sync/index.js +5 -0
- package/dist/forum-sync/markdown.js +117 -0
- package/dist/forum-sync/sync-to-discord.js +417 -0
- package/dist/forum-sync/sync-to-files.js +190 -0
- package/dist/forum-sync/types.js +53 -0
- package/dist/forum-sync/watchers.js +307 -0
- package/dist/gateway-consumer.js +232 -0
- package/dist/gateway-consumer.test.js +18 -0
- package/dist/genai-worker-wrapper.js +111 -0
- package/dist/genai-worker.js +311 -0
- package/dist/genai.js +232 -0
- package/dist/generated/browser.js +17 -0
- package/dist/generated/client.js +35 -0
- package/dist/generated/commonInputTypes.js +10 -0
- package/dist/generated/enums.js +30 -0
- package/dist/generated/internal/class.js +41 -0
- package/dist/generated/internal/prismaNamespace.js +239 -0
- package/dist/generated/internal/prismaNamespaceBrowser.js +209 -0
- package/dist/generated/models/bot_api_keys.js +1 -0
- package/dist/generated/models/bot_tokens.js +1 -0
- package/dist/generated/models/channel_agents.js +1 -0
- package/dist/generated/models/channel_directories.js +1 -0
- package/dist/generated/models/channel_mention_mode.js +1 -0
- package/dist/generated/models/channel_models.js +1 -0
- package/dist/generated/models/channel_verbosity.js +1 -0
- package/dist/generated/models/channel_worktrees.js +1 -0
- package/dist/generated/models/forum_sync_configs.js +1 -0
- package/dist/generated/models/global_models.js +1 -0
- package/dist/generated/models/ipc_requests.js +1 -0
- package/dist/generated/models/part_messages.js +1 -0
- package/dist/generated/models/scheduled_tasks.js +1 -0
- package/dist/generated/models/session_agents.js +1 -0
- package/dist/generated/models/session_models.js +1 -0
- package/dist/generated/models/session_start_sources.js +1 -0
- package/dist/generated/models/thread_sessions.js +1 -0
- package/dist/generated/models/thread_worktrees.js +1 -0
- package/dist/generated/models.js +1 -0
- package/dist/heap-monitor.js +95 -0
- package/dist/hrana-server.js +416 -0
- package/dist/hrana-server.test.js +368 -0
- package/dist/image-utils.js +112 -0
- package/dist/interaction-handler.js +327 -0
- package/dist/ipc-polling.js +251 -0
- package/dist/kimaki-digital-twin.e2e.test.js +165 -0
- package/dist/limit-heading-depth.js +25 -0
- package/dist/limit-heading-depth.test.js +105 -0
- package/dist/logger.js +160 -0
- package/dist/markdown.js +342 -0
- package/dist/markdown.test.js +253 -0
- package/dist/message-formatting.js +433 -0
- package/dist/message-formatting.test.js +73 -0
- package/dist/openai-realtime.js +228 -0
- package/dist/opencode-plugin-loading.e2e.test.js +91 -0
- package/dist/opencode-plugin.js +536 -0
- package/dist/opencode-plugin.test.js +98 -0
- package/dist/opencode.js +409 -0
- package/dist/privacy-sanitizer.js +105 -0
- package/dist/runtime-mode.js +51 -0
- package/dist/runtime-mode.test.js +115 -0
- package/dist/sentry.js +127 -0
- package/dist/session-handler/state.js +151 -0
- package/dist/session-handler.js +1874 -0
- package/dist/session-search.js +100 -0
- package/dist/session-search.test.js +40 -0
- package/dist/startup-service.js +153 -0
- package/dist/system-message.js +499 -0
- package/dist/task-runner.js +282 -0
- package/dist/task-schedule.js +191 -0
- package/dist/task-schedule.test.js +71 -0
- package/dist/thinking-utils.js +35 -0
- package/dist/thread-message-queue.e2e.test.js +781 -0
- package/dist/tools.js +359 -0
- package/dist/unnest-code-blocks.js +136 -0
- package/dist/unnest-code-blocks.test.js +641 -0
- package/dist/upgrade.js +114 -0
- package/dist/utils.js +109 -0
- package/dist/voice-handler.js +606 -0
- package/dist/voice.js +304 -0
- package/dist/voice.test.js +187 -0
- package/dist/wait-session.js +94 -0
- package/dist/worker-types.js +4 -0
- package/dist/worktree-utils.js +727 -0
- package/dist/xml.js +92 -0
- package/dist/xml.test.js +32 -0
- package/package.json +82 -0
- package/schema.prisma +246 -0
- package/skills/batch/SKILL.md +87 -0
- package/skills/critique/SKILL.md +129 -0
- package/skills/errore/SKILL.md +589 -0
- package/skills/goke/.prettierrc +5 -0
- package/skills/goke/CHANGELOG.md +40 -0
- package/skills/goke/LICENSE +21 -0
- package/skills/goke/README.md +666 -0
- package/skills/goke/SKILL.md +458 -0
- package/skills/goke/package.json +43 -0
- package/skills/goke/src/__test__/coerce.test.ts +411 -0
- package/skills/goke/src/__test__/index.test.ts +1798 -0
- package/skills/goke/src/__test__/types.test-d.ts +111 -0
- package/skills/goke/src/coerce.ts +547 -0
- package/skills/goke/src/goke.ts +1362 -0
- package/skills/goke/src/index.ts +16 -0
- package/skills/goke/src/mri.ts +164 -0
- package/skills/goke/tsconfig.json +15 -0
- package/skills/jitter/EDITOR.md +219 -0
- package/skills/jitter/EXPORT-INTERNALS.md +309 -0
- package/skills/jitter/SKILL.md +158 -0
- package/skills/jitter/jitter-clipboard.json +1042 -0
- package/skills/jitter/package.json +14 -0
- package/skills/jitter/tsconfig.json +15 -0
- package/skills/jitter/utils/actions.ts +212 -0
- package/skills/jitter/utils/export.ts +114 -0
- package/skills/jitter/utils/index.ts +141 -0
- package/skills/jitter/utils/snapshot.ts +154 -0
- package/skills/jitter/utils/traverse.ts +246 -0
- package/skills/jitter/utils/types.ts +279 -0
- package/skills/jitter/utils/wait.ts +133 -0
- package/skills/playwriter/SKILL.md +31 -0
- package/skills/security-review/SKILL.md +208 -0
- package/skills/simplify/SKILL.md +58 -0
- package/skills/termcast/SKILL.md +945 -0
- package/skills/tuistory/SKILL.md +250 -0
- package/skills/zustand-centralized-state/SKILL.md +582 -0
- package/src/__snapshots__/compact-session-context-no-system.md +35 -0
- package/src/__snapshots__/compact-session-context.md +41 -0
- package/src/__snapshots__/first-session-no-info.md +17 -0
- package/src/__snapshots__/first-session-with-info.md +23 -0
- package/src/__snapshots__/session-1.md +17 -0
- package/src/__snapshots__/session-2.md +5871 -0
- package/src/__snapshots__/session-3.md +17 -0
- package/src/__snapshots__/session-with-tools.md +5871 -0
- package/src/ai-tool-to-genai.test.ts +296 -0
- package/src/ai-tool-to-genai.ts +282 -0
- package/src/ai-tool.ts +39 -0
- package/src/bin.ts +108 -0
- package/src/bot-token.test.ts +171 -0
- package/src/bot-token.ts +159 -0
- package/src/channel-management.ts +172 -0
- package/src/cli-parsing.test.ts +132 -0
- package/src/cli.ts +3605 -0
- package/src/commands/abort.ts +112 -0
- package/src/commands/action-buttons.ts +376 -0
- package/src/commands/add-project.ts +152 -0
- package/src/commands/agent.ts +404 -0
- package/src/commands/ask-question.ts +330 -0
- package/src/commands/compact.ts +157 -0
- package/src/commands/context-usage.ts +199 -0
- package/src/commands/create-new-project.ts +179 -0
- package/src/commands/diff.ts +165 -0
- package/src/commands/file-upload.ts +389 -0
- package/src/commands/fork.ts +320 -0
- package/src/commands/gemini-apikey.ts +104 -0
- package/src/commands/login.ts +634 -0
- package/src/commands/mention-mode.ts +77 -0
- package/src/commands/merge-worktree.ts +177 -0
- package/src/commands/model.ts +961 -0
- package/src/commands/permissions.ts +261 -0
- package/src/commands/queue.ts +296 -0
- package/src/commands/remove-project.ts +155 -0
- package/src/commands/restart-opencode-server.ts +162 -0
- package/src/commands/resume.ts +242 -0
- package/src/commands/run-command.ts +123 -0
- package/src/commands/session-id.ts +109 -0
- package/src/commands/session.ts +250 -0
- package/src/commands/share.ts +106 -0
- package/src/commands/types.ts +25 -0
- package/src/commands/undo-redo.ts +221 -0
- package/src/commands/unset-model.ts +189 -0
- package/src/commands/upgrade.ts +52 -0
- package/src/commands/user-command.ts +193 -0
- package/src/commands/verbosity.ts +88 -0
- package/src/commands/worktree-settings.ts +79 -0
- package/src/commands/worktree.ts +431 -0
- package/src/condense-memory.ts +36 -0
- package/src/config.ts +148 -0
- package/src/database.ts +1530 -0
- package/src/db.test.ts +60 -0
- package/src/db.ts +190 -0
- package/src/discord-api.ts +35 -0
- package/src/discord-bot.ts +1316 -0
- package/src/discord-utils.test.ts +132 -0
- package/src/discord-utils.ts +767 -0
- package/src/errors.ts +213 -0
- package/src/escape-backticks.test.ts +469 -0
- package/src/format-tables.test.ts +223 -0
- package/src/format-tables.ts +145 -0
- package/src/forum-sync/config.ts +92 -0
- package/src/forum-sync/discord-operations.ts +241 -0
- package/src/forum-sync/index.ts +9 -0
- package/src/forum-sync/markdown.ts +176 -0
- package/src/forum-sync/sync-to-discord.ts +595 -0
- package/src/forum-sync/sync-to-files.ts +294 -0
- package/src/forum-sync/types.ts +175 -0
- package/src/forum-sync/watchers.ts +454 -0
- package/src/genai-worker-wrapper.ts +164 -0
- package/src/genai-worker.ts +386 -0
- package/src/genai.ts +321 -0
- package/src/generated/browser.ts +109 -0
- package/src/generated/client.ts +131 -0
- package/src/generated/commonInputTypes.ts +512 -0
- package/src/generated/enums.ts +46 -0
- package/src/generated/internal/class.ts +362 -0
- package/src/generated/internal/prismaNamespace.ts +2251 -0
- package/src/generated/internal/prismaNamespaceBrowser.ts +308 -0
- package/src/generated/models/bot_api_keys.ts +1288 -0
- package/src/generated/models/bot_tokens.ts +1577 -0
- package/src/generated/models/channel_agents.ts +1256 -0
- package/src/generated/models/channel_directories.ts +2104 -0
- package/src/generated/models/channel_mention_mode.ts +1300 -0
- package/src/generated/models/channel_models.ts +1288 -0
- package/src/generated/models/channel_verbosity.ts +1224 -0
- package/src/generated/models/channel_worktrees.ts +1308 -0
- package/src/generated/models/forum_sync_configs.ts +1452 -0
- package/src/generated/models/global_models.ts +1288 -0
- package/src/generated/models/ipc_requests.ts +1485 -0
- package/src/generated/models/part_messages.ts +1302 -0
- package/src/generated/models/scheduled_tasks.ts +2320 -0
- package/src/generated/models/session_agents.ts +1086 -0
- package/src/generated/models/session_models.ts +1114 -0
- package/src/generated/models/session_start_sources.ts +1408 -0
- package/src/generated/models/thread_sessions.ts +1599 -0
- package/src/generated/models/thread_worktrees.ts +1352 -0
- package/src/generated/models.ts +29 -0
- package/src/heap-monitor.ts +121 -0
- package/src/hrana-server.test.ts +428 -0
- package/src/hrana-server.ts +547 -0
- package/src/image-utils.ts +149 -0
- package/src/interaction-handler.ts +461 -0
- package/src/ipc-polling.ts +325 -0
- package/src/kimaki-digital-twin.e2e.test.ts +201 -0
- package/src/limit-heading-depth.test.ts +116 -0
- package/src/limit-heading-depth.ts +26 -0
- package/src/logger.ts +203 -0
- package/src/markdown.test.ts +360 -0
- package/src/markdown.ts +410 -0
- package/src/message-formatting.test.ts +81 -0
- package/src/message-formatting.ts +549 -0
- package/src/openai-realtime.ts +362 -0
- package/src/opencode-plugin-loading.e2e.test.ts +112 -0
- package/src/opencode-plugin.test.ts +108 -0
- package/src/opencode-plugin.ts +652 -0
- package/src/opencode.ts +554 -0
- package/src/privacy-sanitizer.ts +142 -0
- package/src/schema.sql +158 -0
- package/src/sentry.ts +137 -0
- package/src/session-handler/state.ts +232 -0
- package/src/session-handler.ts +2668 -0
- package/src/session-search.test.ts +50 -0
- package/src/session-search.ts +148 -0
- package/src/startup-service.ts +200 -0
- package/src/system-message.ts +568 -0
- package/src/task-runner.ts +425 -0
- package/src/task-schedule.test.ts +84 -0
- package/src/task-schedule.ts +287 -0
- package/src/thinking-utils.ts +61 -0
- package/src/thread-message-queue.e2e.test.ts +997 -0
- package/src/tools.ts +432 -0
- package/src/unnest-code-blocks.test.ts +679 -0
- package/src/unnest-code-blocks.ts +168 -0
- package/src/upgrade.ts +127 -0
- package/src/utils.ts +145 -0
- package/src/voice-handler.ts +852 -0
- package/src/voice.test.ts +219 -0
- package/src/voice.ts +444 -0
- package/src/wait-session.ts +147 -0
- package/src/worker-types.ts +64 -0
- package/src/worktree-utils.ts +988 -0
- package/src/xml.test.ts +38 -0
- package/src/xml.ts +121 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// /restart-opencode-server command - Restart the opencode server for the current channel.
|
|
2
|
+
// Used for resolving opencode state issues, internal bugs, refreshing auth state, plugins, etc.
|
|
3
|
+
// Aborts all in-progress sessions in this channel before restarting to avoid orphaned requests.
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
ChannelType,
|
|
7
|
+
MessageFlags,
|
|
8
|
+
type ThreadChannel,
|
|
9
|
+
type TextChannel,
|
|
10
|
+
} from 'discord.js'
|
|
11
|
+
import type { CommandContext } from './types.js'
|
|
12
|
+
import {
|
|
13
|
+
initializeOpencodeForDirectory,
|
|
14
|
+
restartOpencodeServer,
|
|
15
|
+
} from '../opencode.js'
|
|
16
|
+
import {
|
|
17
|
+
resolveWorkingDirectory,
|
|
18
|
+
SILENT_MESSAGE_FLAGS,
|
|
19
|
+
} from '../discord-utils.js'
|
|
20
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
21
|
+
import { getAllThreadSessionIds, getThreadIdBySessionId } from '../database.js'
|
|
22
|
+
import { abortControllers } from '../session-handler.js'
|
|
23
|
+
import { SessionAbortError } from '../errors.js'
|
|
24
|
+
import * as errore from 'errore'
|
|
25
|
+
|
|
26
|
+
const logger = createLogger(LogPrefix.OPENCODE)
|
|
27
|
+
|
|
28
|
+
export async function handleRestartOpencodeServerCommand({
|
|
29
|
+
command,
|
|
30
|
+
appId,
|
|
31
|
+
}: CommandContext): Promise<void> {
|
|
32
|
+
const channel = command.channel
|
|
33
|
+
|
|
34
|
+
if (!channel) {
|
|
35
|
+
await command.reply({
|
|
36
|
+
content: 'This command can only be used in a channel',
|
|
37
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
38
|
+
})
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const isThread = [
|
|
43
|
+
ChannelType.PublicThread,
|
|
44
|
+
ChannelType.PrivateThread,
|
|
45
|
+
ChannelType.AnnouncementThread,
|
|
46
|
+
].includes(channel.type)
|
|
47
|
+
|
|
48
|
+
const isTextChannel = channel.type === ChannelType.GuildText
|
|
49
|
+
|
|
50
|
+
if (!isThread && !isTextChannel) {
|
|
51
|
+
await command.reply({
|
|
52
|
+
content: 'This command can only be used in text channels or threads',
|
|
53
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
54
|
+
})
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const resolved = await resolveWorkingDirectory({
|
|
59
|
+
channel: channel as TextChannel | ThreadChannel,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
if (!resolved) {
|
|
63
|
+
await command.reply({
|
|
64
|
+
content: 'Could not determine project directory for this channel',
|
|
65
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
66
|
+
})
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const { projectDirectory, channelAppId } = resolved
|
|
71
|
+
|
|
72
|
+
if (channelAppId && channelAppId !== appId) {
|
|
73
|
+
await command.reply({
|
|
74
|
+
content: 'This channel is not configured for this bot',
|
|
75
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
76
|
+
})
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Defer reply since restart may take a moment
|
|
81
|
+
await command.deferReply({ flags: SILENT_MESSAGE_FLAGS })
|
|
82
|
+
|
|
83
|
+
// Abort all in-progress sessions in this channel before restarting.
|
|
84
|
+
// Find sessions with active abort controllers, check if their thread belongs
|
|
85
|
+
// to this channel (thread parentId matches, or command was run in the thread itself).
|
|
86
|
+
const parentChannelId = isThread
|
|
87
|
+
? (channel as ThreadChannel).parentId
|
|
88
|
+
: channel.id
|
|
89
|
+
const activeSessionIds = [...abortControllers.keys()]
|
|
90
|
+
let abortedCount = 0
|
|
91
|
+
|
|
92
|
+
if (activeSessionIds.length > 0) {
|
|
93
|
+
const getClient = await initializeOpencodeForDirectory(projectDirectory)
|
|
94
|
+
const client = !(getClient instanceof Error) ? getClient : null
|
|
95
|
+
|
|
96
|
+
for (const sessionId of activeSessionIds) {
|
|
97
|
+
const threadId = await getThreadIdBySessionId(sessionId)
|
|
98
|
+
if (!threadId) {
|
|
99
|
+
continue
|
|
100
|
+
}
|
|
101
|
+
// Check if thread belongs to this channel: either the thread IS this channel,
|
|
102
|
+
// or the thread's parent matches the parent channel
|
|
103
|
+
const threadChannel = await errore.tryAsync(() => {
|
|
104
|
+
return command.client.channels.fetch(threadId)
|
|
105
|
+
})
|
|
106
|
+
if (threadChannel instanceof Error || !threadChannel) {
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
const threadParentId =
|
|
110
|
+
'parentId' in threadChannel ? threadChannel.parentId : null
|
|
111
|
+
if (threadId !== channel.id && threadParentId !== parentChannelId) {
|
|
112
|
+
continue
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const controller = abortControllers.get(sessionId)
|
|
116
|
+
if (controller) {
|
|
117
|
+
logger.log(
|
|
118
|
+
`[RESTART] Aborting session ${sessionId} in thread ${threadId}`,
|
|
119
|
+
)
|
|
120
|
+
controller.abort(new SessionAbortError({ reason: 'server-restart' }))
|
|
121
|
+
abortControllers.delete(sessionId)
|
|
122
|
+
abortedCount++
|
|
123
|
+
}
|
|
124
|
+
if (client) {
|
|
125
|
+
await errore.tryAsync(() => {
|
|
126
|
+
return client().session.abort({ sessionID: sessionId })
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (abortedCount > 0) {
|
|
133
|
+
logger.log(
|
|
134
|
+
`[RESTART] Aborted ${abortedCount} active session(s) before restart`,
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
logger.log(
|
|
139
|
+
`[RESTART] Restarting opencode server for directory: ${projectDirectory}`,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
const result = await restartOpencodeServer(projectDirectory)
|
|
143
|
+
|
|
144
|
+
if (result instanceof Error) {
|
|
145
|
+
logger.error('[RESTART] Failed:', result)
|
|
146
|
+
await command.editReply({
|
|
147
|
+
content: `Failed to restart opencode server: ${result.message}`,
|
|
148
|
+
})
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const abortMsg =
|
|
153
|
+
abortedCount > 0
|
|
154
|
+
? ` (aborted ${abortedCount} active session${abortedCount > 1 ? 's' : ''})`
|
|
155
|
+
: ''
|
|
156
|
+
await command.editReply({
|
|
157
|
+
content: `Opencode server **restarted** successfully${abortMsg}`,
|
|
158
|
+
})
|
|
159
|
+
logger.log(
|
|
160
|
+
`[RESTART] Opencode server restarted for directory: ${projectDirectory}`,
|
|
161
|
+
)
|
|
162
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// /resume command - Resume an existing OpenCode session.
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ChannelType,
|
|
5
|
+
ThreadAutoArchiveDuration,
|
|
6
|
+
type TextChannel,
|
|
7
|
+
type ThreadChannel,
|
|
8
|
+
} from 'discord.js'
|
|
9
|
+
import fs from 'node:fs'
|
|
10
|
+
import type { CommandContext, AutocompleteContext } from './types.js'
|
|
11
|
+
import {
|
|
12
|
+
getChannelDirectory,
|
|
13
|
+
setThreadSession,
|
|
14
|
+
setPartMessagesBatch,
|
|
15
|
+
getAllThreadSessionIds,
|
|
16
|
+
} from '../database.js'
|
|
17
|
+
import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
18
|
+
import { sendThreadMessage, resolveTextChannel } from '../discord-utils.js'
|
|
19
|
+
import { collectLastAssistantParts } from '../message-formatting.js'
|
|
20
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
21
|
+
import * as errore from 'errore'
|
|
22
|
+
|
|
23
|
+
const logger = createLogger(LogPrefix.RESUME)
|
|
24
|
+
|
|
25
|
+
export async function handleResumeCommand({
|
|
26
|
+
command,
|
|
27
|
+
appId,
|
|
28
|
+
}: CommandContext): Promise<void> {
|
|
29
|
+
await command.deferReply({ ephemeral: false })
|
|
30
|
+
|
|
31
|
+
const sessionId = command.options.getString('session', true)
|
|
32
|
+
const channel = command.channel
|
|
33
|
+
|
|
34
|
+
const isThread =
|
|
35
|
+
channel &&
|
|
36
|
+
[
|
|
37
|
+
ChannelType.PublicThread,
|
|
38
|
+
ChannelType.PrivateThread,
|
|
39
|
+
ChannelType.AnnouncementThread,
|
|
40
|
+
].includes(channel.type)
|
|
41
|
+
|
|
42
|
+
if (isThread) {
|
|
43
|
+
await command.editReply(
|
|
44
|
+
'This command can only be used in project channels, not threads',
|
|
45
|
+
)
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!channel || channel.type !== ChannelType.GuildText) {
|
|
50
|
+
await command.editReply('This command can only be used in text channels')
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const textChannel = channel as TextChannel
|
|
55
|
+
|
|
56
|
+
const channelConfig = await getChannelDirectory(textChannel.id)
|
|
57
|
+
const projectDirectory = channelConfig?.directory
|
|
58
|
+
const channelAppId = channelConfig?.appId || undefined
|
|
59
|
+
|
|
60
|
+
if (channelAppId && channelAppId !== appId) {
|
|
61
|
+
await command.editReply('This channel is not configured for this bot')
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!projectDirectory) {
|
|
66
|
+
await command.editReply(
|
|
67
|
+
'This channel is not configured with a project directory',
|
|
68
|
+
)
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!fs.existsSync(projectDirectory)) {
|
|
73
|
+
await command.editReply(`Directory does not exist: ${projectDirectory}`)
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const getClient = await initializeOpencodeForDirectory(projectDirectory)
|
|
79
|
+
if (getClient instanceof Error) {
|
|
80
|
+
await command.editReply(getClient.message)
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const sessionResponse = await getClient().session.get({
|
|
85
|
+
sessionID: sessionId,
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
if (!sessionResponse.data) {
|
|
89
|
+
await command.editReply('Session not found')
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const sessionTitle = sessionResponse.data.title
|
|
94
|
+
|
|
95
|
+
const thread = await textChannel.threads.create({
|
|
96
|
+
name: `Resume: ${sessionTitle}`.slice(0, 100),
|
|
97
|
+
autoArchiveDuration: ThreadAutoArchiveDuration.OneDay,
|
|
98
|
+
reason: `Resuming session ${sessionId}`,
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// Add user to thread so it appears in their sidebar
|
|
102
|
+
await thread.members.add(command.user.id)
|
|
103
|
+
|
|
104
|
+
await setThreadSession(thread.id, sessionId)
|
|
105
|
+
|
|
106
|
+
logger.log(`[RESUME] Created thread ${thread.id} for session ${sessionId}`)
|
|
107
|
+
|
|
108
|
+
const messagesResponse = await getClient().session.messages({
|
|
109
|
+
sessionID: sessionId,
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
if (!messagesResponse.data) {
|
|
113
|
+
throw new Error('Failed to fetch session messages')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const messages = messagesResponse.data
|
|
117
|
+
|
|
118
|
+
await command.editReply(
|
|
119
|
+
`Resumed session "${sessionTitle}" in ${thread.toString()}`,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
await sendThreadMessage(
|
|
123
|
+
thread,
|
|
124
|
+
`**Resumed session:** ${sessionTitle}\n**Created:** ${new Date(sessionResponse.data.time.created).toLocaleString()}\n\n*Loading ${messages.length} messages...*`,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const { partIds, content, skippedCount } = collectLastAssistantParts({
|
|
129
|
+
messages,
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
if (skippedCount > 0) {
|
|
133
|
+
await sendThreadMessage(
|
|
134
|
+
thread,
|
|
135
|
+
`*Skipped ${skippedCount} older assistant parts...*`,
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (content.trim()) {
|
|
140
|
+
const discordMessage = await sendThreadMessage(thread, content)
|
|
141
|
+
|
|
142
|
+
// Store part-message mappings atomically
|
|
143
|
+
await setPartMessagesBatch(
|
|
144
|
+
partIds.map((partId) => ({
|
|
145
|
+
partId,
|
|
146
|
+
messageId: discordMessage.id,
|
|
147
|
+
threadId: thread.id,
|
|
148
|
+
})),
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const messageCount = messages.length
|
|
153
|
+
|
|
154
|
+
await sendThreadMessage(
|
|
155
|
+
thread,
|
|
156
|
+
`**Session resumed!** Loaded ${messageCount} messages.\n\nYou can now continue the conversation by sending messages in this thread.`,
|
|
157
|
+
)
|
|
158
|
+
} catch (sendError) {
|
|
159
|
+
logger.error('[RESUME] Error sending messages to thread:', sendError)
|
|
160
|
+
await sendThreadMessage(
|
|
161
|
+
thread,
|
|
162
|
+
`Failed to load message history, but session is connected. You can still send new messages.`,
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
} catch (error) {
|
|
166
|
+
logger.error('[RESUME] Error:', error)
|
|
167
|
+
await command.editReply(
|
|
168
|
+
`Failed to resume session: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function handleResumeAutocomplete({
|
|
174
|
+
interaction,
|
|
175
|
+
appId,
|
|
176
|
+
}: AutocompleteContext): Promise<void> {
|
|
177
|
+
const focusedValue = interaction.options.getFocused()
|
|
178
|
+
|
|
179
|
+
let projectDirectory: string | undefined
|
|
180
|
+
|
|
181
|
+
if (interaction.channel) {
|
|
182
|
+
const textChannel = await resolveTextChannel(
|
|
183
|
+
interaction.channel as TextChannel | ThreadChannel | null,
|
|
184
|
+
)
|
|
185
|
+
if (textChannel) {
|
|
186
|
+
const channelConfig = await getChannelDirectory(textChannel.id)
|
|
187
|
+
if (channelConfig?.appId && channelConfig.appId !== appId) {
|
|
188
|
+
await interaction.respond([])
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
projectDirectory = channelConfig?.directory
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!projectDirectory) {
|
|
196
|
+
await interaction.respond([])
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
const getClient = await initializeOpencodeForDirectory(projectDirectory)
|
|
202
|
+
if (getClient instanceof Error) {
|
|
203
|
+
await interaction.respond([])
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const sessionsResponse = await getClient().session.list()
|
|
208
|
+
if (!sessionsResponse.data) {
|
|
209
|
+
await interaction.respond([])
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const existingSessionIds = new Set(await getAllThreadSessionIds())
|
|
214
|
+
|
|
215
|
+
const sessions = sessionsResponse.data
|
|
216
|
+
.filter((session) => !existingSessionIds.has(session.id))
|
|
217
|
+
.filter((session) =>
|
|
218
|
+
session.title.toLowerCase().includes(focusedValue.toLowerCase()),
|
|
219
|
+
)
|
|
220
|
+
.slice(0, 25)
|
|
221
|
+
.map((session) => {
|
|
222
|
+
const dateStr = new Date(session.time.updated).toLocaleString()
|
|
223
|
+
const suffix = ` (${dateStr})`
|
|
224
|
+
const maxTitleLength = 100 - suffix.length
|
|
225
|
+
|
|
226
|
+
let title = session.title
|
|
227
|
+
if (title.length > maxTitleLength) {
|
|
228
|
+
title = title.slice(0, Math.max(0, maxTitleLength - 1)) + '…'
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
name: `${title}${suffix}`,
|
|
233
|
+
value: session.id,
|
|
234
|
+
}
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
await interaction.respond(sessions)
|
|
238
|
+
} catch (error) {
|
|
239
|
+
logger.error('[AUTOCOMPLETE] Error fetching sessions:', error)
|
|
240
|
+
await interaction.respond([])
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// /run-shell-command command - Run an arbitrary shell command in the project directory.
|
|
2
|
+
// Resolves the project directory from the channel and executes the command with it as cwd.
|
|
3
|
+
// Also used by the ! prefix shortcut in discord messages (e.g. "!ls -la").
|
|
4
|
+
// Messages starting with ! are intercepted before session handling and routed here.
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
ChannelType,
|
|
8
|
+
MessageFlags,
|
|
9
|
+
type TextChannel,
|
|
10
|
+
type ThreadChannel,
|
|
11
|
+
} from 'discord.js'
|
|
12
|
+
import type { CommandContext } from './types.js'
|
|
13
|
+
import {
|
|
14
|
+
resolveWorkingDirectory,
|
|
15
|
+
SILENT_MESSAGE_FLAGS,
|
|
16
|
+
} from '../discord-utils.js'
|
|
17
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
18
|
+
import { execAsync } from '../worktree-utils.js'
|
|
19
|
+
import { stripAnsi } from '../utils.js'
|
|
20
|
+
|
|
21
|
+
const logger = createLogger(LogPrefix.INTERACTION)
|
|
22
|
+
|
|
23
|
+
const MAX_OUTPUT_CHARS = 1900
|
|
24
|
+
|
|
25
|
+
export async function runShellCommand({
|
|
26
|
+
command,
|
|
27
|
+
directory,
|
|
28
|
+
}: {
|
|
29
|
+
command: string
|
|
30
|
+
directory: string
|
|
31
|
+
}): Promise<string> {
|
|
32
|
+
try {
|
|
33
|
+
const { stdout, stderr } = await execAsync(command, { cwd: directory })
|
|
34
|
+
const output = stripAnsi([stdout, stderr].filter(Boolean).join('\n').trim())
|
|
35
|
+
|
|
36
|
+
const header = `\`${command}\` exited with 0`
|
|
37
|
+
if (!output) {
|
|
38
|
+
return header
|
|
39
|
+
}
|
|
40
|
+
return formatOutput(output, header)
|
|
41
|
+
} catch (error) {
|
|
42
|
+
const execError = error as {
|
|
43
|
+
stdout?: string
|
|
44
|
+
stderr?: string
|
|
45
|
+
message?: string
|
|
46
|
+
code?: number | string
|
|
47
|
+
}
|
|
48
|
+
const output = stripAnsi(
|
|
49
|
+
[execError.stdout, execError.stderr].filter(Boolean).join('\n').trim(),
|
|
50
|
+
)
|
|
51
|
+
const exitCode = execError.code ?? 1
|
|
52
|
+
logger.error(
|
|
53
|
+
`[RUN-COMMAND] Command "${command}" exited with ${exitCode}:`,
|
|
54
|
+
error,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
const header = `\`${command}\` exited with ${exitCode}`
|
|
58
|
+
return formatOutput(output || execError.message || 'Unknown error', header)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function handleRunCommand({
|
|
63
|
+
command,
|
|
64
|
+
}: CommandContext): Promise<void> {
|
|
65
|
+
const channel = command.channel
|
|
66
|
+
|
|
67
|
+
if (!channel) {
|
|
68
|
+
await command.reply({
|
|
69
|
+
content: 'This command can only be used in a channel.',
|
|
70
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
71
|
+
})
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const isThread = [
|
|
76
|
+
ChannelType.PublicThread,
|
|
77
|
+
ChannelType.PrivateThread,
|
|
78
|
+
ChannelType.AnnouncementThread,
|
|
79
|
+
].includes(channel.type)
|
|
80
|
+
|
|
81
|
+
const isTextChannel = channel.type === ChannelType.GuildText
|
|
82
|
+
|
|
83
|
+
if (!isThread && !isTextChannel) {
|
|
84
|
+
await command.reply({
|
|
85
|
+
content: 'This command can only be used in a text channel or thread.',
|
|
86
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
87
|
+
})
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const resolved = await resolveWorkingDirectory({
|
|
92
|
+
channel: channel as TextChannel | ThreadChannel,
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
if (!resolved) {
|
|
96
|
+
await command.reply({
|
|
97
|
+
content: 'Could not determine project directory for this channel.',
|
|
98
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
99
|
+
})
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const input = command.options.getString('command', true)
|
|
104
|
+
|
|
105
|
+
await command.deferReply()
|
|
106
|
+
|
|
107
|
+
const result = await runShellCommand({
|
|
108
|
+
command: input,
|
|
109
|
+
directory: resolved.workingDirectory,
|
|
110
|
+
})
|
|
111
|
+
await command.editReply({ content: result })
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function formatOutput(output: string, header: string): string {
|
|
115
|
+
// Reserve space for header + newline + code block delimiters (```\n...\n```)
|
|
116
|
+
const overhead = header.length + 1 + 3 + 1 + 1 + 3 // header\n```\n...\n```
|
|
117
|
+
const maxContent = MAX_OUTPUT_CHARS - overhead
|
|
118
|
+
const truncated =
|
|
119
|
+
output.length > maxContent
|
|
120
|
+
? output.slice(0, maxContent - 14) + '\n... truncated'
|
|
121
|
+
: output
|
|
122
|
+
return `${header}\n\`\`\`\n${truncated}\n\`\`\``
|
|
123
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// /session-id command - Show current session ID and an opencode attach command.
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ChannelType,
|
|
5
|
+
MessageFlags,
|
|
6
|
+
type TextChannel,
|
|
7
|
+
type ThreadChannel,
|
|
8
|
+
} from 'discord.js'
|
|
9
|
+
import type { CommandContext } from './types.js'
|
|
10
|
+
import { getThreadSession } from '../database.js'
|
|
11
|
+
import {
|
|
12
|
+
resolveWorkingDirectory,
|
|
13
|
+
SILENT_MESSAGE_FLAGS,
|
|
14
|
+
} from '../discord-utils.js'
|
|
15
|
+
import {
|
|
16
|
+
getOpencodeServerPort,
|
|
17
|
+
initializeOpencodeForDirectory,
|
|
18
|
+
} from '../opencode.js'
|
|
19
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
20
|
+
|
|
21
|
+
const logger = createLogger(LogPrefix.SESSION)
|
|
22
|
+
|
|
23
|
+
function shellQuote(value: string): string {
|
|
24
|
+
if (!value) {
|
|
25
|
+
return "''"
|
|
26
|
+
}
|
|
27
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function handleSessionIdCommand({
|
|
31
|
+
command,
|
|
32
|
+
}: CommandContext): Promise<void> {
|
|
33
|
+
const channel = command.channel
|
|
34
|
+
|
|
35
|
+
if (!channel) {
|
|
36
|
+
await command.reply({
|
|
37
|
+
content: 'This command can only be used in a channel',
|
|
38
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
39
|
+
})
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const isThread = [
|
|
44
|
+
ChannelType.PublicThread,
|
|
45
|
+
ChannelType.PrivateThread,
|
|
46
|
+
ChannelType.AnnouncementThread,
|
|
47
|
+
].includes(channel.type)
|
|
48
|
+
|
|
49
|
+
if (!isThread) {
|
|
50
|
+
await command.reply({
|
|
51
|
+
content:
|
|
52
|
+
'This command can only be used in a thread with an active session',
|
|
53
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
54
|
+
})
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const resolved = await resolveWorkingDirectory({
|
|
59
|
+
channel: channel as TextChannel | ThreadChannel,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
if (!resolved) {
|
|
63
|
+
await command.reply({
|
|
64
|
+
content: 'Could not determine project directory for this channel',
|
|
65
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
66
|
+
})
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const { projectDirectory, workingDirectory } = resolved
|
|
71
|
+
const sessionId = await getThreadSession(channel.id)
|
|
72
|
+
|
|
73
|
+
if (!sessionId) {
|
|
74
|
+
await command.reply({
|
|
75
|
+
content: 'No active session in this thread',
|
|
76
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
77
|
+
})
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
await command.deferReply({ flags: SILENT_MESSAGE_FLAGS })
|
|
82
|
+
|
|
83
|
+
let port = getOpencodeServerPort(projectDirectory)
|
|
84
|
+
if (!port) {
|
|
85
|
+
const getClient = await initializeOpencodeForDirectory(projectDirectory)
|
|
86
|
+
if (getClient instanceof Error) {
|
|
87
|
+
await command.editReply({
|
|
88
|
+
content: `Session ID: \`${sessionId}\`\nFailed to resolve OpenCode server port: ${getClient.message}`,
|
|
89
|
+
})
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
port = getOpencodeServerPort(projectDirectory)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!port) {
|
|
96
|
+
await command.editReply({
|
|
97
|
+
content: `Session ID: \`${sessionId}\`\nCould not determine OpenCode server port`,
|
|
98
|
+
})
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const attachUrl = `http://127.0.0.1:${port}`
|
|
103
|
+
const attachCommand = `opencode attach ${attachUrl} --session ${sessionId} --dir ${shellQuote(workingDirectory)}`
|
|
104
|
+
|
|
105
|
+
await command.editReply({
|
|
106
|
+
content: `**Session ID:** \`${sessionId}\`\n**Attach command:**\n\`\`\`bash\n${attachCommand}\n\`\`\``,
|
|
107
|
+
})
|
|
108
|
+
logger.log(`Session ID shown for thread ${channel.id}: ${sessionId}`)
|
|
109
|
+
}
|