disunday 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai-tool-to-genai.js +208 -0
- package/dist/ai-tool-to-genai.test.js +267 -0
- package/dist/channel-management.js +96 -0
- package/dist/cli.js +1674 -0
- package/dist/commands/abort.js +89 -0
- package/dist/commands/add-project.js +117 -0
- package/dist/commands/agent.js +250 -0
- package/dist/commands/ask-question.js +219 -0
- package/dist/commands/compact.js +126 -0
- package/dist/commands/context-menu.js +171 -0
- package/dist/commands/context.js +89 -0
- package/dist/commands/cost.js +93 -0
- package/dist/commands/create-new-project.js +111 -0
- package/dist/commands/diff.js +77 -0
- package/dist/commands/export.js +100 -0
- package/dist/commands/files.js +73 -0
- package/dist/commands/fork.js +199 -0
- package/dist/commands/help.js +54 -0
- package/dist/commands/login.js +488 -0
- package/dist/commands/merge-worktree.js +165 -0
- package/dist/commands/model.js +325 -0
- package/dist/commands/permissions.js +140 -0
- package/dist/commands/ping.js +13 -0
- package/dist/commands/queue.js +133 -0
- package/dist/commands/remove-project.js +119 -0
- package/dist/commands/rename.js +70 -0
- package/dist/commands/restart-opencode-server.js +77 -0
- package/dist/commands/resume.js +276 -0
- package/dist/commands/run-config.js +79 -0
- package/dist/commands/run.js +240 -0
- package/dist/commands/schedule.js +170 -0
- package/dist/commands/session-info.js +58 -0
- package/dist/commands/session.js +191 -0
- package/dist/commands/settings.js +84 -0
- package/dist/commands/share.js +89 -0
- package/dist/commands/status.js +79 -0
- package/dist/commands/sync.js +119 -0
- package/dist/commands/theme.js +53 -0
- package/dist/commands/types.js +2 -0
- package/dist/commands/undo-redo.js +170 -0
- package/dist/commands/user-command.js +135 -0
- package/dist/commands/verbosity.js +59 -0
- package/dist/commands/worktree-settings.js +50 -0
- package/dist/commands/worktree.js +288 -0
- package/dist/config.js +139 -0
- package/dist/database.js +585 -0
- package/dist/discord-bot.js +700 -0
- package/dist/discord-utils.js +336 -0
- package/dist/discord-utils.test.js +20 -0
- package/dist/errors.js +193 -0
- package/dist/escape-backticks.test.js +429 -0
- package/dist/format-tables.js +96 -0
- package/dist/format-tables.test.js +418 -0
- package/dist/genai-worker-wrapper.js +109 -0
- package/dist/genai-worker.js +299 -0
- package/dist/genai.js +230 -0
- package/dist/image-utils.js +107 -0
- package/dist/interaction-handler.js +289 -0
- package/dist/limit-heading-depth.js +25 -0
- package/dist/limit-heading-depth.test.js +105 -0
- package/dist/logger.js +111 -0
- package/dist/markdown.js +323 -0
- package/dist/markdown.test.js +269 -0
- package/dist/message-formatting.js +447 -0
- package/dist/message-formatting.test.js +73 -0
- package/dist/openai-realtime.js +226 -0
- package/dist/opencode.js +224 -0
- package/dist/reaction-handler.js +128 -0
- package/dist/scheduler.js +93 -0
- package/dist/security.js +200 -0
- package/dist/session-handler.js +1436 -0
- package/dist/system-message.js +138 -0
- package/dist/tools.js +354 -0
- package/dist/unnest-code-blocks.js +117 -0
- package/dist/unnest-code-blocks.test.js +432 -0
- package/dist/utils.js +95 -0
- package/dist/voice-handler.js +569 -0
- package/dist/voice.js +344 -0
- package/dist/worker-types.js +4 -0
- package/dist/worktree-utils.js +134 -0
- package/dist/xml.js +90 -0
- package/dist/xml.test.js +32 -0
- package/package.json +84 -0
package/dist/database.js
ADDED
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
// SQLite database manager for persistent bot state.
|
|
2
|
+
// Stores thread-session mappings, bot tokens, channel directories,
|
|
3
|
+
// API keys, and model preferences in <dataDir>/discord-sessions.db.
|
|
4
|
+
import Database from 'better-sqlite3';
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import * as errore from 'errore';
|
|
8
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
9
|
+
import { getDataDir, getDefaultVerbosity } from './config.js';
|
|
10
|
+
import { getEncryptionKey, encrypt, decrypt, serializeEncrypted, deserializeEncrypted, isEncrypted, } from './security.js';
|
|
11
|
+
const dbLogger = createLogger(LogPrefix.DB);
|
|
12
|
+
let db = null;
|
|
13
|
+
export function getDatabase() {
|
|
14
|
+
if (!db) {
|
|
15
|
+
const dataDir = getDataDir();
|
|
16
|
+
const mkdirError = errore.tryFn({
|
|
17
|
+
try: () => {
|
|
18
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
19
|
+
},
|
|
20
|
+
catch: (e) => e,
|
|
21
|
+
});
|
|
22
|
+
if (mkdirError instanceof Error) {
|
|
23
|
+
dbLogger.error(`Failed to create data directory ${dataDir}:`, mkdirError.message);
|
|
24
|
+
}
|
|
25
|
+
const dbPath = path.join(dataDir, 'discord-sessions.db');
|
|
26
|
+
dbLogger.log(`Opening database at: ${dbPath}`);
|
|
27
|
+
db = new Database(dbPath);
|
|
28
|
+
db.exec(`
|
|
29
|
+
CREATE TABLE IF NOT EXISTS thread_sessions (
|
|
30
|
+
thread_id TEXT PRIMARY KEY,
|
|
31
|
+
session_id TEXT NOT NULL,
|
|
32
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
33
|
+
)
|
|
34
|
+
`);
|
|
35
|
+
db.exec(`
|
|
36
|
+
CREATE TABLE IF NOT EXISTS part_messages (
|
|
37
|
+
part_id TEXT PRIMARY KEY,
|
|
38
|
+
message_id TEXT NOT NULL,
|
|
39
|
+
thread_id TEXT NOT NULL,
|
|
40
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
41
|
+
)
|
|
42
|
+
`);
|
|
43
|
+
db.exec(`
|
|
44
|
+
CREATE TABLE IF NOT EXISTS bot_tokens (
|
|
45
|
+
app_id TEXT PRIMARY KEY,
|
|
46
|
+
token TEXT NOT NULL,
|
|
47
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
48
|
+
)
|
|
49
|
+
`);
|
|
50
|
+
db.exec(`
|
|
51
|
+
CREATE TABLE IF NOT EXISTS channel_directories (
|
|
52
|
+
channel_id TEXT PRIMARY KEY,
|
|
53
|
+
directory TEXT NOT NULL,
|
|
54
|
+
channel_type TEXT NOT NULL,
|
|
55
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
56
|
+
)
|
|
57
|
+
`);
|
|
58
|
+
// Migration: add app_id column to channel_directories for multi-bot support
|
|
59
|
+
try {
|
|
60
|
+
db.exec(`ALTER TABLE channel_directories ADD COLUMN app_id TEXT`);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
dbLogger.debug('Failed to add app_id column to channel_directories (likely exists):', error instanceof Error ? error.message : String(error));
|
|
64
|
+
}
|
|
65
|
+
// Table for threads that should auto-start a session (created by CLI without --notify-only)
|
|
66
|
+
db.exec(`
|
|
67
|
+
CREATE TABLE IF NOT EXISTS pending_auto_start (
|
|
68
|
+
thread_id TEXT PRIMARY KEY,
|
|
69
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
70
|
+
)
|
|
71
|
+
`);
|
|
72
|
+
db.exec(`
|
|
73
|
+
CREATE TABLE IF NOT EXISTS bot_api_keys (
|
|
74
|
+
app_id TEXT PRIMARY KEY,
|
|
75
|
+
gemini_api_key TEXT,
|
|
76
|
+
xai_api_key TEXT,
|
|
77
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
78
|
+
)
|
|
79
|
+
`);
|
|
80
|
+
// Track worktrees created for threads (for /new-worktree command)
|
|
81
|
+
// status: 'pending' while creating, 'ready' when done, 'error' if failed
|
|
82
|
+
db.exec(`
|
|
83
|
+
CREATE TABLE IF NOT EXISTS thread_worktrees (
|
|
84
|
+
thread_id TEXT PRIMARY KEY,
|
|
85
|
+
worktree_name TEXT NOT NULL,
|
|
86
|
+
worktree_directory TEXT,
|
|
87
|
+
project_directory TEXT NOT NULL,
|
|
88
|
+
status TEXT DEFAULT 'pending',
|
|
89
|
+
error_message TEXT,
|
|
90
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
91
|
+
)
|
|
92
|
+
`);
|
|
93
|
+
runModelMigrations(db);
|
|
94
|
+
runWorktreeSettingsMigrations(db);
|
|
95
|
+
runVerbosityMigrations(db);
|
|
96
|
+
runRunConfigMigrations(db);
|
|
97
|
+
runThemeMigrations(db);
|
|
98
|
+
runBotSettingsMigrations(db);
|
|
99
|
+
}
|
|
100
|
+
return db;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Run migrations for model preferences tables.
|
|
104
|
+
* Called on startup and can be called on-demand.
|
|
105
|
+
*/
|
|
106
|
+
export function runModelMigrations(database) {
|
|
107
|
+
const targetDb = database || getDatabase();
|
|
108
|
+
targetDb.exec(`
|
|
109
|
+
CREATE TABLE IF NOT EXISTS channel_models (
|
|
110
|
+
channel_id TEXT PRIMARY KEY,
|
|
111
|
+
model_id TEXT NOT NULL,
|
|
112
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
113
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
114
|
+
)
|
|
115
|
+
`);
|
|
116
|
+
targetDb.exec(`
|
|
117
|
+
CREATE TABLE IF NOT EXISTS session_models (
|
|
118
|
+
session_id TEXT PRIMARY KEY,
|
|
119
|
+
model_id TEXT NOT NULL,
|
|
120
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
121
|
+
)
|
|
122
|
+
`);
|
|
123
|
+
targetDb.exec(`
|
|
124
|
+
CREATE TABLE IF NOT EXISTS channel_agents (
|
|
125
|
+
channel_id TEXT PRIMARY KEY,
|
|
126
|
+
agent_name TEXT NOT NULL,
|
|
127
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
128
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
129
|
+
)
|
|
130
|
+
`);
|
|
131
|
+
targetDb.exec(`
|
|
132
|
+
CREATE TABLE IF NOT EXISTS session_agents (
|
|
133
|
+
session_id TEXT PRIMARY KEY,
|
|
134
|
+
agent_name TEXT NOT NULL,
|
|
135
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
136
|
+
)
|
|
137
|
+
`);
|
|
138
|
+
dbLogger.log('Model preferences migrations complete');
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Get the model preference for a channel.
|
|
142
|
+
* @returns Model ID in format "provider_id/model_id" or undefined
|
|
143
|
+
*/
|
|
144
|
+
export function getChannelModel(channelId) {
|
|
145
|
+
const db = getDatabase();
|
|
146
|
+
const row = db
|
|
147
|
+
.prepare('SELECT model_id FROM channel_models WHERE channel_id = ?')
|
|
148
|
+
.get(channelId);
|
|
149
|
+
return row?.model_id;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Set the model preference for a channel.
|
|
153
|
+
* @param modelId Model ID in format "provider_id/model_id"
|
|
154
|
+
*/
|
|
155
|
+
export function setChannelModel(channelId, modelId) {
|
|
156
|
+
const db = getDatabase();
|
|
157
|
+
db.prepare(`INSERT INTO channel_models (channel_id, model_id, updated_at)
|
|
158
|
+
VALUES (?, ?, CURRENT_TIMESTAMP)
|
|
159
|
+
ON CONFLICT(channel_id) DO UPDATE SET model_id = ?, updated_at = CURRENT_TIMESTAMP`).run(channelId, modelId, modelId);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Get the model preference for a session.
|
|
163
|
+
* @returns Model ID in format "provider_id/model_id" or undefined
|
|
164
|
+
*/
|
|
165
|
+
export function getSessionModel(sessionId) {
|
|
166
|
+
const db = getDatabase();
|
|
167
|
+
const row = db
|
|
168
|
+
.prepare('SELECT model_id FROM session_models WHERE session_id = ?')
|
|
169
|
+
.get(sessionId);
|
|
170
|
+
return row?.model_id;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Set the model preference for a session.
|
|
174
|
+
* @param modelId Model ID in format "provider_id/model_id"
|
|
175
|
+
*/
|
|
176
|
+
export function setSessionModel(sessionId, modelId) {
|
|
177
|
+
const db = getDatabase();
|
|
178
|
+
db.prepare(`INSERT OR REPLACE INTO session_models (session_id, model_id) VALUES (?, ?)`).run(sessionId, modelId);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Clear the model preference for a session.
|
|
182
|
+
* Used when switching agents so the agent's model takes effect.
|
|
183
|
+
*/
|
|
184
|
+
export function clearSessionModel(sessionId) {
|
|
185
|
+
const db = getDatabase();
|
|
186
|
+
db.prepare('DELETE FROM session_models WHERE session_id = ?').run(sessionId);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Get the agent preference for a channel.
|
|
190
|
+
*/
|
|
191
|
+
export function getChannelAgent(channelId) {
|
|
192
|
+
const db = getDatabase();
|
|
193
|
+
const row = db
|
|
194
|
+
.prepare('SELECT agent_name FROM channel_agents WHERE channel_id = ?')
|
|
195
|
+
.get(channelId);
|
|
196
|
+
return row?.agent_name;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Set the agent preference for a channel.
|
|
200
|
+
*/
|
|
201
|
+
export function setChannelAgent(channelId, agentName) {
|
|
202
|
+
const db = getDatabase();
|
|
203
|
+
db.prepare(`INSERT INTO channel_agents (channel_id, agent_name, updated_at)
|
|
204
|
+
VALUES (?, ?, CURRENT_TIMESTAMP)
|
|
205
|
+
ON CONFLICT(channel_id) DO UPDATE SET agent_name = ?, updated_at = CURRENT_TIMESTAMP`).run(channelId, agentName, agentName);
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Get the agent preference for a session.
|
|
209
|
+
*/
|
|
210
|
+
export function getSessionAgent(sessionId) {
|
|
211
|
+
const db = getDatabase();
|
|
212
|
+
const row = db
|
|
213
|
+
.prepare('SELECT agent_name FROM session_agents WHERE session_id = ?')
|
|
214
|
+
.get(sessionId);
|
|
215
|
+
return row?.agent_name;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Set the agent preference for a session.
|
|
219
|
+
*/
|
|
220
|
+
export function setSessionAgent(sessionId, agentName) {
|
|
221
|
+
const db = getDatabase();
|
|
222
|
+
db.prepare(`INSERT OR REPLACE INTO session_agents (session_id, agent_name) VALUES (?, ?)`).run(sessionId, agentName);
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Get the worktree info for a thread.
|
|
226
|
+
*/
|
|
227
|
+
export function getThreadWorktree(threadId) {
|
|
228
|
+
const db = getDatabase();
|
|
229
|
+
return db
|
|
230
|
+
.prepare('SELECT * FROM thread_worktrees WHERE thread_id = ?')
|
|
231
|
+
.get(threadId);
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Create a pending worktree entry for a thread.
|
|
235
|
+
*/
|
|
236
|
+
export function createPendingWorktree({ threadId, worktreeName, projectDirectory, }) {
|
|
237
|
+
const db = getDatabase();
|
|
238
|
+
db.prepare(`INSERT OR REPLACE INTO thread_worktrees (thread_id, worktree_name, project_directory, status) VALUES (?, ?, ?, 'pending')`).run(threadId, worktreeName, projectDirectory);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Mark a worktree as ready with its directory.
|
|
242
|
+
*/
|
|
243
|
+
export function setWorktreeReady({ threadId, worktreeDirectory, }) {
|
|
244
|
+
const db = getDatabase();
|
|
245
|
+
db.prepare(`UPDATE thread_worktrees SET worktree_directory = ?, status = 'ready' WHERE thread_id = ?`).run(worktreeDirectory, threadId);
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Mark a worktree as failed with error message.
|
|
249
|
+
*/
|
|
250
|
+
export function setWorktreeError({ threadId, errorMessage, }) {
|
|
251
|
+
const db = getDatabase();
|
|
252
|
+
db.prepare(`UPDATE thread_worktrees SET status = 'error', error_message = ? WHERE thread_id = ?`).run(errorMessage, threadId);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Delete the worktree info for a thread.
|
|
256
|
+
*/
|
|
257
|
+
export function deleteThreadWorktree(threadId) {
|
|
258
|
+
const db = getDatabase();
|
|
259
|
+
db.prepare('DELETE FROM thread_worktrees WHERE thread_id = ?').run(threadId);
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Run migrations for channel worktree settings table.
|
|
263
|
+
* Called on startup. Allows per-channel opt-in for automatic worktree creation.
|
|
264
|
+
*/
|
|
265
|
+
export function runWorktreeSettingsMigrations(database) {
|
|
266
|
+
const targetDb = database || getDatabase();
|
|
267
|
+
targetDb.exec(`
|
|
268
|
+
CREATE TABLE IF NOT EXISTS channel_worktrees (
|
|
269
|
+
channel_id TEXT PRIMARY KEY,
|
|
270
|
+
enabled INTEGER NOT NULL DEFAULT 0,
|
|
271
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
272
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
273
|
+
)
|
|
274
|
+
`);
|
|
275
|
+
dbLogger.log('Channel worktree settings migrations complete');
|
|
276
|
+
}
|
|
277
|
+
export function runVerbosityMigrations(database) {
|
|
278
|
+
const targetDb = database || getDatabase();
|
|
279
|
+
targetDb.exec(`
|
|
280
|
+
CREATE TABLE IF NOT EXISTS channel_verbosity (
|
|
281
|
+
channel_id TEXT PRIMARY KEY,
|
|
282
|
+
verbosity TEXT NOT NULL DEFAULT 'tools-and-text',
|
|
283
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
284
|
+
)
|
|
285
|
+
`);
|
|
286
|
+
dbLogger.log('Channel verbosity settings migrations complete');
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Get the verbosity setting for a channel.
|
|
290
|
+
* Falls back to the global default set via --verbosity CLI flag if no per-channel override exists.
|
|
291
|
+
*/
|
|
292
|
+
export function getChannelVerbosity(channelId) {
|
|
293
|
+
const db = getDatabase();
|
|
294
|
+
const row = db
|
|
295
|
+
.prepare('SELECT verbosity FROM channel_verbosity WHERE channel_id = ?')
|
|
296
|
+
.get(channelId);
|
|
297
|
+
if (row?.verbosity) {
|
|
298
|
+
return row.verbosity;
|
|
299
|
+
}
|
|
300
|
+
return getDefaultVerbosity();
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Set the verbosity setting for a channel.
|
|
304
|
+
*/
|
|
305
|
+
export function setChannelVerbosity(channelId, verbosity) {
|
|
306
|
+
const db = getDatabase();
|
|
307
|
+
db.prepare(`INSERT INTO channel_verbosity (channel_id, verbosity, updated_at)
|
|
308
|
+
VALUES (?, ?, CURRENT_TIMESTAMP)
|
|
309
|
+
ON CONFLICT(channel_id) DO UPDATE SET verbosity = ?, updated_at = CURRENT_TIMESTAMP`).run(channelId, verbosity, verbosity);
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Check if automatic worktree creation is enabled for a channel.
|
|
313
|
+
*/
|
|
314
|
+
export function getChannelWorktreesEnabled(channelId) {
|
|
315
|
+
const db = getDatabase();
|
|
316
|
+
const row = db
|
|
317
|
+
.prepare('SELECT enabled FROM channel_worktrees WHERE channel_id = ?')
|
|
318
|
+
.get(channelId);
|
|
319
|
+
return row?.enabled === 1;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Enable or disable automatic worktree creation for a channel.
|
|
323
|
+
*/
|
|
324
|
+
export function setChannelWorktreesEnabled(channelId, enabled) {
|
|
325
|
+
const db = getDatabase();
|
|
326
|
+
db.prepare(`INSERT INTO channel_worktrees (channel_id, enabled, updated_at)
|
|
327
|
+
VALUES (?, ?, CURRENT_TIMESTAMP)
|
|
328
|
+
ON CONFLICT(channel_id) DO UPDATE SET enabled = ?, updated_at = CURRENT_TIMESTAMP`).run(channelId, enabled ? 1 : 0, enabled ? 1 : 0);
|
|
329
|
+
}
|
|
330
|
+
export function runRunConfigMigrations(database) {
|
|
331
|
+
const targetDb = database || getDatabase();
|
|
332
|
+
targetDb.exec(`
|
|
333
|
+
CREATE TABLE IF NOT EXISTS run_config (
|
|
334
|
+
channel_id TEXT PRIMARY KEY,
|
|
335
|
+
notify_discord INTEGER NOT NULL DEFAULT 1,
|
|
336
|
+
notify_system INTEGER NOT NULL DEFAULT 1,
|
|
337
|
+
webhook_url TEXT,
|
|
338
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
339
|
+
)
|
|
340
|
+
`);
|
|
341
|
+
dbLogger.log('Run config migrations complete');
|
|
342
|
+
}
|
|
343
|
+
export function getRunConfig(channelId) {
|
|
344
|
+
const db = getDatabase();
|
|
345
|
+
const row = db
|
|
346
|
+
.prepare('SELECT * FROM run_config WHERE channel_id = ?')
|
|
347
|
+
.get(channelId);
|
|
348
|
+
return row || { notify_discord: 1, notify_system: 1, webhook_url: null };
|
|
349
|
+
}
|
|
350
|
+
export function setRunConfig(channelId, config) {
|
|
351
|
+
const db = getDatabase();
|
|
352
|
+
const current = getRunConfig(channelId);
|
|
353
|
+
const updated = { ...current, ...config };
|
|
354
|
+
db.prepare(`INSERT INTO run_config (channel_id, notify_discord, notify_system, webhook_url, updated_at)
|
|
355
|
+
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
356
|
+
ON CONFLICT(channel_id) DO UPDATE SET
|
|
357
|
+
notify_discord = ?, notify_system = ?, webhook_url = ?, updated_at = CURRENT_TIMESTAMP`).run(channelId, updated.notify_discord, updated.notify_system, updated.webhook_url, updated.notify_discord, updated.notify_system, updated.webhook_url);
|
|
358
|
+
}
|
|
359
|
+
export function runThemeMigrations(database) {
|
|
360
|
+
const targetDb = database || getDatabase();
|
|
361
|
+
targetDb.exec(`
|
|
362
|
+
CREATE TABLE IF NOT EXISTS channel_theme (
|
|
363
|
+
channel_id TEXT PRIMARY KEY,
|
|
364
|
+
theme TEXT NOT NULL DEFAULT 'default',
|
|
365
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
366
|
+
)
|
|
367
|
+
`);
|
|
368
|
+
dbLogger.log('Channel theme migrations complete');
|
|
369
|
+
}
|
|
370
|
+
export function getChannelTheme(channelId) {
|
|
371
|
+
const db = getDatabase();
|
|
372
|
+
const row = db
|
|
373
|
+
.prepare('SELECT theme FROM channel_theme WHERE channel_id = ?')
|
|
374
|
+
.get(channelId);
|
|
375
|
+
return row?.theme || 'default';
|
|
376
|
+
}
|
|
377
|
+
export function setChannelTheme(channelId, theme) {
|
|
378
|
+
const db = getDatabase();
|
|
379
|
+
db.prepare(`INSERT INTO channel_theme (channel_id, theme, updated_at)
|
|
380
|
+
VALUES (?, ?, CURRENT_TIMESTAMP)
|
|
381
|
+
ON CONFLICT(channel_id) DO UPDATE SET theme = ?, updated_at = CURRENT_TIMESTAMP`).run(channelId, theme, theme);
|
|
382
|
+
}
|
|
383
|
+
export function runBotSettingsMigrations(database) {
|
|
384
|
+
const targetDb = database || getDatabase();
|
|
385
|
+
targetDb.exec(`
|
|
386
|
+
CREATE TABLE IF NOT EXISTS bot_settings (
|
|
387
|
+
app_id TEXT PRIMARY KEY,
|
|
388
|
+
hub_channel_id TEXT,
|
|
389
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
390
|
+
)
|
|
391
|
+
`);
|
|
392
|
+
dbLogger.log('Bot settings migrations complete');
|
|
393
|
+
}
|
|
394
|
+
export function getBotSettings(appId) {
|
|
395
|
+
const db = getDatabase();
|
|
396
|
+
const row = db
|
|
397
|
+
.prepare('SELECT hub_channel_id FROM bot_settings WHERE app_id = ?')
|
|
398
|
+
.get(appId);
|
|
399
|
+
return { hub_channel_id: row?.hub_channel_id || null };
|
|
400
|
+
}
|
|
401
|
+
export function setBotSettings(appId, settings) {
|
|
402
|
+
const db = getDatabase();
|
|
403
|
+
const current = getBotSettings(appId);
|
|
404
|
+
const updated = { ...current, ...settings };
|
|
405
|
+
db.prepare(`INSERT INTO bot_settings (app_id, hub_channel_id, updated_at)
|
|
406
|
+
VALUES (?, ?, CURRENT_TIMESTAMP)
|
|
407
|
+
ON CONFLICT(app_id) DO UPDATE SET
|
|
408
|
+
hub_channel_id = ?, updated_at = CURRENT_TIMESTAMP`).run(appId, updated.hub_channel_id, updated.hub_channel_id);
|
|
409
|
+
}
|
|
410
|
+
export function getChannelDirectory(channelId) {
|
|
411
|
+
const db = getDatabase();
|
|
412
|
+
const row = db
|
|
413
|
+
.prepare('SELECT directory, app_id FROM channel_directories WHERE channel_id = ?')
|
|
414
|
+
.get(channelId);
|
|
415
|
+
if (!row) {
|
|
416
|
+
return undefined;
|
|
417
|
+
}
|
|
418
|
+
return {
|
|
419
|
+
directory: row.directory,
|
|
420
|
+
appId: row.app_id,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
424
|
+
// BOT TOKEN MANAGEMENT (Encrypted)
|
|
425
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
426
|
+
export function getBotToken(appId) {
|
|
427
|
+
const database = getDatabase();
|
|
428
|
+
const row = database
|
|
429
|
+
.prepare('SELECT token FROM bot_tokens WHERE app_id = ?')
|
|
430
|
+
.get(appId);
|
|
431
|
+
if (!row?.token) {
|
|
432
|
+
return undefined;
|
|
433
|
+
}
|
|
434
|
+
// Check if already encrypted
|
|
435
|
+
if (!isEncrypted(row.token)) {
|
|
436
|
+
// Auto-migrate: encrypt and update
|
|
437
|
+
const key = getEncryptionKey();
|
|
438
|
+
const encrypted = encrypt({ plaintext: row.token, key });
|
|
439
|
+
const serialized = serializeEncrypted(encrypted);
|
|
440
|
+
database
|
|
441
|
+
.prepare('UPDATE bot_tokens SET token = ? WHERE app_id = ?')
|
|
442
|
+
.run(serialized, appId);
|
|
443
|
+
return row.token;
|
|
444
|
+
}
|
|
445
|
+
// Decrypt
|
|
446
|
+
const data = deserializeEncrypted(row.token);
|
|
447
|
+
if (!data) {
|
|
448
|
+
return undefined;
|
|
449
|
+
}
|
|
450
|
+
try {
|
|
451
|
+
const key = getEncryptionKey();
|
|
452
|
+
return decrypt({ data, key });
|
|
453
|
+
}
|
|
454
|
+
catch {
|
|
455
|
+
return undefined;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
export function setBotToken(appId, token) {
|
|
459
|
+
const database = getDatabase();
|
|
460
|
+
const key = getEncryptionKey();
|
|
461
|
+
const encrypted = encrypt({ plaintext: token, key });
|
|
462
|
+
const serialized = serializeEncrypted(encrypted);
|
|
463
|
+
database
|
|
464
|
+
.prepare('INSERT OR REPLACE INTO bot_tokens (app_id, token) VALUES (?, ?)')
|
|
465
|
+
.run(appId, serialized);
|
|
466
|
+
}
|
|
467
|
+
export function getBotApiKeys(appId) {
|
|
468
|
+
const database = getDatabase();
|
|
469
|
+
const row = database
|
|
470
|
+
.prepare('SELECT gemini_api_key, xai_api_key FROM bot_api_keys WHERE app_id = ?')
|
|
471
|
+
.get(appId);
|
|
472
|
+
if (!row) {
|
|
473
|
+
return undefined;
|
|
474
|
+
}
|
|
475
|
+
const key = getEncryptionKey();
|
|
476
|
+
const decryptValue = (value) => {
|
|
477
|
+
if (!value) {
|
|
478
|
+
return undefined;
|
|
479
|
+
}
|
|
480
|
+
if (!isEncrypted(value)) {
|
|
481
|
+
// Auto-migrate on next setBotApiKeys call
|
|
482
|
+
return value;
|
|
483
|
+
}
|
|
484
|
+
const data = deserializeEncrypted(value);
|
|
485
|
+
if (!data) {
|
|
486
|
+
return undefined;
|
|
487
|
+
}
|
|
488
|
+
try {
|
|
489
|
+
return decrypt({ data, key });
|
|
490
|
+
}
|
|
491
|
+
catch {
|
|
492
|
+
return undefined;
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
return {
|
|
496
|
+
geminiApiKey: decryptValue(row.gemini_api_key),
|
|
497
|
+
xaiApiKey: decryptValue(row.xai_api_key),
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
export function setBotApiKeys(appId, keys) {
|
|
501
|
+
const database = getDatabase();
|
|
502
|
+
const encryptionKey = getEncryptionKey();
|
|
503
|
+
const encryptValue = (value) => {
|
|
504
|
+
if (!value) {
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
const encrypted = encrypt({ plaintext: value, key: encryptionKey });
|
|
508
|
+
return serializeEncrypted(encrypted);
|
|
509
|
+
};
|
|
510
|
+
database
|
|
511
|
+
.prepare('INSERT OR REPLACE INTO bot_api_keys (app_id, gemini_api_key, xai_api_key) VALUES (?, ?, ?)')
|
|
512
|
+
.run(appId, encryptValue(keys.geminiApiKey), encryptValue(keys.xaiApiKey));
|
|
513
|
+
}
|
|
514
|
+
export function closeDatabase() {
|
|
515
|
+
if (db) {
|
|
516
|
+
db.close();
|
|
517
|
+
db = null;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
export function runScheduleMigrations(database) {
|
|
521
|
+
const targetDb = database || getDatabase();
|
|
522
|
+
targetDb.exec(`
|
|
523
|
+
CREATE TABLE IF NOT EXISTS scheduled_messages (
|
|
524
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
525
|
+
channel_id TEXT NOT NULL,
|
|
526
|
+
thread_id TEXT,
|
|
527
|
+
prompt TEXT NOT NULL,
|
|
528
|
+
scheduled_at INTEGER NOT NULL,
|
|
529
|
+
created_by TEXT NOT NULL,
|
|
530
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
531
|
+
status TEXT DEFAULT 'pending',
|
|
532
|
+
error_message TEXT
|
|
533
|
+
)
|
|
534
|
+
`);
|
|
535
|
+
targetDb.exec(`
|
|
536
|
+
CREATE INDEX IF NOT EXISTS idx_scheduled_pending
|
|
537
|
+
ON scheduled_messages(status, scheduled_at)
|
|
538
|
+
WHERE status = 'pending'
|
|
539
|
+
`);
|
|
540
|
+
dbLogger.log('Schedule migrations complete');
|
|
541
|
+
}
|
|
542
|
+
export function createScheduledMessage({ channelId, threadId, prompt, scheduledAt, createdBy, }) {
|
|
543
|
+
const db = getDatabase();
|
|
544
|
+
const result = db
|
|
545
|
+
.prepare(`INSERT INTO scheduled_messages (channel_id, thread_id, prompt, scheduled_at, created_by)
|
|
546
|
+
VALUES (?, ?, ?, ?, ?)`)
|
|
547
|
+
.run(channelId, threadId || null, prompt, scheduledAt, createdBy);
|
|
548
|
+
return result.lastInsertRowid;
|
|
549
|
+
}
|
|
550
|
+
export function getPendingSchedules(beforeTime) {
|
|
551
|
+
const db = getDatabase();
|
|
552
|
+
const time = beforeTime || Date.now();
|
|
553
|
+
return db
|
|
554
|
+
.prepare(`SELECT * FROM scheduled_messages
|
|
555
|
+
WHERE status = 'pending' AND scheduled_at <= ?
|
|
556
|
+
ORDER BY scheduled_at ASC`)
|
|
557
|
+
.all(time);
|
|
558
|
+
}
|
|
559
|
+
export function getSchedulesByChannel(channelId) {
|
|
560
|
+
const db = getDatabase();
|
|
561
|
+
return db
|
|
562
|
+
.prepare(`SELECT * FROM scheduled_messages
|
|
563
|
+
WHERE (channel_id = ? OR thread_id = ?) AND status = 'pending'
|
|
564
|
+
ORDER BY scheduled_at ASC`)
|
|
565
|
+
.all(channelId, channelId);
|
|
566
|
+
}
|
|
567
|
+
export function getScheduleById(id) {
|
|
568
|
+
const db = getDatabase();
|
|
569
|
+
return db
|
|
570
|
+
.prepare('SELECT * FROM scheduled_messages WHERE id = ?')
|
|
571
|
+
.get(id);
|
|
572
|
+
}
|
|
573
|
+
export function updateScheduleStatus(id, status, errorMessage) {
|
|
574
|
+
const db = getDatabase();
|
|
575
|
+
db.prepare(`UPDATE scheduled_messages SET status = ?, error_message = ? WHERE id = ?`).run(status, errorMessage || null, id);
|
|
576
|
+
}
|
|
577
|
+
export function cancelSchedule(id, userId) {
|
|
578
|
+
const db = getDatabase();
|
|
579
|
+
const schedule = getScheduleById(id);
|
|
580
|
+
if (!schedule || schedule.status !== 'pending') {
|
|
581
|
+
return false;
|
|
582
|
+
}
|
|
583
|
+
updateScheduleStatus(id, 'cancelled');
|
|
584
|
+
return true;
|
|
585
|
+
}
|