@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,454 @@
|
|
|
1
|
+
// Runtime state management, file watchers, and Discord event listeners.
|
|
2
|
+
// Manages the lifecycle of forum sync: initial sync, live Discord event handling,
|
|
3
|
+
// file system watcher for bidirectional sync, and debounced sync scheduling.
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs'
|
|
6
|
+
import parcelWatcher from '@parcel/watcher'
|
|
7
|
+
import {
|
|
8
|
+
ChannelType,
|
|
9
|
+
Events,
|
|
10
|
+
type Client,
|
|
11
|
+
type Message,
|
|
12
|
+
type PartialMessage,
|
|
13
|
+
type ThreadChannel,
|
|
14
|
+
} from 'discord.js'
|
|
15
|
+
import { createLogger } from '../logger.js'
|
|
16
|
+
import { readForumSyncConfig } from './config.js'
|
|
17
|
+
import {
|
|
18
|
+
ensureDirectory,
|
|
19
|
+
getCanonicalThreadFilePath,
|
|
20
|
+
} from './discord-operations.js'
|
|
21
|
+
import { syncForumToFiles } from './sync-to-files.js'
|
|
22
|
+
import { syncFilesToForum } from './sync-to-discord.js'
|
|
23
|
+
import {
|
|
24
|
+
DEFAULT_DEBOUNCE_MS,
|
|
25
|
+
ForumSyncOperationError,
|
|
26
|
+
addIgnoredPath,
|
|
27
|
+
shouldIgnorePath,
|
|
28
|
+
type ForumRuntimeState,
|
|
29
|
+
type ForumSyncDirection,
|
|
30
|
+
type StartForumSyncOptions,
|
|
31
|
+
} from './types.js'
|
|
32
|
+
|
|
33
|
+
const forumLogger = createLogger('FORUM')
|
|
34
|
+
|
|
35
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
36
|
+
// MODULE STATE
|
|
37
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
38
|
+
|
|
39
|
+
const forumStateById = new Map<string, ForumRuntimeState>()
|
|
40
|
+
const watcherUnsubscribeByForumId = new Map<string, () => Promise<void>>()
|
|
41
|
+
let discordListenersRegistered = false
|
|
42
|
+
|
|
43
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
44
|
+
// RUNTIME STATE
|
|
45
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
46
|
+
|
|
47
|
+
function buildRuntimeState({
|
|
48
|
+
forumChannelId,
|
|
49
|
+
outputDir,
|
|
50
|
+
direction,
|
|
51
|
+
}: {
|
|
52
|
+
forumChannelId: string
|
|
53
|
+
outputDir: string
|
|
54
|
+
direction: ForumSyncDirection
|
|
55
|
+
}): ForumRuntimeState {
|
|
56
|
+
return {
|
|
57
|
+
forumChannelId,
|
|
58
|
+
outputDir,
|
|
59
|
+
direction,
|
|
60
|
+
dirtyThreadIds: new Set<string>(),
|
|
61
|
+
ignoredPaths: new Map<string, number>(),
|
|
62
|
+
queuedFileEvents: new Map<string, 'create' | 'update' | 'delete'>(),
|
|
63
|
+
discordDebounceTimer: null,
|
|
64
|
+
fileDebounceTimer: null,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
69
|
+
// FILE WATCHER EVENT HANDLING
|
|
70
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
71
|
+
|
|
72
|
+
async function runQueuedFileEvents({
|
|
73
|
+
runtimeState,
|
|
74
|
+
discordClient,
|
|
75
|
+
}: {
|
|
76
|
+
runtimeState: ForumRuntimeState
|
|
77
|
+
discordClient: Client
|
|
78
|
+
}) {
|
|
79
|
+
const queuedEntries = Array.from(runtimeState.queuedFileEvents.entries())
|
|
80
|
+
runtimeState.queuedFileEvents.clear()
|
|
81
|
+
|
|
82
|
+
if (queuedEntries.length === 0) return
|
|
83
|
+
|
|
84
|
+
const changedFilePaths = queuedEntries
|
|
85
|
+
.filter(([, eventType]) => eventType === 'create' || eventType === 'update')
|
|
86
|
+
.map(([filePath]) => filePath)
|
|
87
|
+
const deletedFilePaths = queuedEntries
|
|
88
|
+
.filter(([, eventType]) => eventType === 'delete')
|
|
89
|
+
.map(([filePath]) => filePath)
|
|
90
|
+
|
|
91
|
+
const fileSyncResult = await syncFilesToForum({
|
|
92
|
+
discordClient,
|
|
93
|
+
forumChannelId: runtimeState.forumChannelId,
|
|
94
|
+
outputDir: runtimeState.outputDir,
|
|
95
|
+
runtimeState,
|
|
96
|
+
changedFilePaths,
|
|
97
|
+
deletedFilePaths,
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
if (fileSyncResult instanceof Error) {
|
|
101
|
+
forumLogger.warn(
|
|
102
|
+
`FS -> Discord sync failed for ${runtimeState.forumChannelId}: ${fileSyncResult.message}`,
|
|
103
|
+
)
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (
|
|
108
|
+
fileSyncResult.created + fileSyncResult.updated + fileSyncResult.deleted >
|
|
109
|
+
0
|
|
110
|
+
) {
|
|
111
|
+
forumLogger.log(
|
|
112
|
+
`FS -> Discord ${runtimeState.forumChannelId}: +${fileSyncResult.created} ~${fileSyncResult.updated} -${fileSyncResult.deleted} (skip ${fileSyncResult.skipped})`,
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Refresh the FS mirror for any threads that were touched
|
|
117
|
+
const discordSyncResult = await syncForumToFiles({
|
|
118
|
+
discordClient,
|
|
119
|
+
forumChannelId: runtimeState.forumChannelId,
|
|
120
|
+
outputDir: runtimeState.outputDir,
|
|
121
|
+
runtimeState,
|
|
122
|
+
forceThreadIds: runtimeState.dirtyThreadIds,
|
|
123
|
+
})
|
|
124
|
+
if (discordSyncResult instanceof Error) {
|
|
125
|
+
forumLogger.warn(
|
|
126
|
+
`Discord -> FS refresh failed for ${runtimeState.forumChannelId}: ${discordSyncResult.message}`,
|
|
127
|
+
)
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
runtimeState.dirtyThreadIds.clear()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function queueFileEvent({
|
|
134
|
+
runtimeState,
|
|
135
|
+
filePath,
|
|
136
|
+
eventType,
|
|
137
|
+
discordClient,
|
|
138
|
+
}: {
|
|
139
|
+
runtimeState: ForumRuntimeState
|
|
140
|
+
filePath: string
|
|
141
|
+
eventType: 'create' | 'update' | 'delete'
|
|
142
|
+
discordClient: Client
|
|
143
|
+
}) {
|
|
144
|
+
if (shouldIgnorePath({ runtimeState, filePath })) return
|
|
145
|
+
|
|
146
|
+
runtimeState.queuedFileEvents.set(filePath, eventType)
|
|
147
|
+
|
|
148
|
+
if (runtimeState.fileDebounceTimer) {
|
|
149
|
+
clearTimeout(runtimeState.fileDebounceTimer)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
runtimeState.fileDebounceTimer = setTimeout(() => {
|
|
153
|
+
runtimeState.fileDebounceTimer = null
|
|
154
|
+
void runQueuedFileEvents({ runtimeState, discordClient })
|
|
155
|
+
}, DEFAULT_DEBOUNCE_MS)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
159
|
+
// DISCORD EVENT HANDLING
|
|
160
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
161
|
+
|
|
162
|
+
function scheduleDiscordSync({
|
|
163
|
+
runtimeState,
|
|
164
|
+
threadId,
|
|
165
|
+
discordClient,
|
|
166
|
+
}: {
|
|
167
|
+
runtimeState: ForumRuntimeState
|
|
168
|
+
threadId: string
|
|
169
|
+
discordClient: Client
|
|
170
|
+
}) {
|
|
171
|
+
runtimeState.dirtyThreadIds.add(threadId)
|
|
172
|
+
|
|
173
|
+
if (runtimeState.discordDebounceTimer) {
|
|
174
|
+
clearTimeout(runtimeState.discordDebounceTimer)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
runtimeState.discordDebounceTimer = setTimeout(() => {
|
|
178
|
+
runtimeState.discordDebounceTimer = null
|
|
179
|
+
void (async () => {
|
|
180
|
+
const syncResult = await syncForumToFiles({
|
|
181
|
+
discordClient,
|
|
182
|
+
forumChannelId: runtimeState.forumChannelId,
|
|
183
|
+
outputDir: runtimeState.outputDir,
|
|
184
|
+
runtimeState,
|
|
185
|
+
forceThreadIds: runtimeState.dirtyThreadIds,
|
|
186
|
+
})
|
|
187
|
+
if (syncResult instanceof Error) {
|
|
188
|
+
forumLogger.warn(
|
|
189
|
+
`Debounced Discord -> FS sync failed for ${runtimeState.forumChannelId}: ${syncResult.message}`,
|
|
190
|
+
)
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
runtimeState.dirtyThreadIds.clear()
|
|
194
|
+
})()
|
|
195
|
+
}, DEFAULT_DEBOUNCE_MS)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function getThreadEventData({
|
|
199
|
+
channel,
|
|
200
|
+
}: {
|
|
201
|
+
channel: ThreadChannel | null
|
|
202
|
+
}): { forumChannelId: string; threadId: string } | null {
|
|
203
|
+
if (!channel) return null
|
|
204
|
+
if (
|
|
205
|
+
channel.type !== ChannelType.PublicThread &&
|
|
206
|
+
channel.type !== ChannelType.PrivateThread &&
|
|
207
|
+
channel.type !== ChannelType.AnnouncementThread
|
|
208
|
+
) {
|
|
209
|
+
return null
|
|
210
|
+
}
|
|
211
|
+
if (!channel.parentId) return null
|
|
212
|
+
return { forumChannelId: channel.parentId, threadId: channel.id }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function getEventThreadFromMessage({
|
|
216
|
+
message,
|
|
217
|
+
}: {
|
|
218
|
+
message: Message | PartialMessage
|
|
219
|
+
}): ThreadChannel | null {
|
|
220
|
+
const channel = message.channel
|
|
221
|
+
if (!channel || !channel.isThread()) return null
|
|
222
|
+
return channel
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function tryHandleThreadEvent({
|
|
226
|
+
channel,
|
|
227
|
+
discordClient,
|
|
228
|
+
}: {
|
|
229
|
+
channel: ThreadChannel | null
|
|
230
|
+
discordClient: Client
|
|
231
|
+
}) {
|
|
232
|
+
const data = getThreadEventData({ channel })
|
|
233
|
+
if (!data) return
|
|
234
|
+
const runtimeState = forumStateById.get(data.forumChannelId)
|
|
235
|
+
if (!runtimeState) return
|
|
236
|
+
scheduleDiscordSync({ runtimeState, threadId: data.threadId, discordClient })
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Find the file path for a thread, checking root and one level of subdirectories.
|
|
241
|
+
*/
|
|
242
|
+
function findThreadFilePath({
|
|
243
|
+
outputDir,
|
|
244
|
+
threadId,
|
|
245
|
+
}: {
|
|
246
|
+
outputDir: string
|
|
247
|
+
threadId: string
|
|
248
|
+
}): string | null {
|
|
249
|
+
const rootPath = getCanonicalThreadFilePath({ outputDir, threadId })
|
|
250
|
+
if (fs.existsSync(rootPath)) return rootPath
|
|
251
|
+
|
|
252
|
+
const dirEntries = (() => {
|
|
253
|
+
try {
|
|
254
|
+
return fs.readdirSync(outputDir, { withFileTypes: true })
|
|
255
|
+
} catch {
|
|
256
|
+
return []
|
|
257
|
+
}
|
|
258
|
+
})()
|
|
259
|
+
for (const entry of dirEntries) {
|
|
260
|
+
if (!entry.isDirectory()) continue
|
|
261
|
+
const subPath = getCanonicalThreadFilePath({
|
|
262
|
+
outputDir,
|
|
263
|
+
threadId,
|
|
264
|
+
subfolder: entry.name,
|
|
265
|
+
})
|
|
266
|
+
if (fs.existsSync(subPath)) return subPath
|
|
267
|
+
}
|
|
268
|
+
return null
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function registerDiscordSyncListeners({
|
|
272
|
+
discordClient,
|
|
273
|
+
}: {
|
|
274
|
+
discordClient: Client
|
|
275
|
+
}) {
|
|
276
|
+
if (discordListenersRegistered) return
|
|
277
|
+
discordListenersRegistered = true
|
|
278
|
+
|
|
279
|
+
discordClient.on(Events.MessageCreate, (message) => {
|
|
280
|
+
if (message.author?.bot) return
|
|
281
|
+
const thread = getEventThreadFromMessage({ message })
|
|
282
|
+
tryHandleThreadEvent({ channel: thread, discordClient })
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
discordClient.on(Events.MessageUpdate, (_oldMessage, newMessage) => {
|
|
286
|
+
const thread = getEventThreadFromMessage({ message: newMessage })
|
|
287
|
+
tryHandleThreadEvent({ channel: thread, discordClient })
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
discordClient.on(Events.ThreadUpdate, (_oldThread, newThread) => {
|
|
291
|
+
tryHandleThreadEvent({ channel: newThread, discordClient })
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
discordClient.on(Events.ThreadDelete, async (thread) => {
|
|
295
|
+
const data = getThreadEventData({ channel: thread })
|
|
296
|
+
if (!data) return
|
|
297
|
+
const runtimeState = forumStateById.get(data.forumChannelId)
|
|
298
|
+
if (!runtimeState) return
|
|
299
|
+
const targetPath = findThreadFilePath({
|
|
300
|
+
outputDir: runtimeState.outputDir,
|
|
301
|
+
threadId: data.threadId,
|
|
302
|
+
})
|
|
303
|
+
if (!targetPath) return
|
|
304
|
+
addIgnoredPath({ runtimeState, filePath: targetPath })
|
|
305
|
+
await fs.promises.unlink(targetPath).catch((cause) => {
|
|
306
|
+
forumLogger.warn(
|
|
307
|
+
`Failed to delete forum file on thread delete ${targetPath}:`,
|
|
308
|
+
cause,
|
|
309
|
+
)
|
|
310
|
+
})
|
|
311
|
+
})
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
315
|
+
// FILE WATCHER SETUP
|
|
316
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
317
|
+
|
|
318
|
+
async function startWatcherForRuntimeState({
|
|
319
|
+
runtimeState,
|
|
320
|
+
discordClient,
|
|
321
|
+
}: {
|
|
322
|
+
runtimeState: ForumRuntimeState
|
|
323
|
+
discordClient: Client
|
|
324
|
+
}): Promise<void | ForumSyncOperationError> {
|
|
325
|
+
if (runtimeState.direction !== 'bidirectional') return
|
|
326
|
+
|
|
327
|
+
const subscription = await parcelWatcher
|
|
328
|
+
.subscribe(runtimeState.outputDir, (_error, events) => {
|
|
329
|
+
const mdEvents = events.filter((event) => event.path.endsWith('.md'))
|
|
330
|
+
mdEvents
|
|
331
|
+
.filter(
|
|
332
|
+
(event) =>
|
|
333
|
+
event.type === 'create' ||
|
|
334
|
+
event.type === 'update' ||
|
|
335
|
+
event.type === 'delete',
|
|
336
|
+
)
|
|
337
|
+
.map((event) => {
|
|
338
|
+
queueFileEvent({
|
|
339
|
+
runtimeState,
|
|
340
|
+
filePath: event.path,
|
|
341
|
+
eventType: event.type as 'create' | 'update' | 'delete',
|
|
342
|
+
discordClient,
|
|
343
|
+
})
|
|
344
|
+
})
|
|
345
|
+
})
|
|
346
|
+
.catch(
|
|
347
|
+
(cause) =>
|
|
348
|
+
new ForumSyncOperationError({
|
|
349
|
+
forumChannelId: runtimeState.forumChannelId,
|
|
350
|
+
reason: `failed to subscribe watcher for ${runtimeState.outputDir}`,
|
|
351
|
+
cause,
|
|
352
|
+
}),
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
if (subscription instanceof Error) return subscription
|
|
356
|
+
|
|
357
|
+
watcherUnsubscribeByForumId.set(runtimeState.forumChannelId, () => {
|
|
358
|
+
return subscription.unsubscribe()
|
|
359
|
+
})
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
363
|
+
// PUBLIC API
|
|
364
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
365
|
+
|
|
366
|
+
export async function stopConfiguredForumSync() {
|
|
367
|
+
const unsubscribers = Array.from(watcherUnsubscribeByForumId.values())
|
|
368
|
+
watcherUnsubscribeByForumId.clear()
|
|
369
|
+
forumStateById.clear()
|
|
370
|
+
|
|
371
|
+
await Promise.all(
|
|
372
|
+
unsubscribers.map(async (unsubscribe) => {
|
|
373
|
+
await unsubscribe().catch((cause) => {
|
|
374
|
+
forumLogger.warn('Failed to unsubscribe forum watcher:', cause)
|
|
375
|
+
})
|
|
376
|
+
}),
|
|
377
|
+
)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export async function startConfiguredForumSync({
|
|
381
|
+
discordClient,
|
|
382
|
+
appId,
|
|
383
|
+
}: StartForumSyncOptions) {
|
|
384
|
+
const loadedConfig = await readForumSyncConfig({ appId })
|
|
385
|
+
if (loadedConfig instanceof Error) return loadedConfig
|
|
386
|
+
|
|
387
|
+
if (loadedConfig.length === 0) return
|
|
388
|
+
|
|
389
|
+
registerDiscordSyncListeners({ discordClient })
|
|
390
|
+
|
|
391
|
+
// Process each config independently so one stale/deleted forum channel
|
|
392
|
+
// doesn't block the watcher from starting for other valid configs.
|
|
393
|
+
for (const entry of loadedConfig) {
|
|
394
|
+
const runtimeState = buildRuntimeState({
|
|
395
|
+
forumChannelId: entry.forumChannelId,
|
|
396
|
+
outputDir: entry.outputDir,
|
|
397
|
+
direction: entry.direction,
|
|
398
|
+
})
|
|
399
|
+
forumStateById.set(entry.forumChannelId, runtimeState)
|
|
400
|
+
|
|
401
|
+
const ensureResult = await ensureDirectory({ directory: entry.outputDir })
|
|
402
|
+
if (ensureResult instanceof Error) {
|
|
403
|
+
forumLogger.warn(
|
|
404
|
+
`Skipping forum ${entry.forumChannelId}: failed to create ${entry.outputDir}`,
|
|
405
|
+
)
|
|
406
|
+
continue
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const fileToDiscordResult = await syncFilesToForum({
|
|
410
|
+
discordClient,
|
|
411
|
+
forumChannelId: entry.forumChannelId,
|
|
412
|
+
outputDir: entry.outputDir,
|
|
413
|
+
runtimeState,
|
|
414
|
+
})
|
|
415
|
+
if (fileToDiscordResult instanceof Error) {
|
|
416
|
+
forumLogger.warn(
|
|
417
|
+
`Skipping forum ${entry.forumChannelId}: FS->Discord sync failed: ${fileToDiscordResult.message}`,
|
|
418
|
+
)
|
|
419
|
+
continue
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const discordToFileResult = await syncForumToFiles({
|
|
423
|
+
discordClient,
|
|
424
|
+
forumChannelId: entry.forumChannelId,
|
|
425
|
+
outputDir: entry.outputDir,
|
|
426
|
+
forceFullRefresh: true,
|
|
427
|
+
runtimeState,
|
|
428
|
+
})
|
|
429
|
+
if (discordToFileResult instanceof Error) {
|
|
430
|
+
forumLogger.warn(
|
|
431
|
+
`Skipping forum ${entry.forumChannelId}: Discord->FS sync failed: ${discordToFileResult.message}`,
|
|
432
|
+
)
|
|
433
|
+
continue
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const watcherResult = await startWatcherForRuntimeState({
|
|
437
|
+
runtimeState,
|
|
438
|
+
discordClient,
|
|
439
|
+
})
|
|
440
|
+
if (watcherResult instanceof Error) {
|
|
441
|
+
forumLogger.warn(
|
|
442
|
+
`Skipping forum ${entry.forumChannelId}: watcher failed: ${watcherResult.message}`,
|
|
443
|
+
)
|
|
444
|
+
continue
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
forumLogger.log(
|
|
448
|
+
`Forum sync started for ${entry.forumChannelId} (${entry.direction}) -> ${entry.outputDir}`,
|
|
449
|
+
)
|
|
450
|
+
forumLogger.log(
|
|
451
|
+
`Initial sync: Discord->FS synced ${discordToFileResult.synced}, skipped ${discordToFileResult.skipped}, deleted ${discordToFileResult.deleted}; FS->Discord created ${fileToDiscordResult.created}, updated ${fileToDiscordResult.updated}, deleted ${fileToDiscordResult.deleted}`,
|
|
452
|
+
)
|
|
453
|
+
}
|
|
454
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// Main thread interface for the GenAI worker.
|
|
2
|
+
// Spawns and manages the worker thread, handling message passing for
|
|
3
|
+
// audio input/output, tool call completions, and graceful shutdown.
|
|
4
|
+
|
|
5
|
+
import { Worker } from 'node:worker_threads'
|
|
6
|
+
import type { WorkerInMessage, WorkerOutMessage } from './worker-types.js'
|
|
7
|
+
import { createLogger, LogPrefix } from './logger.js'
|
|
8
|
+
import { notifyError } from './sentry.js'
|
|
9
|
+
|
|
10
|
+
const genaiWorkerLogger = createLogger(LogPrefix.GENAI_WORKER)
|
|
11
|
+
const genaiWrapperLogger = createLogger(LogPrefix.GENAI_WORKER)
|
|
12
|
+
|
|
13
|
+
export interface GenAIWorkerOptions {
|
|
14
|
+
directory: string
|
|
15
|
+
systemMessage?: string
|
|
16
|
+
guildId: string
|
|
17
|
+
channelId: string
|
|
18
|
+
appId: string
|
|
19
|
+
geminiApiKey?: string | null
|
|
20
|
+
onAssistantOpusPacket: (packet: ArrayBuffer) => void
|
|
21
|
+
onAssistantStartSpeaking?: () => void
|
|
22
|
+
onAssistantStopSpeaking?: () => void
|
|
23
|
+
onAssistantInterruptSpeaking?: () => void
|
|
24
|
+
onToolCallCompleted?: (params: {
|
|
25
|
+
sessionId: string
|
|
26
|
+
messageId: string
|
|
27
|
+
data?: unknown
|
|
28
|
+
error?: unknown
|
|
29
|
+
markdown?: string
|
|
30
|
+
}) => void
|
|
31
|
+
onError?: (error: string) => void
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface GenAIWorker {
|
|
35
|
+
sendRealtimeInput(params: {
|
|
36
|
+
audio?: { mimeType: string; data: string }
|
|
37
|
+
audioStreamEnd?: boolean
|
|
38
|
+
}): void
|
|
39
|
+
sendTextInput(text: string): void
|
|
40
|
+
interrupt(): void
|
|
41
|
+
stop(): Promise<void>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createGenAIWorker(
|
|
45
|
+
options: GenAIWorkerOptions,
|
|
46
|
+
): Promise<GenAIWorker> {
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
const worker = new Worker(
|
|
49
|
+
new URL('../dist/genai-worker.js', import.meta.url),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
// Handle messages from worker
|
|
53
|
+
worker.on('message', (message: WorkerOutMessage) => {
|
|
54
|
+
switch (message.type) {
|
|
55
|
+
case 'assistantOpusPacket':
|
|
56
|
+
options.onAssistantOpusPacket(message.packet)
|
|
57
|
+
break
|
|
58
|
+
case 'assistantStartSpeaking':
|
|
59
|
+
options.onAssistantStartSpeaking?.()
|
|
60
|
+
break
|
|
61
|
+
case 'assistantStopSpeaking':
|
|
62
|
+
options.onAssistantStopSpeaking?.()
|
|
63
|
+
break
|
|
64
|
+
case 'assistantInterruptSpeaking':
|
|
65
|
+
options.onAssistantInterruptSpeaking?.()
|
|
66
|
+
break
|
|
67
|
+
case 'toolCallCompleted':
|
|
68
|
+
options.onToolCallCompleted?.(message)
|
|
69
|
+
break
|
|
70
|
+
case 'error':
|
|
71
|
+
genaiWorkerLogger.error('Error:', message.error)
|
|
72
|
+
options.onError?.(message.error)
|
|
73
|
+
break
|
|
74
|
+
case 'ready':
|
|
75
|
+
genaiWorkerLogger.log('Ready')
|
|
76
|
+
// Resolve with the worker interface
|
|
77
|
+
resolve({
|
|
78
|
+
sendRealtimeInput({ audio, audioStreamEnd }) {
|
|
79
|
+
worker.postMessage({
|
|
80
|
+
type: 'sendRealtimeInput',
|
|
81
|
+
audio,
|
|
82
|
+
audioStreamEnd,
|
|
83
|
+
} satisfies WorkerInMessage)
|
|
84
|
+
},
|
|
85
|
+
sendTextInput(text) {
|
|
86
|
+
worker.postMessage({
|
|
87
|
+
type: 'sendTextInput',
|
|
88
|
+
text,
|
|
89
|
+
} satisfies WorkerInMessage)
|
|
90
|
+
},
|
|
91
|
+
interrupt() {
|
|
92
|
+
worker.postMessage({
|
|
93
|
+
type: 'interrupt',
|
|
94
|
+
} satisfies WorkerInMessage)
|
|
95
|
+
},
|
|
96
|
+
async stop() {
|
|
97
|
+
genaiWrapperLogger.log('Stopping worker...')
|
|
98
|
+
// Send stop message to trigger graceful shutdown
|
|
99
|
+
worker.postMessage({ type: 'stop' } satisfies WorkerInMessage)
|
|
100
|
+
|
|
101
|
+
// Wait for worker to exit gracefully (with timeout)
|
|
102
|
+
await new Promise<void>((resolve) => {
|
|
103
|
+
let resolved = false
|
|
104
|
+
|
|
105
|
+
// Listen for worker exit
|
|
106
|
+
worker.once('exit', (code) => {
|
|
107
|
+
if (!resolved) {
|
|
108
|
+
resolved = true
|
|
109
|
+
genaiWrapperLogger.log(
|
|
110
|
+
`[GENAI WORKER WRAPPER] Worker exited with code ${code}`,
|
|
111
|
+
)
|
|
112
|
+
resolve()
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
// Timeout after 5 seconds and force terminate
|
|
117
|
+
setTimeout(() => {
|
|
118
|
+
if (!resolved) {
|
|
119
|
+
resolved = true
|
|
120
|
+
genaiWrapperLogger.log(
|
|
121
|
+
'[GENAI WORKER WRAPPER] Worker did not exit gracefully, terminating...',
|
|
122
|
+
)
|
|
123
|
+
worker.terminate().then(() => {
|
|
124
|
+
genaiWrapperLogger.log('Worker terminated')
|
|
125
|
+
resolve()
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
}, 5000)
|
|
129
|
+
})
|
|
130
|
+
},
|
|
131
|
+
})
|
|
132
|
+
break
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
// Handle worker errors
|
|
137
|
+
worker.on('error', (error) => {
|
|
138
|
+
genaiWorkerLogger.error('Worker error:', error)
|
|
139
|
+
reject(error)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
worker.on('exit', (code) => {
|
|
143
|
+
if (code !== 0) {
|
|
144
|
+
genaiWorkerLogger.error(`Worker stopped with exit code ${code}`)
|
|
145
|
+
void notifyError(
|
|
146
|
+
new Error(`GenAI worker exited with code ${code}`),
|
|
147
|
+
'GenAI worker non-zero exit after init',
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
// Send initialization message
|
|
153
|
+
const initMessage: WorkerInMessage = {
|
|
154
|
+
type: 'init',
|
|
155
|
+
directory: options.directory,
|
|
156
|
+
systemMessage: options.systemMessage,
|
|
157
|
+
guildId: options.guildId,
|
|
158
|
+
channelId: options.channelId,
|
|
159
|
+
appId: options.appId,
|
|
160
|
+
geminiApiKey: options.geminiApiKey,
|
|
161
|
+
}
|
|
162
|
+
worker.postMessage(initMessage)
|
|
163
|
+
})
|
|
164
|
+
}
|