@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
package/src/database.ts
ADDED
|
@@ -0,0 +1,1530 @@
|
|
|
1
|
+
// SQLite database manager for persistent bot state using Prisma.
|
|
2
|
+
// Stores thread-session mappings, bot tokens, channel directories,
|
|
3
|
+
// API keys, and model preferences in <dataDir>/discord-sessions.db.
|
|
4
|
+
|
|
5
|
+
import { getPrisma, closePrisma } from './db.js'
|
|
6
|
+
import { getDefaultVerbosity, getDefaultMentionMode } from './config.js'
|
|
7
|
+
import { hydrateBotTokenCache, isAuthModeEnabled } from './bot-token.js'
|
|
8
|
+
import { createLogger, LogPrefix } from './logger.js'
|
|
9
|
+
|
|
10
|
+
const dbLogger = createLogger(LogPrefix.DB)
|
|
11
|
+
|
|
12
|
+
// Re-export Prisma utilities
|
|
13
|
+
export { getPrisma, closePrisma }
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Initialize the database.
|
|
17
|
+
* Returns the Prisma client.
|
|
18
|
+
*/
|
|
19
|
+
export async function initDatabase() {
|
|
20
|
+
const prisma = await getPrisma()
|
|
21
|
+
const botRow = await prisma.bot_tokens.findFirst({
|
|
22
|
+
orderBy: { created_at: 'desc' },
|
|
23
|
+
})
|
|
24
|
+
hydrateBotTokenCache(
|
|
25
|
+
botRow ? { app_id: botRow.app_id, token: botRow.token } : null,
|
|
26
|
+
)
|
|
27
|
+
dbLogger.log('Database initialized')
|
|
28
|
+
return prisma
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Close the database connection.
|
|
33
|
+
*/
|
|
34
|
+
export async function closeDatabase() {
|
|
35
|
+
await closePrisma()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Verbosity levels for controlling output detail
|
|
39
|
+
// - tools-and-text: shows all output including tool executions
|
|
40
|
+
// - text-and-essential-tools: shows text + edits + custom MCP tools, hides read/search/navigation tools
|
|
41
|
+
// - text-only: only shows text responses (⬥ diamond parts)
|
|
42
|
+
export type VerbosityLevel =
|
|
43
|
+
| 'tools-and-text'
|
|
44
|
+
| 'text-and-essential-tools'
|
|
45
|
+
| 'text-only'
|
|
46
|
+
|
|
47
|
+
// Worktree status types
|
|
48
|
+
export type WorktreeStatus = 'pending' | 'ready' | 'error'
|
|
49
|
+
|
|
50
|
+
export type ThreadWorktree = {
|
|
51
|
+
thread_id: string
|
|
52
|
+
worktree_name: string
|
|
53
|
+
worktree_directory: string | null
|
|
54
|
+
project_directory: string
|
|
55
|
+
status: WorktreeStatus
|
|
56
|
+
error_message: string | null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type ScheduledTaskStatus =
|
|
60
|
+
| 'planned'
|
|
61
|
+
| 'running'
|
|
62
|
+
| 'completed'
|
|
63
|
+
| 'cancelled'
|
|
64
|
+
| 'failed'
|
|
65
|
+
export type ScheduledTaskScheduleKind = 'at' | 'cron'
|
|
66
|
+
|
|
67
|
+
export type ScheduledTask = {
|
|
68
|
+
id: number
|
|
69
|
+
status: ScheduledTaskStatus
|
|
70
|
+
schedule_kind: ScheduledTaskScheduleKind
|
|
71
|
+
run_at: Date | null
|
|
72
|
+
cron_expr: string | null
|
|
73
|
+
timezone: string | null
|
|
74
|
+
next_run_at: Date
|
|
75
|
+
running_started_at: Date | null
|
|
76
|
+
last_run_at: Date | null
|
|
77
|
+
last_error: string | null
|
|
78
|
+
attempts: number
|
|
79
|
+
payload_json: string
|
|
80
|
+
prompt_preview: string
|
|
81
|
+
channel_id: string | null
|
|
82
|
+
thread_id: string | null
|
|
83
|
+
session_id: string | null
|
|
84
|
+
project_directory: string | null
|
|
85
|
+
created_at: Date | null
|
|
86
|
+
updated_at: Date | null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export type SessionStartSource = {
|
|
90
|
+
session_id: string
|
|
91
|
+
schedule_kind: ScheduledTaskScheduleKind
|
|
92
|
+
scheduled_task_id: number | null
|
|
93
|
+
created_at: Date | null
|
|
94
|
+
updated_at: Date | null
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function toScheduledTask(row: {
|
|
98
|
+
id: number
|
|
99
|
+
status: string
|
|
100
|
+
schedule_kind: string
|
|
101
|
+
run_at: Date | null
|
|
102
|
+
cron_expr: string | null
|
|
103
|
+
timezone: string | null
|
|
104
|
+
next_run_at: Date
|
|
105
|
+
running_started_at: Date | null
|
|
106
|
+
last_run_at: Date | null
|
|
107
|
+
last_error: string | null
|
|
108
|
+
attempts: number
|
|
109
|
+
payload_json: string
|
|
110
|
+
prompt_preview: string
|
|
111
|
+
channel_id: string | null
|
|
112
|
+
thread_id: string | null
|
|
113
|
+
session_id: string | null
|
|
114
|
+
project_directory: string | null
|
|
115
|
+
created_at: Date | null
|
|
116
|
+
updated_at: Date | null
|
|
117
|
+
}): ScheduledTask {
|
|
118
|
+
return {
|
|
119
|
+
id: row.id,
|
|
120
|
+
status: row.status as ScheduledTaskStatus,
|
|
121
|
+
schedule_kind: row.schedule_kind as ScheduledTaskScheduleKind,
|
|
122
|
+
run_at: row.run_at,
|
|
123
|
+
cron_expr: row.cron_expr,
|
|
124
|
+
timezone: row.timezone,
|
|
125
|
+
next_run_at: row.next_run_at,
|
|
126
|
+
running_started_at: row.running_started_at,
|
|
127
|
+
last_run_at: row.last_run_at,
|
|
128
|
+
last_error: row.last_error,
|
|
129
|
+
attempts: row.attempts,
|
|
130
|
+
payload_json: row.payload_json,
|
|
131
|
+
prompt_preview: row.prompt_preview,
|
|
132
|
+
channel_id: row.channel_id,
|
|
133
|
+
thread_id: row.thread_id,
|
|
134
|
+
session_id: row.session_id,
|
|
135
|
+
project_directory: row.project_directory,
|
|
136
|
+
created_at: row.created_at,
|
|
137
|
+
updated_at: row.updated_at,
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function toSessionStartSource(row: {
|
|
142
|
+
session_id: string
|
|
143
|
+
schedule_kind: string
|
|
144
|
+
scheduled_task_id: number | null
|
|
145
|
+
created_at: Date | null
|
|
146
|
+
updated_at: Date | null
|
|
147
|
+
}): SessionStartSource {
|
|
148
|
+
return {
|
|
149
|
+
session_id: row.session_id,
|
|
150
|
+
schedule_kind: row.schedule_kind as ScheduledTaskScheduleKind,
|
|
151
|
+
scheduled_task_id: row.scheduled_task_id,
|
|
152
|
+
created_at: row.created_at,
|
|
153
|
+
updated_at: row.updated_at,
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ============================================================================
|
|
158
|
+
// Scheduled Task Functions
|
|
159
|
+
// ============================================================================
|
|
160
|
+
|
|
161
|
+
export async function createScheduledTask({
|
|
162
|
+
scheduleKind,
|
|
163
|
+
runAt,
|
|
164
|
+
cronExpr,
|
|
165
|
+
timezone,
|
|
166
|
+
nextRunAt,
|
|
167
|
+
payloadJson,
|
|
168
|
+
promptPreview,
|
|
169
|
+
channelId,
|
|
170
|
+
threadId,
|
|
171
|
+
sessionId,
|
|
172
|
+
projectDirectory,
|
|
173
|
+
}: {
|
|
174
|
+
scheduleKind: ScheduledTaskScheduleKind
|
|
175
|
+
runAt?: Date | null
|
|
176
|
+
cronExpr?: string | null
|
|
177
|
+
timezone?: string | null
|
|
178
|
+
nextRunAt: Date
|
|
179
|
+
payloadJson: string
|
|
180
|
+
promptPreview: string
|
|
181
|
+
channelId?: string | null
|
|
182
|
+
threadId?: string | null
|
|
183
|
+
sessionId?: string | null
|
|
184
|
+
projectDirectory?: string | null
|
|
185
|
+
}): Promise<number> {
|
|
186
|
+
const prisma = await getPrisma()
|
|
187
|
+
const row = await prisma.scheduled_tasks.create({
|
|
188
|
+
data: {
|
|
189
|
+
status: 'planned',
|
|
190
|
+
schedule_kind: scheduleKind,
|
|
191
|
+
run_at: runAt ?? null,
|
|
192
|
+
cron_expr: cronExpr ?? null,
|
|
193
|
+
timezone: timezone ?? null,
|
|
194
|
+
next_run_at: nextRunAt,
|
|
195
|
+
payload_json: payloadJson,
|
|
196
|
+
prompt_preview: promptPreview,
|
|
197
|
+
channel_id: channelId ?? null,
|
|
198
|
+
thread_id: threadId ?? null,
|
|
199
|
+
session_id: sessionId ?? null,
|
|
200
|
+
project_directory: projectDirectory ?? null,
|
|
201
|
+
},
|
|
202
|
+
select: { id: true },
|
|
203
|
+
})
|
|
204
|
+
return row.id
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export async function listScheduledTasks({
|
|
208
|
+
statuses,
|
|
209
|
+
}: {
|
|
210
|
+
statuses?: ScheduledTaskStatus[]
|
|
211
|
+
} = {}): Promise<ScheduledTask[]> {
|
|
212
|
+
const prisma = await getPrisma()
|
|
213
|
+
const rows = await prisma.scheduled_tasks.findMany({
|
|
214
|
+
where:
|
|
215
|
+
statuses && statuses.length > 0
|
|
216
|
+
? { status: { in: statuses } }
|
|
217
|
+
: undefined,
|
|
218
|
+
orderBy: [{ next_run_at: 'asc' }, { id: 'asc' }],
|
|
219
|
+
})
|
|
220
|
+
return rows.map((row) => toScheduledTask(row))
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export async function cancelScheduledTask(taskId: number): Promise<boolean> {
|
|
224
|
+
const prisma = await getPrisma()
|
|
225
|
+
const result = await prisma.scheduled_tasks.updateMany({
|
|
226
|
+
where: {
|
|
227
|
+
id: taskId,
|
|
228
|
+
status: {
|
|
229
|
+
in: ['planned', 'running'],
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
data: {
|
|
233
|
+
status: 'cancelled',
|
|
234
|
+
running_started_at: null,
|
|
235
|
+
},
|
|
236
|
+
})
|
|
237
|
+
return result.count > 0
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export async function getDuePlannedScheduledTasks({
|
|
241
|
+
now,
|
|
242
|
+
limit,
|
|
243
|
+
}: {
|
|
244
|
+
now: Date
|
|
245
|
+
limit: number
|
|
246
|
+
}): Promise<ScheduledTask[]> {
|
|
247
|
+
const prisma = await getPrisma()
|
|
248
|
+
const rows = await prisma.scheduled_tasks.findMany({
|
|
249
|
+
where: {
|
|
250
|
+
status: 'planned',
|
|
251
|
+
next_run_at: {
|
|
252
|
+
lte: now,
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
orderBy: [{ next_run_at: 'asc' }, { id: 'asc' }],
|
|
256
|
+
take: limit,
|
|
257
|
+
})
|
|
258
|
+
return rows.map((row) => toScheduledTask(row))
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export async function claimScheduledTaskRunning({
|
|
262
|
+
taskId,
|
|
263
|
+
startedAt,
|
|
264
|
+
}: {
|
|
265
|
+
taskId: number
|
|
266
|
+
startedAt: Date
|
|
267
|
+
}): Promise<boolean> {
|
|
268
|
+
const prisma = await getPrisma()
|
|
269
|
+
const result = await prisma.scheduled_tasks.updateMany({
|
|
270
|
+
where: {
|
|
271
|
+
id: taskId,
|
|
272
|
+
status: 'planned',
|
|
273
|
+
},
|
|
274
|
+
data: {
|
|
275
|
+
status: 'running',
|
|
276
|
+
running_started_at: startedAt,
|
|
277
|
+
},
|
|
278
|
+
})
|
|
279
|
+
return result.count > 0
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export async function recoverStaleRunningScheduledTasks({
|
|
283
|
+
staleBefore,
|
|
284
|
+
}: {
|
|
285
|
+
staleBefore: Date
|
|
286
|
+
}): Promise<number> {
|
|
287
|
+
const prisma = await getPrisma()
|
|
288
|
+
const result = await prisma.scheduled_tasks.updateMany({
|
|
289
|
+
where: {
|
|
290
|
+
status: 'running',
|
|
291
|
+
running_started_at: {
|
|
292
|
+
lte: staleBefore,
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
data: {
|
|
296
|
+
status: 'planned',
|
|
297
|
+
running_started_at: null,
|
|
298
|
+
},
|
|
299
|
+
})
|
|
300
|
+
return result.count
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export async function markScheduledTaskOneShotCompleted({
|
|
304
|
+
taskId,
|
|
305
|
+
completedAt,
|
|
306
|
+
}: {
|
|
307
|
+
taskId: number
|
|
308
|
+
completedAt: Date
|
|
309
|
+
}): Promise<void> {
|
|
310
|
+
const prisma = await getPrisma()
|
|
311
|
+
await prisma.scheduled_tasks.update({
|
|
312
|
+
where: { id: taskId },
|
|
313
|
+
data: {
|
|
314
|
+
status: 'completed',
|
|
315
|
+
last_run_at: completedAt,
|
|
316
|
+
running_started_at: null,
|
|
317
|
+
last_error: null,
|
|
318
|
+
},
|
|
319
|
+
})
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export async function markScheduledTaskCronRescheduled({
|
|
323
|
+
taskId,
|
|
324
|
+
completedAt,
|
|
325
|
+
nextRunAt,
|
|
326
|
+
}: {
|
|
327
|
+
taskId: number
|
|
328
|
+
completedAt: Date
|
|
329
|
+
nextRunAt: Date
|
|
330
|
+
}): Promise<void> {
|
|
331
|
+
const prisma = await getPrisma()
|
|
332
|
+
await prisma.scheduled_tasks.update({
|
|
333
|
+
where: { id: taskId },
|
|
334
|
+
data: {
|
|
335
|
+
status: 'planned',
|
|
336
|
+
last_run_at: completedAt,
|
|
337
|
+
running_started_at: null,
|
|
338
|
+
last_error: null,
|
|
339
|
+
next_run_at: nextRunAt,
|
|
340
|
+
},
|
|
341
|
+
})
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export async function markScheduledTaskFailed({
|
|
345
|
+
taskId,
|
|
346
|
+
failedAt,
|
|
347
|
+
errorMessage,
|
|
348
|
+
}: {
|
|
349
|
+
taskId: number
|
|
350
|
+
failedAt: Date
|
|
351
|
+
errorMessage: string
|
|
352
|
+
}): Promise<void> {
|
|
353
|
+
const prisma = await getPrisma()
|
|
354
|
+
await prisma.scheduled_tasks.update({
|
|
355
|
+
where: { id: taskId },
|
|
356
|
+
data: {
|
|
357
|
+
status: 'failed',
|
|
358
|
+
last_run_at: failedAt,
|
|
359
|
+
running_started_at: null,
|
|
360
|
+
last_error: errorMessage,
|
|
361
|
+
attempts: {
|
|
362
|
+
increment: 1,
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
})
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export async function markScheduledTaskCronRetry({
|
|
369
|
+
taskId,
|
|
370
|
+
failedAt,
|
|
371
|
+
errorMessage,
|
|
372
|
+
nextRunAt,
|
|
373
|
+
}: {
|
|
374
|
+
taskId: number
|
|
375
|
+
failedAt: Date
|
|
376
|
+
errorMessage: string
|
|
377
|
+
nextRunAt: Date
|
|
378
|
+
}): Promise<void> {
|
|
379
|
+
const prisma = await getPrisma()
|
|
380
|
+
await prisma.scheduled_tasks.update({
|
|
381
|
+
where: { id: taskId },
|
|
382
|
+
data: {
|
|
383
|
+
status: 'planned',
|
|
384
|
+
next_run_at: nextRunAt,
|
|
385
|
+
last_run_at: failedAt,
|
|
386
|
+
running_started_at: null,
|
|
387
|
+
last_error: errorMessage,
|
|
388
|
+
attempts: {
|
|
389
|
+
increment: 1,
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
})
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export async function setSessionStartSource({
|
|
396
|
+
sessionId,
|
|
397
|
+
scheduleKind,
|
|
398
|
+
scheduledTaskId,
|
|
399
|
+
}: {
|
|
400
|
+
sessionId: string
|
|
401
|
+
scheduleKind: ScheduledTaskScheduleKind
|
|
402
|
+
scheduledTaskId?: number
|
|
403
|
+
}): Promise<void> {
|
|
404
|
+
const prisma = await getPrisma()
|
|
405
|
+
await prisma.session_start_sources.upsert({
|
|
406
|
+
where: { session_id: sessionId },
|
|
407
|
+
create: {
|
|
408
|
+
session_id: sessionId,
|
|
409
|
+
schedule_kind: scheduleKind,
|
|
410
|
+
scheduled_task_id: scheduledTaskId ?? null,
|
|
411
|
+
},
|
|
412
|
+
update: {
|
|
413
|
+
schedule_kind: scheduleKind,
|
|
414
|
+
scheduled_task_id: scheduledTaskId ?? null,
|
|
415
|
+
},
|
|
416
|
+
})
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export async function getSessionStartSourcesBySessionIds(
|
|
420
|
+
sessionIds: string[],
|
|
421
|
+
): Promise<Map<string, SessionStartSource>> {
|
|
422
|
+
if (sessionIds.length === 0) {
|
|
423
|
+
return new Map<string, SessionStartSource>()
|
|
424
|
+
}
|
|
425
|
+
const prisma = await getPrisma()
|
|
426
|
+
const chunkSize = 500
|
|
427
|
+
const chunks: string[][] = []
|
|
428
|
+
for (let index = 0; index < sessionIds.length; index += chunkSize) {
|
|
429
|
+
chunks.push(sessionIds.slice(index, index + chunkSize))
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const rowGroups = await Promise.all(
|
|
433
|
+
chunks.map((chunkSessionIds) => {
|
|
434
|
+
return prisma.session_start_sources.findMany({
|
|
435
|
+
where: {
|
|
436
|
+
session_id: {
|
|
437
|
+
in: chunkSessionIds,
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
})
|
|
441
|
+
}),
|
|
442
|
+
)
|
|
443
|
+
const rows = rowGroups.flatMap((group) => group)
|
|
444
|
+
return new Map(rows.map((row) => [row.session_id, toSessionStartSource(row)]))
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// ============================================================================
|
|
448
|
+
// Channel Model Functions
|
|
449
|
+
// ============================================================================
|
|
450
|
+
|
|
451
|
+
export type ModelPreference = { modelId: string; variant: string | null }
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Get the model preference for a channel.
|
|
455
|
+
* @returns Model ID in format "provider_id/model_id" + optional variant, or undefined
|
|
456
|
+
*/
|
|
457
|
+
export async function getChannelModel(
|
|
458
|
+
channelId: string,
|
|
459
|
+
): Promise<ModelPreference | undefined> {
|
|
460
|
+
const prisma = await getPrisma()
|
|
461
|
+
const row = await prisma.channel_models.findUnique({
|
|
462
|
+
where: { channel_id: channelId },
|
|
463
|
+
})
|
|
464
|
+
if (!row) {
|
|
465
|
+
return undefined
|
|
466
|
+
}
|
|
467
|
+
return { modelId: row.model_id, variant: row.variant }
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Set the model preference for a channel.
|
|
472
|
+
* @param modelId Model ID in format "provider_id/model_id"
|
|
473
|
+
* @param variant Optional thinking/reasoning variant name
|
|
474
|
+
*/
|
|
475
|
+
export async function setChannelModel({
|
|
476
|
+
channelId,
|
|
477
|
+
modelId,
|
|
478
|
+
variant,
|
|
479
|
+
}: {
|
|
480
|
+
channelId: string
|
|
481
|
+
modelId: string
|
|
482
|
+
variant?: string | null
|
|
483
|
+
}): Promise<void> {
|
|
484
|
+
const prisma = await getPrisma()
|
|
485
|
+
await prisma.channel_models.upsert({
|
|
486
|
+
where: { channel_id: channelId },
|
|
487
|
+
create: {
|
|
488
|
+
channel_id: channelId,
|
|
489
|
+
model_id: modelId,
|
|
490
|
+
variant: variant ?? null,
|
|
491
|
+
},
|
|
492
|
+
update: {
|
|
493
|
+
model_id: modelId,
|
|
494
|
+
variant: variant ?? null,
|
|
495
|
+
updated_at: new Date(),
|
|
496
|
+
},
|
|
497
|
+
})
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// ============================================================================
|
|
501
|
+
// Global Model Functions
|
|
502
|
+
// ============================================================================
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Get the global default model for a bot.
|
|
506
|
+
* @returns Model ID in format "provider_id/model_id" + optional variant, or undefined
|
|
507
|
+
*/
|
|
508
|
+
export async function getGlobalModel(
|
|
509
|
+
appId: string,
|
|
510
|
+
): Promise<ModelPreference | undefined> {
|
|
511
|
+
const prisma = await getPrisma()
|
|
512
|
+
const row = await prisma.global_models.findUnique({
|
|
513
|
+
where: { app_id: appId },
|
|
514
|
+
})
|
|
515
|
+
if (!row) {
|
|
516
|
+
return undefined
|
|
517
|
+
}
|
|
518
|
+
return { modelId: row.model_id, variant: row.variant }
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Set the global default model for a bot.
|
|
523
|
+
* @param modelId Model ID in format "provider_id/model_id"
|
|
524
|
+
* @param variant Optional thinking/reasoning variant name
|
|
525
|
+
*/
|
|
526
|
+
export async function setGlobalModel({
|
|
527
|
+
appId,
|
|
528
|
+
modelId,
|
|
529
|
+
variant,
|
|
530
|
+
}: {
|
|
531
|
+
appId: string
|
|
532
|
+
modelId: string
|
|
533
|
+
variant?: string | null
|
|
534
|
+
}): Promise<void> {
|
|
535
|
+
const prisma = await getPrisma()
|
|
536
|
+
await prisma.global_models.upsert({
|
|
537
|
+
where: { app_id: appId },
|
|
538
|
+
create: { app_id: appId, model_id: modelId, variant: variant ?? null },
|
|
539
|
+
update: {
|
|
540
|
+
model_id: modelId,
|
|
541
|
+
variant: variant ?? null,
|
|
542
|
+
updated_at: new Date(),
|
|
543
|
+
},
|
|
544
|
+
})
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// ============================================================================
|
|
548
|
+
// Session Model Functions
|
|
549
|
+
// ============================================================================
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Get the model preference for a session.
|
|
553
|
+
* @returns Model ID in format "provider_id/model_id" + optional variant, or undefined
|
|
554
|
+
*/
|
|
555
|
+
export async function getSessionModel(
|
|
556
|
+
sessionId: string,
|
|
557
|
+
): Promise<ModelPreference | undefined> {
|
|
558
|
+
const prisma = await getPrisma()
|
|
559
|
+
const row = await prisma.session_models.findUnique({
|
|
560
|
+
where: { session_id: sessionId },
|
|
561
|
+
})
|
|
562
|
+
if (!row) {
|
|
563
|
+
return undefined
|
|
564
|
+
}
|
|
565
|
+
return { modelId: row.model_id, variant: row.variant }
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Set the model preference for a session.
|
|
570
|
+
* @param modelId Model ID in format "provider_id/model_id"
|
|
571
|
+
* @param variant Optional thinking/reasoning variant name
|
|
572
|
+
*/
|
|
573
|
+
export async function setSessionModel({
|
|
574
|
+
sessionId,
|
|
575
|
+
modelId,
|
|
576
|
+
variant,
|
|
577
|
+
}: {
|
|
578
|
+
sessionId: string
|
|
579
|
+
modelId: string
|
|
580
|
+
variant?: string | null
|
|
581
|
+
}): Promise<void> {
|
|
582
|
+
const prisma = await getPrisma()
|
|
583
|
+
await prisma.session_models.upsert({
|
|
584
|
+
where: { session_id: sessionId },
|
|
585
|
+
create: {
|
|
586
|
+
session_id: sessionId,
|
|
587
|
+
model_id: modelId,
|
|
588
|
+
variant: variant ?? null,
|
|
589
|
+
},
|
|
590
|
+
update: { model_id: modelId, variant: variant ?? null },
|
|
591
|
+
})
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Clear the model preference for a session.
|
|
596
|
+
* Used when switching agents so the agent's model takes effect.
|
|
597
|
+
*/
|
|
598
|
+
export async function clearSessionModel(sessionId: string): Promise<void> {
|
|
599
|
+
const prisma = await getPrisma()
|
|
600
|
+
await prisma.session_models.deleteMany({
|
|
601
|
+
where: { session_id: sessionId },
|
|
602
|
+
})
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// ============================================================================
|
|
606
|
+
// Variant Cascade Resolution
|
|
607
|
+
// ============================================================================
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Resolve the variant (thinking level) using the session → channel → global cascade.
|
|
611
|
+
* Returns the first non-null variant found, or undefined if none set at any level.
|
|
612
|
+
*/
|
|
613
|
+
export async function getVariantCascade({
|
|
614
|
+
sessionId,
|
|
615
|
+
channelId,
|
|
616
|
+
appId,
|
|
617
|
+
}: {
|
|
618
|
+
sessionId?: string
|
|
619
|
+
channelId?: string
|
|
620
|
+
appId?: string
|
|
621
|
+
}): Promise<string | undefined> {
|
|
622
|
+
if (sessionId) {
|
|
623
|
+
const session = await getSessionModel(sessionId)
|
|
624
|
+
if (session?.variant) {
|
|
625
|
+
return session.variant
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
if (channelId) {
|
|
629
|
+
const channel = await getChannelModel(channelId)
|
|
630
|
+
if (channel?.variant) {
|
|
631
|
+
return channel.variant
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
if (appId) {
|
|
635
|
+
const global = await getGlobalModel(appId)
|
|
636
|
+
if (global?.variant) {
|
|
637
|
+
return global.variant
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return undefined
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// ============================================================================
|
|
644
|
+
// Channel Agent Functions
|
|
645
|
+
// ============================================================================
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Get the agent preference for a channel.
|
|
649
|
+
*/
|
|
650
|
+
export async function getChannelAgent(
|
|
651
|
+
channelId: string,
|
|
652
|
+
): Promise<string | undefined> {
|
|
653
|
+
const prisma = await getPrisma()
|
|
654
|
+
const row = await prisma.channel_agents.findUnique({
|
|
655
|
+
where: { channel_id: channelId },
|
|
656
|
+
})
|
|
657
|
+
return row?.agent_name
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Set the agent preference for a channel.
|
|
662
|
+
*/
|
|
663
|
+
export async function setChannelAgent(
|
|
664
|
+
channelId: string,
|
|
665
|
+
agentName: string,
|
|
666
|
+
): Promise<void> {
|
|
667
|
+
const prisma = await getPrisma()
|
|
668
|
+
await prisma.channel_agents.upsert({
|
|
669
|
+
where: { channel_id: channelId },
|
|
670
|
+
create: { channel_id: channelId, agent_name: agentName },
|
|
671
|
+
update: { agent_name: agentName, updated_at: new Date() },
|
|
672
|
+
})
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// ============================================================================
|
|
676
|
+
// Session Agent Functions
|
|
677
|
+
// ============================================================================
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Get the agent preference for a session.
|
|
681
|
+
*/
|
|
682
|
+
export async function getSessionAgent(
|
|
683
|
+
sessionId: string,
|
|
684
|
+
): Promise<string | undefined> {
|
|
685
|
+
const prisma = await getPrisma()
|
|
686
|
+
const row = await prisma.session_agents.findUnique({
|
|
687
|
+
where: { session_id: sessionId },
|
|
688
|
+
})
|
|
689
|
+
return row?.agent_name
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Set the agent preference for a session.
|
|
694
|
+
*/
|
|
695
|
+
export async function setSessionAgent(
|
|
696
|
+
sessionId: string,
|
|
697
|
+
agentName: string,
|
|
698
|
+
): Promise<void> {
|
|
699
|
+
const prisma = await getPrisma()
|
|
700
|
+
await prisma.session_agents.upsert({
|
|
701
|
+
where: { session_id: sessionId },
|
|
702
|
+
create: { session_id: sessionId, agent_name: agentName },
|
|
703
|
+
update: { agent_name: agentName },
|
|
704
|
+
})
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// ============================================================================
|
|
708
|
+
// Thread Worktree Functions
|
|
709
|
+
// ============================================================================
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Get the worktree info for a thread.
|
|
713
|
+
*/
|
|
714
|
+
export async function getThreadWorktree(
|
|
715
|
+
threadId: string,
|
|
716
|
+
): Promise<ThreadWorktree | undefined> {
|
|
717
|
+
const prisma = await getPrisma()
|
|
718
|
+
const row = await prisma.thread_worktrees.findUnique({
|
|
719
|
+
where: { thread_id: threadId },
|
|
720
|
+
})
|
|
721
|
+
if (!row) {
|
|
722
|
+
return undefined
|
|
723
|
+
}
|
|
724
|
+
return {
|
|
725
|
+
thread_id: row.thread_id,
|
|
726
|
+
worktree_name: row.worktree_name,
|
|
727
|
+
worktree_directory: row.worktree_directory,
|
|
728
|
+
project_directory: row.project_directory,
|
|
729
|
+
status: row.status as WorktreeStatus,
|
|
730
|
+
error_message: row.error_message,
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Create a pending worktree entry for a thread.
|
|
736
|
+
* Ensures the parent thread_sessions row exists first (with empty session_id)
|
|
737
|
+
* to satisfy the FK constraint. The real session_id is set later by setThreadSession().
|
|
738
|
+
*/
|
|
739
|
+
export async function createPendingWorktree({
|
|
740
|
+
threadId,
|
|
741
|
+
worktreeName,
|
|
742
|
+
projectDirectory,
|
|
743
|
+
}: {
|
|
744
|
+
threadId: string
|
|
745
|
+
worktreeName: string
|
|
746
|
+
projectDirectory: string
|
|
747
|
+
}): Promise<void> {
|
|
748
|
+
const prisma = await getPrisma()
|
|
749
|
+
await prisma.$transaction([
|
|
750
|
+
prisma.thread_sessions.upsert({
|
|
751
|
+
where: { thread_id: threadId },
|
|
752
|
+
create: { thread_id: threadId, session_id: '' },
|
|
753
|
+
update: {},
|
|
754
|
+
}),
|
|
755
|
+
prisma.thread_worktrees.upsert({
|
|
756
|
+
where: { thread_id: threadId },
|
|
757
|
+
create: {
|
|
758
|
+
thread_id: threadId,
|
|
759
|
+
worktree_name: worktreeName,
|
|
760
|
+
project_directory: projectDirectory,
|
|
761
|
+
status: 'pending',
|
|
762
|
+
},
|
|
763
|
+
update: {
|
|
764
|
+
worktree_name: worktreeName,
|
|
765
|
+
project_directory: projectDirectory,
|
|
766
|
+
status: 'pending',
|
|
767
|
+
worktree_directory: null,
|
|
768
|
+
error_message: null,
|
|
769
|
+
},
|
|
770
|
+
}),
|
|
771
|
+
])
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Mark a worktree as ready with its directory.
|
|
776
|
+
*/
|
|
777
|
+
export async function setWorktreeReady({
|
|
778
|
+
threadId,
|
|
779
|
+
worktreeDirectory,
|
|
780
|
+
}: {
|
|
781
|
+
threadId: string
|
|
782
|
+
worktreeDirectory: string
|
|
783
|
+
}): Promise<void> {
|
|
784
|
+
const prisma = await getPrisma()
|
|
785
|
+
await prisma.thread_worktrees.update({
|
|
786
|
+
where: { thread_id: threadId },
|
|
787
|
+
data: {
|
|
788
|
+
worktree_directory: worktreeDirectory,
|
|
789
|
+
status: 'ready',
|
|
790
|
+
},
|
|
791
|
+
})
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Mark a worktree as failed with error message.
|
|
796
|
+
*/
|
|
797
|
+
export async function setWorktreeError({
|
|
798
|
+
threadId,
|
|
799
|
+
errorMessage,
|
|
800
|
+
}: {
|
|
801
|
+
threadId: string
|
|
802
|
+
errorMessage: string
|
|
803
|
+
}): Promise<void> {
|
|
804
|
+
const prisma = await getPrisma()
|
|
805
|
+
await prisma.thread_worktrees.update({
|
|
806
|
+
where: { thread_id: threadId },
|
|
807
|
+
data: {
|
|
808
|
+
status: 'error',
|
|
809
|
+
error_message: errorMessage,
|
|
810
|
+
},
|
|
811
|
+
})
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Delete the worktree info for a thread.
|
|
816
|
+
*/
|
|
817
|
+
export async function deleteThreadWorktree(threadId: string): Promise<void> {
|
|
818
|
+
const prisma = await getPrisma()
|
|
819
|
+
await prisma.thread_worktrees.deleteMany({
|
|
820
|
+
where: { thread_id: threadId },
|
|
821
|
+
})
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// ============================================================================
|
|
825
|
+
// Channel Verbosity Functions
|
|
826
|
+
// ============================================================================
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Get the verbosity setting for a channel.
|
|
830
|
+
* Falls back to the global default set via --verbosity CLI flag if no per-channel override exists.
|
|
831
|
+
*/
|
|
832
|
+
export async function getChannelVerbosity(
|
|
833
|
+
channelId: string,
|
|
834
|
+
): Promise<VerbosityLevel> {
|
|
835
|
+
const prisma = await getPrisma()
|
|
836
|
+
const row = await prisma.channel_verbosity.findUnique({
|
|
837
|
+
where: { channel_id: channelId },
|
|
838
|
+
})
|
|
839
|
+
if (row?.verbosity) {
|
|
840
|
+
return row.verbosity as VerbosityLevel
|
|
841
|
+
}
|
|
842
|
+
return getDefaultVerbosity()
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Set the verbosity setting for a channel.
|
|
847
|
+
*/
|
|
848
|
+
export async function setChannelVerbosity(
|
|
849
|
+
channelId: string,
|
|
850
|
+
verbosity: VerbosityLevel,
|
|
851
|
+
): Promise<void> {
|
|
852
|
+
const prisma = await getPrisma()
|
|
853
|
+
await prisma.channel_verbosity.upsert({
|
|
854
|
+
where: { channel_id: channelId },
|
|
855
|
+
create: { channel_id: channelId, verbosity },
|
|
856
|
+
update: { verbosity, updated_at: new Date() },
|
|
857
|
+
})
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// ============================================================================
|
|
861
|
+
// Channel Mention Mode Functions
|
|
862
|
+
// ============================================================================
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* Get the mention mode setting for a channel.
|
|
866
|
+
* Falls back to the global default set via --mention-mode CLI flag if no per-channel override exists.
|
|
867
|
+
*/
|
|
868
|
+
export async function getChannelMentionMode(
|
|
869
|
+
channelId: string,
|
|
870
|
+
): Promise<boolean> {
|
|
871
|
+
const prisma = await getPrisma()
|
|
872
|
+
const row = await prisma.channel_mention_mode.findUnique({
|
|
873
|
+
where: { channel_id: channelId },
|
|
874
|
+
})
|
|
875
|
+
if (row) {
|
|
876
|
+
return row.enabled === 1
|
|
877
|
+
}
|
|
878
|
+
return getDefaultMentionMode()
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* Set the mention mode setting for a channel.
|
|
883
|
+
*/
|
|
884
|
+
export async function setChannelMentionMode(
|
|
885
|
+
channelId: string,
|
|
886
|
+
enabled: boolean,
|
|
887
|
+
): Promise<void> {
|
|
888
|
+
const prisma = await getPrisma()
|
|
889
|
+
await prisma.channel_mention_mode.upsert({
|
|
890
|
+
where: { channel_id: channelId },
|
|
891
|
+
create: { channel_id: channelId, enabled: enabled ? 1 : 0 },
|
|
892
|
+
update: { enabled: enabled ? 1 : 0, updated_at: new Date() },
|
|
893
|
+
})
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// ============================================================================
|
|
897
|
+
// Channel Worktree Settings Functions
|
|
898
|
+
// ============================================================================
|
|
899
|
+
|
|
900
|
+
/**
|
|
901
|
+
* Check if automatic worktree creation is enabled for a channel.
|
|
902
|
+
*/
|
|
903
|
+
export async function getChannelWorktreesEnabled(
|
|
904
|
+
channelId: string,
|
|
905
|
+
): Promise<boolean> {
|
|
906
|
+
const prisma = await getPrisma()
|
|
907
|
+
const row = await prisma.channel_worktrees.findUnique({
|
|
908
|
+
where: { channel_id: channelId },
|
|
909
|
+
})
|
|
910
|
+
return row?.enabled === 1
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* Enable or disable automatic worktree creation for a channel.
|
|
915
|
+
*/
|
|
916
|
+
export async function setChannelWorktreesEnabled(
|
|
917
|
+
channelId: string,
|
|
918
|
+
enabled: boolean,
|
|
919
|
+
): Promise<void> {
|
|
920
|
+
const prisma = await getPrisma()
|
|
921
|
+
await prisma.channel_worktrees.upsert({
|
|
922
|
+
where: { channel_id: channelId },
|
|
923
|
+
create: { channel_id: channelId, enabled: enabled ? 1 : 0 },
|
|
924
|
+
update: { enabled: enabled ? 1 : 0, updated_at: new Date() },
|
|
925
|
+
})
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// ============================================================================
|
|
929
|
+
// Channel Directory Functions
|
|
930
|
+
// ============================================================================
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* Get the directory and app_id for a channel from the database.
|
|
934
|
+
* This is the single source of truth for channel-project mappings.
|
|
935
|
+
*/
|
|
936
|
+
export async function getChannelDirectory(channelId: string): Promise<
|
|
937
|
+
| {
|
|
938
|
+
directory: string
|
|
939
|
+
appId: string | null
|
|
940
|
+
}
|
|
941
|
+
| undefined
|
|
942
|
+
> {
|
|
943
|
+
const prisma = await getPrisma()
|
|
944
|
+
const row = await prisma.channel_directories.findUnique({
|
|
945
|
+
where: { channel_id: channelId },
|
|
946
|
+
})
|
|
947
|
+
|
|
948
|
+
if (!row) {
|
|
949
|
+
return undefined
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
return {
|
|
953
|
+
directory: row.directory,
|
|
954
|
+
appId: row.app_id,
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// ============================================================================
|
|
959
|
+
// Thread Session Functions
|
|
960
|
+
// ============================================================================
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* Get the session ID for a thread.
|
|
964
|
+
*/
|
|
965
|
+
export async function getThreadSession(
|
|
966
|
+
threadId: string,
|
|
967
|
+
): Promise<string | undefined> {
|
|
968
|
+
const prisma = await getPrisma()
|
|
969
|
+
const row = await prisma.thread_sessions.findUnique({
|
|
970
|
+
where: { thread_id: threadId },
|
|
971
|
+
})
|
|
972
|
+
return row?.session_id
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* Set the session ID for a thread.
|
|
977
|
+
*/
|
|
978
|
+
export async function setThreadSession(
|
|
979
|
+
threadId: string,
|
|
980
|
+
sessionId: string,
|
|
981
|
+
): Promise<void> {
|
|
982
|
+
const prisma = await getPrisma()
|
|
983
|
+
await prisma.thread_sessions.upsert({
|
|
984
|
+
where: { thread_id: threadId },
|
|
985
|
+
create: { thread_id: threadId, session_id: sessionId },
|
|
986
|
+
update: { session_id: sessionId },
|
|
987
|
+
})
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* Get the thread ID for a session.
|
|
992
|
+
*/
|
|
993
|
+
export async function getThreadIdBySessionId(
|
|
994
|
+
sessionId: string,
|
|
995
|
+
): Promise<string | undefined> {
|
|
996
|
+
const prisma = await getPrisma()
|
|
997
|
+
const row = await prisma.thread_sessions.findFirst({
|
|
998
|
+
where: { session_id: sessionId },
|
|
999
|
+
})
|
|
1000
|
+
return row?.thread_id
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* Get all session IDs that are associated with threads.
|
|
1005
|
+
*/
|
|
1006
|
+
export async function getAllThreadSessionIds(): Promise<string[]> {
|
|
1007
|
+
const prisma = await getPrisma()
|
|
1008
|
+
const rows = await prisma.thread_sessions.findMany({
|
|
1009
|
+
select: { session_id: true },
|
|
1010
|
+
})
|
|
1011
|
+
return rows.map((row) => row.session_id).filter((id) => id !== '')
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// ============================================================================
|
|
1015
|
+
// Part Messages Functions
|
|
1016
|
+
// ============================================================================
|
|
1017
|
+
|
|
1018
|
+
/**
|
|
1019
|
+
* Get all part IDs for a thread.
|
|
1020
|
+
*/
|
|
1021
|
+
export async function getPartMessageIds(threadId: string): Promise<string[]> {
|
|
1022
|
+
const prisma = await getPrisma()
|
|
1023
|
+
const rows = await prisma.part_messages.findMany({
|
|
1024
|
+
where: { thread_id: threadId },
|
|
1025
|
+
select: { part_id: true },
|
|
1026
|
+
})
|
|
1027
|
+
return rows.map((row) => row.part_id)
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
/**
|
|
1031
|
+
* Store a part-message mapping.
|
|
1032
|
+
* Note: The thread must already have a session (via setThreadSession) before calling this.
|
|
1033
|
+
*/
|
|
1034
|
+
export async function setPartMessage(
|
|
1035
|
+
partId: string,
|
|
1036
|
+
messageId: string,
|
|
1037
|
+
threadId: string,
|
|
1038
|
+
): Promise<void> {
|
|
1039
|
+
const prisma = await getPrisma()
|
|
1040
|
+
await prisma.part_messages.upsert({
|
|
1041
|
+
where: { part_id: partId },
|
|
1042
|
+
create: { part_id: partId, message_id: messageId, thread_id: threadId },
|
|
1043
|
+
update: { message_id: messageId, thread_id: threadId },
|
|
1044
|
+
})
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
/**
|
|
1048
|
+
* Store multiple part-message mappings in a transaction.
|
|
1049
|
+
* More efficient and atomic for batch operations.
|
|
1050
|
+
* Note: The thread must already have a session (via setThreadSession) before calling this.
|
|
1051
|
+
*/
|
|
1052
|
+
export async function setPartMessagesBatch(
|
|
1053
|
+
partMappings: Array<{ partId: string; messageId: string; threadId: string }>,
|
|
1054
|
+
): Promise<void> {
|
|
1055
|
+
if (partMappings.length === 0) {
|
|
1056
|
+
return
|
|
1057
|
+
}
|
|
1058
|
+
const prisma = await getPrisma()
|
|
1059
|
+
await prisma.$transaction(
|
|
1060
|
+
partMappings.map(({ partId, messageId, threadId }) => {
|
|
1061
|
+
return prisma.part_messages.upsert({
|
|
1062
|
+
where: { part_id: partId },
|
|
1063
|
+
create: { part_id: partId, message_id: messageId, thread_id: threadId },
|
|
1064
|
+
update: { message_id: messageId, thread_id: threadId },
|
|
1065
|
+
})
|
|
1066
|
+
}),
|
|
1067
|
+
)
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// ============================================================================
|
|
1071
|
+
// Bot Token Functions
|
|
1072
|
+
// ============================================================================
|
|
1073
|
+
|
|
1074
|
+
/**
|
|
1075
|
+
* Store a bot token.
|
|
1076
|
+
*/
|
|
1077
|
+
export async function setBotToken(appId: string, token: string): Promise<void> {
|
|
1078
|
+
if (isAuthModeEnabled()) {
|
|
1079
|
+
return
|
|
1080
|
+
}
|
|
1081
|
+
const prisma = await getPrisma()
|
|
1082
|
+
await prisma.bot_tokens.upsert({
|
|
1083
|
+
where: { app_id: appId },
|
|
1084
|
+
create: { app_id: appId, token },
|
|
1085
|
+
update: { token },
|
|
1086
|
+
})
|
|
1087
|
+
hydrateBotTokenCache({ app_id: appId, token })
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// ============================================================================
|
|
1091
|
+
// Bot API Keys Functions
|
|
1092
|
+
// ============================================================================
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* Get the Gemini API key for a bot.
|
|
1096
|
+
*/
|
|
1097
|
+
export async function getGeminiApiKey(appId: string): Promise<string | null> {
|
|
1098
|
+
const prisma = await getPrisma()
|
|
1099
|
+
const row = await prisma.bot_api_keys.findUnique({
|
|
1100
|
+
where: { app_id: appId },
|
|
1101
|
+
})
|
|
1102
|
+
return row?.gemini_api_key ?? null
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
/**
|
|
1106
|
+
* Set the Gemini API key for a bot.
|
|
1107
|
+
* Note: The bot must already have a token (via setBotToken) before calling this.
|
|
1108
|
+
*/
|
|
1109
|
+
export async function setGeminiApiKey(
|
|
1110
|
+
appId: string,
|
|
1111
|
+
apiKey: string,
|
|
1112
|
+
): Promise<void> {
|
|
1113
|
+
const prisma = await getPrisma()
|
|
1114
|
+
await prisma.bot_api_keys.upsert({
|
|
1115
|
+
where: { app_id: appId },
|
|
1116
|
+
create: { app_id: appId, gemini_api_key: apiKey },
|
|
1117
|
+
update: { gemini_api_key: apiKey },
|
|
1118
|
+
})
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
/**
|
|
1122
|
+
* Get the OpenAI API key for a bot.
|
|
1123
|
+
*/
|
|
1124
|
+
export async function getOpenAIApiKey(appId: string): Promise<string | null> {
|
|
1125
|
+
const prisma = await getPrisma()
|
|
1126
|
+
const row = await prisma.bot_api_keys.findUnique({
|
|
1127
|
+
where: { app_id: appId },
|
|
1128
|
+
})
|
|
1129
|
+
return row?.openai_api_key ?? null
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
/**
|
|
1133
|
+
* Set the OpenAI API key for a bot.
|
|
1134
|
+
*/
|
|
1135
|
+
export async function setOpenAIApiKey(
|
|
1136
|
+
appId: string,
|
|
1137
|
+
apiKey: string,
|
|
1138
|
+
): Promise<void> {
|
|
1139
|
+
const prisma = await getPrisma()
|
|
1140
|
+
await prisma.bot_api_keys.upsert({
|
|
1141
|
+
where: { app_id: appId },
|
|
1142
|
+
create: { app_id: appId, openai_api_key: apiKey },
|
|
1143
|
+
update: { openai_api_key: apiKey },
|
|
1144
|
+
})
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
/**
|
|
1148
|
+
* Get the best available transcription API key for a bot.
|
|
1149
|
+
* Prefers OpenAI, falls back to Gemini.
|
|
1150
|
+
*/
|
|
1151
|
+
export async function getTranscriptionApiKey(
|
|
1152
|
+
appId: string,
|
|
1153
|
+
): Promise<{ provider: 'openai' | 'gemini'; apiKey: string } | null> {
|
|
1154
|
+
const prisma = await getPrisma()
|
|
1155
|
+
const row = await prisma.bot_api_keys.findUnique({
|
|
1156
|
+
where: { app_id: appId },
|
|
1157
|
+
})
|
|
1158
|
+
if (!row) return null
|
|
1159
|
+
if (row.openai_api_key) {
|
|
1160
|
+
return { provider: 'openai', apiKey: row.openai_api_key }
|
|
1161
|
+
}
|
|
1162
|
+
if (row.gemini_api_key) {
|
|
1163
|
+
return { provider: 'gemini', apiKey: row.gemini_api_key }
|
|
1164
|
+
}
|
|
1165
|
+
return null
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// ============================================================================
|
|
1169
|
+
// Channel Directory CRUD Functions
|
|
1170
|
+
// ============================================================================
|
|
1171
|
+
|
|
1172
|
+
/**
|
|
1173
|
+
* Store a channel-directory mapping.
|
|
1174
|
+
* @param skipIfExists If true, behaves like INSERT OR IGNORE - skips if record exists.
|
|
1175
|
+
* If false (default), behaves like INSERT OR REPLACE - updates if exists.
|
|
1176
|
+
*/
|
|
1177
|
+
export async function setChannelDirectory({
|
|
1178
|
+
channelId,
|
|
1179
|
+
directory,
|
|
1180
|
+
channelType,
|
|
1181
|
+
appId,
|
|
1182
|
+
skipIfExists = false,
|
|
1183
|
+
}: {
|
|
1184
|
+
channelId: string
|
|
1185
|
+
directory: string
|
|
1186
|
+
channelType: 'text' | 'voice'
|
|
1187
|
+
appId?: string | null
|
|
1188
|
+
skipIfExists?: boolean
|
|
1189
|
+
}): Promise<void> {
|
|
1190
|
+
const prisma = await getPrisma()
|
|
1191
|
+
if (skipIfExists) {
|
|
1192
|
+
// INSERT OR IGNORE semantics - only insert if not exists
|
|
1193
|
+
const existing = await prisma.channel_directories.findUnique({
|
|
1194
|
+
where: { channel_id: channelId },
|
|
1195
|
+
})
|
|
1196
|
+
if (existing) {
|
|
1197
|
+
return
|
|
1198
|
+
}
|
|
1199
|
+
await prisma.channel_directories.create({
|
|
1200
|
+
data: {
|
|
1201
|
+
channel_id: channelId,
|
|
1202
|
+
directory,
|
|
1203
|
+
channel_type: channelType,
|
|
1204
|
+
app_id: appId ?? null,
|
|
1205
|
+
},
|
|
1206
|
+
})
|
|
1207
|
+
} else {
|
|
1208
|
+
// INSERT OR REPLACE semantics - upsert
|
|
1209
|
+
await prisma.channel_directories.upsert({
|
|
1210
|
+
where: { channel_id: channelId },
|
|
1211
|
+
create: {
|
|
1212
|
+
channel_id: channelId,
|
|
1213
|
+
directory,
|
|
1214
|
+
channel_type: channelType,
|
|
1215
|
+
app_id: appId ?? null,
|
|
1216
|
+
},
|
|
1217
|
+
update: {
|
|
1218
|
+
directory,
|
|
1219
|
+
channel_type: channelType,
|
|
1220
|
+
app_id: appId ?? null,
|
|
1221
|
+
},
|
|
1222
|
+
})
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
/**
|
|
1227
|
+
* Find channels by directory path.
|
|
1228
|
+
*/
|
|
1229
|
+
export async function findChannelsByDirectory({
|
|
1230
|
+
directory,
|
|
1231
|
+
channelType,
|
|
1232
|
+
appId,
|
|
1233
|
+
}: {
|
|
1234
|
+
directory?: string
|
|
1235
|
+
channelType?: 'text' | 'voice'
|
|
1236
|
+
appId?: string
|
|
1237
|
+
}): Promise<
|
|
1238
|
+
Array<{ channel_id: string; directory: string; channel_type: string }>
|
|
1239
|
+
> {
|
|
1240
|
+
const prisma = await getPrisma()
|
|
1241
|
+
const where: {
|
|
1242
|
+
directory?: string
|
|
1243
|
+
channel_type?: string
|
|
1244
|
+
app_id?: string
|
|
1245
|
+
} = {}
|
|
1246
|
+
if (directory) {
|
|
1247
|
+
where.directory = directory
|
|
1248
|
+
}
|
|
1249
|
+
if (channelType) {
|
|
1250
|
+
where.channel_type = channelType
|
|
1251
|
+
}
|
|
1252
|
+
if (appId) {
|
|
1253
|
+
where.app_id = appId
|
|
1254
|
+
}
|
|
1255
|
+
const rows = await prisma.channel_directories.findMany({
|
|
1256
|
+
where,
|
|
1257
|
+
select: { channel_id: true, directory: true, channel_type: true },
|
|
1258
|
+
})
|
|
1259
|
+
return rows
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
/**
|
|
1263
|
+
* Get all distinct directories with text channels.
|
|
1264
|
+
*/
|
|
1265
|
+
export async function getAllTextChannelDirectories(): Promise<string[]> {
|
|
1266
|
+
const prisma = await getPrisma()
|
|
1267
|
+
const rows = await prisma.channel_directories.findMany({
|
|
1268
|
+
where: { channel_type: 'text' },
|
|
1269
|
+
select: { directory: true },
|
|
1270
|
+
distinct: ['directory'],
|
|
1271
|
+
})
|
|
1272
|
+
return rows.map((row) => row.directory)
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
/**
|
|
1276
|
+
* Delete all channel directories for a specific directory.
|
|
1277
|
+
*/
|
|
1278
|
+
export async function deleteChannelDirectoriesByDirectory(
|
|
1279
|
+
directory: string,
|
|
1280
|
+
): Promise<void> {
|
|
1281
|
+
const prisma = await getPrisma()
|
|
1282
|
+
await prisma.channel_directories.deleteMany({
|
|
1283
|
+
where: { directory },
|
|
1284
|
+
})
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
/**
|
|
1288
|
+
* Find a channel by app ID.
|
|
1289
|
+
*/
|
|
1290
|
+
export async function findChannelByAppId(
|
|
1291
|
+
appId: string,
|
|
1292
|
+
): Promise<string | undefined> {
|
|
1293
|
+
const prisma = await getPrisma()
|
|
1294
|
+
const row = await prisma.channel_directories.findFirst({
|
|
1295
|
+
where: { app_id: appId },
|
|
1296
|
+
orderBy: { created_at: 'desc' },
|
|
1297
|
+
select: { channel_id: true },
|
|
1298
|
+
})
|
|
1299
|
+
return row?.channel_id
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
/**
|
|
1303
|
+
* Get the directory for a voice channel.
|
|
1304
|
+
*/
|
|
1305
|
+
export async function getVoiceChannelDirectory(
|
|
1306
|
+
channelId: string,
|
|
1307
|
+
): Promise<string | undefined> {
|
|
1308
|
+
const prisma = await getPrisma()
|
|
1309
|
+
const row = await prisma.channel_directories.findFirst({
|
|
1310
|
+
where: { channel_id: channelId, channel_type: 'voice' },
|
|
1311
|
+
})
|
|
1312
|
+
return row?.directory
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
/**
|
|
1316
|
+
* Find the text channel ID that shares the same directory as a voice channel.
|
|
1317
|
+
* Used to send error messages to text channels from voice handlers.
|
|
1318
|
+
*/
|
|
1319
|
+
export async function findTextChannelByVoiceChannel(
|
|
1320
|
+
voiceChannelId: string,
|
|
1321
|
+
): Promise<string | undefined> {
|
|
1322
|
+
const prisma = await getPrisma()
|
|
1323
|
+
// First get the directory for the voice channel
|
|
1324
|
+
const voiceChannel = await prisma.channel_directories.findFirst({
|
|
1325
|
+
where: { channel_id: voiceChannelId, channel_type: 'voice' },
|
|
1326
|
+
})
|
|
1327
|
+
if (!voiceChannel) {
|
|
1328
|
+
return undefined
|
|
1329
|
+
}
|
|
1330
|
+
// Then find the text channel with the same directory
|
|
1331
|
+
const textChannel = await prisma.channel_directories.findFirst({
|
|
1332
|
+
where: { directory: voiceChannel.directory, channel_type: 'text' },
|
|
1333
|
+
})
|
|
1334
|
+
return textChannel?.channel_id
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
// ============================================================================
|
|
1338
|
+
// Forum Sync Config Functions
|
|
1339
|
+
// ============================================================================
|
|
1340
|
+
|
|
1341
|
+
export type ForumSyncConfigRow = {
|
|
1342
|
+
appId: string
|
|
1343
|
+
forumChannelId: string
|
|
1344
|
+
outputDir: string
|
|
1345
|
+
direction: string
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
export async function getForumSyncConfigs({
|
|
1349
|
+
appId,
|
|
1350
|
+
}: {
|
|
1351
|
+
appId: string
|
|
1352
|
+
}): Promise<ForumSyncConfigRow[]> {
|
|
1353
|
+
const prisma = await getPrisma()
|
|
1354
|
+
const rows = await prisma.forum_sync_configs.findMany({
|
|
1355
|
+
where: { app_id: appId },
|
|
1356
|
+
})
|
|
1357
|
+
return rows.map((row) => ({
|
|
1358
|
+
appId: row.app_id,
|
|
1359
|
+
forumChannelId: row.forum_channel_id,
|
|
1360
|
+
outputDir: row.output_dir,
|
|
1361
|
+
direction: row.direction,
|
|
1362
|
+
}))
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
export async function upsertForumSyncConfig({
|
|
1366
|
+
appId,
|
|
1367
|
+
forumChannelId,
|
|
1368
|
+
outputDir,
|
|
1369
|
+
direction = 'bidirectional',
|
|
1370
|
+
}: {
|
|
1371
|
+
appId: string
|
|
1372
|
+
forumChannelId: string
|
|
1373
|
+
outputDir: string
|
|
1374
|
+
direction?: string
|
|
1375
|
+
}) {
|
|
1376
|
+
const prisma = await getPrisma()
|
|
1377
|
+
await prisma.forum_sync_configs.upsert({
|
|
1378
|
+
where: {
|
|
1379
|
+
app_id_forum_channel_id: {
|
|
1380
|
+
app_id: appId,
|
|
1381
|
+
forum_channel_id: forumChannelId,
|
|
1382
|
+
},
|
|
1383
|
+
},
|
|
1384
|
+
update: { output_dir: outputDir, direction },
|
|
1385
|
+
create: {
|
|
1386
|
+
app_id: appId,
|
|
1387
|
+
forum_channel_id: forumChannelId,
|
|
1388
|
+
output_dir: outputDir,
|
|
1389
|
+
direction,
|
|
1390
|
+
},
|
|
1391
|
+
})
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
export async function deleteForumSyncConfig({
|
|
1395
|
+
appId,
|
|
1396
|
+
forumChannelId,
|
|
1397
|
+
}: {
|
|
1398
|
+
appId: string
|
|
1399
|
+
forumChannelId: string
|
|
1400
|
+
}) {
|
|
1401
|
+
const prisma = await getPrisma()
|
|
1402
|
+
await prisma.forum_sync_configs.deleteMany({
|
|
1403
|
+
where: { app_id: appId, forum_channel_id: forumChannelId },
|
|
1404
|
+
})
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
/** Delete forum sync configs that share the same outputDir but have a different forumChannelId.
|
|
1408
|
+
* This cleans up stale entries left behind when a forum channel is deleted and recreated. */
|
|
1409
|
+
export async function deleteStaleForumSyncConfigs({
|
|
1410
|
+
appId,
|
|
1411
|
+
forumChannelId,
|
|
1412
|
+
outputDir,
|
|
1413
|
+
}: {
|
|
1414
|
+
appId: string
|
|
1415
|
+
forumChannelId: string
|
|
1416
|
+
outputDir: string
|
|
1417
|
+
}) {
|
|
1418
|
+
const prisma = await getPrisma()
|
|
1419
|
+
await prisma.forum_sync_configs.deleteMany({
|
|
1420
|
+
where: {
|
|
1421
|
+
app_id: appId,
|
|
1422
|
+
output_dir: outputDir,
|
|
1423
|
+
NOT: { forum_channel_id: forumChannelId },
|
|
1424
|
+
},
|
|
1425
|
+
})
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1429
|
+
// IPC REQUESTS - plugin <-> bot communication via DB polling
|
|
1430
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1431
|
+
|
|
1432
|
+
export async function createIpcRequest({
|
|
1433
|
+
type,
|
|
1434
|
+
sessionId,
|
|
1435
|
+
threadId,
|
|
1436
|
+
payload,
|
|
1437
|
+
}: {
|
|
1438
|
+
type: import('./generated/client.js').ipc_request_type
|
|
1439
|
+
sessionId: string
|
|
1440
|
+
threadId: string
|
|
1441
|
+
payload: string
|
|
1442
|
+
}) {
|
|
1443
|
+
const prisma = await getPrisma()
|
|
1444
|
+
return prisma.ipc_requests.create({
|
|
1445
|
+
data: {
|
|
1446
|
+
type,
|
|
1447
|
+
session_id: sessionId,
|
|
1448
|
+
thread_id: threadId,
|
|
1449
|
+
payload,
|
|
1450
|
+
},
|
|
1451
|
+
})
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
/**
|
|
1455
|
+
* Atomically claim pending IPC requests by updating status to 'processing'
|
|
1456
|
+
* only for rows that are still 'pending'. Returns the claimed rows.
|
|
1457
|
+
* This prevents duplicate dispatch when poll ticks overlap.
|
|
1458
|
+
*/
|
|
1459
|
+
export async function claimPendingIpcRequests() {
|
|
1460
|
+
const prisma = await getPrisma()
|
|
1461
|
+
const pending = await prisma.ipc_requests.findMany({
|
|
1462
|
+
where: { status: 'pending' },
|
|
1463
|
+
orderBy: { created_at: 'asc' },
|
|
1464
|
+
})
|
|
1465
|
+
if (pending.length === 0) return pending
|
|
1466
|
+
|
|
1467
|
+
// Atomically claim each one (updateMany with status guard)
|
|
1468
|
+
const claimed: typeof pending = []
|
|
1469
|
+
for (const req of pending) {
|
|
1470
|
+
const result = await prisma.ipc_requests.updateMany({
|
|
1471
|
+
where: { id: req.id, status: 'pending' },
|
|
1472
|
+
data: { status: 'processing' },
|
|
1473
|
+
})
|
|
1474
|
+
if (result.count > 0) {
|
|
1475
|
+
claimed.push(req)
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
return claimed
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
export async function completeIpcRequest({
|
|
1482
|
+
id,
|
|
1483
|
+
response,
|
|
1484
|
+
}: {
|
|
1485
|
+
id: string
|
|
1486
|
+
response: string
|
|
1487
|
+
}) {
|
|
1488
|
+
const prisma = await getPrisma()
|
|
1489
|
+
return prisma.ipc_requests.update({
|
|
1490
|
+
where: { id },
|
|
1491
|
+
data: { response, status: 'completed' as const },
|
|
1492
|
+
})
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
export async function getIpcRequestById({ id }: { id: string }) {
|
|
1496
|
+
const prisma = await getPrisma()
|
|
1497
|
+
return prisma.ipc_requests.findUnique({ where: { id } })
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
/** Cancel IPC requests stuck in 'processing' longer than the TTL (e.g. hung file upload). */
|
|
1501
|
+
export async function cancelStaleProcessingRequests({
|
|
1502
|
+
ttlMs,
|
|
1503
|
+
}: {
|
|
1504
|
+
ttlMs: number
|
|
1505
|
+
}) {
|
|
1506
|
+
const prisma = await getPrisma()
|
|
1507
|
+
const cutoff = new Date(Date.now() - ttlMs)
|
|
1508
|
+
return prisma.ipc_requests.updateMany({
|
|
1509
|
+
where: {
|
|
1510
|
+
status: 'processing',
|
|
1511
|
+
updated_at: { lt: cutoff },
|
|
1512
|
+
},
|
|
1513
|
+
data: {
|
|
1514
|
+
status: 'cancelled' as const,
|
|
1515
|
+
response: JSON.stringify({ error: 'Request timed out' }),
|
|
1516
|
+
},
|
|
1517
|
+
})
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
/** Cancel all pending IPC requests (on startup cleanup and shutdown). */
|
|
1521
|
+
export async function cancelAllPendingIpcRequests() {
|
|
1522
|
+
const prisma = await getPrisma()
|
|
1523
|
+
await prisma.ipc_requests.updateMany({
|
|
1524
|
+
where: { status: { in: ['pending', 'processing'] } },
|
|
1525
|
+
data: {
|
|
1526
|
+
status: 'cancelled' as const,
|
|
1527
|
+
response: JSON.stringify({ error: 'Bot shutting down' }),
|
|
1528
|
+
},
|
|
1529
|
+
})
|
|
1530
|
+
}
|