zardbot-telegram 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.
Files changed (111) hide show
  1. package/.env.example +116 -0
  2. package/LICENSE +21 -0
  3. package/README.md +250 -0
  4. package/dist/agent/manager.js +88 -0
  5. package/dist/agent/types.js +26 -0
  6. package/dist/app/start-bot-app.js +49 -0
  7. package/dist/bot/commands/abort.js +121 -0
  8. package/dist/bot/commands/commands.js +480 -0
  9. package/dist/bot/commands/definitions.js +27 -0
  10. package/dist/bot/commands/help.js +10 -0
  11. package/dist/bot/commands/models.js +38 -0
  12. package/dist/bot/commands/new.js +70 -0
  13. package/dist/bot/commands/opencode-start.js +101 -0
  14. package/dist/bot/commands/opencode-stop.js +44 -0
  15. package/dist/bot/commands/projects.js +223 -0
  16. package/dist/bot/commands/rename.js +139 -0
  17. package/dist/bot/commands/sessions.js +351 -0
  18. package/dist/bot/commands/start.js +43 -0
  19. package/dist/bot/commands/status.js +95 -0
  20. package/dist/bot/commands/task.js +399 -0
  21. package/dist/bot/commands/tasklist.js +220 -0
  22. package/dist/bot/commands/voice.js +145 -0
  23. package/dist/bot/handlers/agent.js +118 -0
  24. package/dist/bot/handlers/context.js +100 -0
  25. package/dist/bot/handlers/document.js +65 -0
  26. package/dist/bot/handlers/inline-menu.js +119 -0
  27. package/dist/bot/handlers/model.js +143 -0
  28. package/dist/bot/handlers/permission.js +235 -0
  29. package/dist/bot/handlers/prompt.js +240 -0
  30. package/dist/bot/handlers/question.js +390 -0
  31. package/dist/bot/handlers/tts.js +89 -0
  32. package/dist/bot/handlers/variant.js +138 -0
  33. package/dist/bot/handlers/voice.js +173 -0
  34. package/dist/bot/index.js +977 -0
  35. package/dist/bot/message-patterns.js +4 -0
  36. package/dist/bot/middleware/auth.js +30 -0
  37. package/dist/bot/middleware/interaction-guard.js +95 -0
  38. package/dist/bot/middleware/unknown-command.js +22 -0
  39. package/dist/bot/streaming/response-streamer.js +286 -0
  40. package/dist/bot/streaming/tool-call-streamer.js +285 -0
  41. package/dist/bot/utils/busy-guard.js +15 -0
  42. package/dist/bot/utils/commands.js +21 -0
  43. package/dist/bot/utils/file-download.js +91 -0
  44. package/dist/bot/utils/finalize-assistant-response.js +52 -0
  45. package/dist/bot/utils/keyboard.js +69 -0
  46. package/dist/bot/utils/send-with-markdown-fallback.js +165 -0
  47. package/dist/bot/utils/telegram-text.js +28 -0
  48. package/dist/bot/utils/thinking-message.js +8 -0
  49. package/dist/cli/args.js +98 -0
  50. package/dist/cli.js +80 -0
  51. package/dist/config.js +97 -0
  52. package/dist/i18n/de.js +357 -0
  53. package/dist/i18n/en.js +357 -0
  54. package/dist/i18n/es.js +357 -0
  55. package/dist/i18n/fr.js +357 -0
  56. package/dist/i18n/index.js +109 -0
  57. package/dist/i18n/ru.js +357 -0
  58. package/dist/i18n/zh.js +357 -0
  59. package/dist/index.js +26 -0
  60. package/dist/interaction/busy.js +8 -0
  61. package/dist/interaction/cleanup.js +32 -0
  62. package/dist/interaction/guard.js +140 -0
  63. package/dist/interaction/manager.js +106 -0
  64. package/dist/interaction/types.js +1 -0
  65. package/dist/keyboard/manager.js +172 -0
  66. package/dist/keyboard/types.js +1 -0
  67. package/dist/model/capabilities.js +62 -0
  68. package/dist/model/context-limit.js +57 -0
  69. package/dist/model/manager.js +259 -0
  70. package/dist/model/types.js +24 -0
  71. package/dist/opencode/client.js +13 -0
  72. package/dist/opencode/events.js +140 -0
  73. package/dist/permission/manager.js +100 -0
  74. package/dist/permission/types.js +1 -0
  75. package/dist/pinned/format.js +29 -0
  76. package/dist/pinned/manager.js +682 -0
  77. package/dist/pinned/types.js +1 -0
  78. package/dist/process/manager.js +273 -0
  79. package/dist/process/types.js +1 -0
  80. package/dist/project/manager.js +88 -0
  81. package/dist/question/manager.js +176 -0
  82. package/dist/question/types.js +1 -0
  83. package/dist/rename/manager.js +53 -0
  84. package/dist/runtime/bootstrap.js +350 -0
  85. package/dist/runtime/mode.js +74 -0
  86. package/dist/runtime/paths.js +37 -0
  87. package/dist/scheduled-task/creation-manager.js +113 -0
  88. package/dist/scheduled-task/display.js +239 -0
  89. package/dist/scheduled-task/executor.js +87 -0
  90. package/dist/scheduled-task/foreground-state.js +32 -0
  91. package/dist/scheduled-task/next-run.js +207 -0
  92. package/dist/scheduled-task/runtime.js +368 -0
  93. package/dist/scheduled-task/schedule-parser.js +169 -0
  94. package/dist/scheduled-task/store.js +65 -0
  95. package/dist/scheduled-task/types.js +19 -0
  96. package/dist/session/cache-manager.js +455 -0
  97. package/dist/session/manager.js +10 -0
  98. package/dist/settings/manager.js +158 -0
  99. package/dist/stt/client.js +97 -0
  100. package/dist/summary/aggregator.js +1136 -0
  101. package/dist/summary/formatter.js +491 -0
  102. package/dist/summary/subagent-formatter.js +63 -0
  103. package/dist/summary/tool-message-batcher.js +90 -0
  104. package/dist/tts/client.js +130 -0
  105. package/dist/utils/error-format.js +29 -0
  106. package/dist/utils/logger.js +127 -0
  107. package/dist/utils/safe-background-task.js +33 -0
  108. package/dist/utils/telegram-rate-limit-retry.js +93 -0
  109. package/dist/variant/manager.js +103 -0
  110. package/dist/variant/types.js +1 -0
  111. package/package.json +79 -0
@@ -0,0 +1,455 @@
1
+ import { createHash } from "node:crypto";
2
+ import path from "node:path";
3
+ import Database from "better-sqlite3";
4
+ import { opencodeClient } from "../opencode/client.js";
5
+ import { getSessionDirectoryCache, setSessionDirectoryCache } from "../settings/manager.js";
6
+ import { logger } from "../utils/logger.js";
7
+ const CACHE_VERSION = 1;
8
+ const INITIAL_WARMUP_LIMIT = 1000;
9
+ const INCREMENTAL_SYNC_LIMIT = 1000;
10
+ const MAX_CACHED_DIRECTORIES = 10;
11
+ const SYNC_SAFETY_WINDOW_MS = 60_000;
12
+ const SYNC_COOLDOWN_MS = 60_000;
13
+ const STORAGE_FALLBACK_SCAN_LIMIT = 200;
14
+ const SQLITE_FALLBACK_QUERY_LIMIT = 200;
15
+ const SERVER_UNAVAILABLE_ERROR_MARKERS = [
16
+ "fetch failed",
17
+ "econnrefused",
18
+ "connection refused",
19
+ "connect refused",
20
+ ];
21
+ const EMPTY_CACHE = {
22
+ version: CACHE_VERSION,
23
+ lastSyncedUpdatedAt: 0,
24
+ directories: [],
25
+ };
26
+ function createEmptyCacheData() {
27
+ return {
28
+ version: EMPTY_CACHE.version,
29
+ lastSyncedUpdatedAt: EMPTY_CACHE.lastSyncedUpdatedAt,
30
+ directories: [],
31
+ };
32
+ }
33
+ let cacheData = createEmptyCacheData();
34
+ let cacheLoaded = false;
35
+ let syncInFlight = null;
36
+ let lastSyncAttemptAt = 0;
37
+ let persistQueue = Promise.resolve();
38
+ function worktreeKey(worktree) {
39
+ if (process.platform === "win32") {
40
+ return worktree.toLowerCase();
41
+ }
42
+ return worktree;
43
+ }
44
+ function isValidWorktree(worktree) {
45
+ const trimmed = worktree.trim();
46
+ return trimmed.length > 0 && trimmed !== "/";
47
+ }
48
+ function normalizeCacheData(raw) {
49
+ if (!raw || typeof raw !== "object") {
50
+ return createEmptyCacheData();
51
+ }
52
+ const value = raw;
53
+ const lastSyncedUpdatedAt = typeof value.lastSyncedUpdatedAt === "number" && Number.isFinite(value.lastSyncedUpdatedAt)
54
+ ? value.lastSyncedUpdatedAt
55
+ : 0;
56
+ const directories = Array.isArray(value.directories)
57
+ ? value.directories
58
+ .filter((item) => Boolean(item) &&
59
+ typeof item === "object" &&
60
+ typeof item.worktree === "string" &&
61
+ typeof item.lastUpdated === "number")
62
+ .map((item) => ({
63
+ worktree: item.worktree.trim(),
64
+ lastUpdated: item.lastUpdated,
65
+ }))
66
+ .filter((item) => isValidWorktree(item.worktree))
67
+ : [];
68
+ const data = {
69
+ version: CACHE_VERSION,
70
+ lastSyncedUpdatedAt,
71
+ directories,
72
+ };
73
+ dedupeAndTrimDirectories(data);
74
+ return data;
75
+ }
76
+ function dedupeAndTrimDirectories(data) {
77
+ const unique = new Map();
78
+ for (const item of data.directories) {
79
+ const key = worktreeKey(item.worktree);
80
+ const existing = unique.get(key);
81
+ if (!existing || existing.lastUpdated < item.lastUpdated) {
82
+ unique.set(key, item);
83
+ }
84
+ }
85
+ data.directories = Array.from(unique.values())
86
+ .sort((a, b) => b.lastUpdated - a.lastUpdated)
87
+ .slice(0, MAX_CACHED_DIRECTORIES);
88
+ }
89
+ async function ensureCacheLoaded() {
90
+ if (cacheLoaded) {
91
+ return;
92
+ }
93
+ const storedCache = getSessionDirectoryCache();
94
+ cacheData = normalizeCacheData(storedCache);
95
+ cacheLoaded = true;
96
+ logger.debug(`[SessionCache] Loaded ${cacheData.directories.length} directories from settings.sessionDirectoryCache`);
97
+ }
98
+ function queuePersist() {
99
+ persistQueue = persistQueue
100
+ .catch(() => {
101
+ // Keep queue chain alive if previous write failed.
102
+ })
103
+ .then(async () => {
104
+ try {
105
+ await setSessionDirectoryCache(cacheData);
106
+ }
107
+ catch (error) {
108
+ logger.error("[SessionCache] Failed to persist sessions cache", error);
109
+ }
110
+ });
111
+ return persistQueue;
112
+ }
113
+ function upsertDirectory(worktree, lastUpdated) {
114
+ if (!isValidWorktree(worktree)) {
115
+ return false;
116
+ }
117
+ const normalizedWorktree = worktree.trim();
118
+ const key = worktreeKey(normalizedWorktree);
119
+ const existingIndex = cacheData.directories.findIndex((item) => worktreeKey(item.worktree) === key);
120
+ if (existingIndex >= 0) {
121
+ const existing = cacheData.directories[existingIndex];
122
+ if (existing.lastUpdated >= lastUpdated) {
123
+ return false;
124
+ }
125
+ cacheData.directories[existingIndex] = {
126
+ worktree: existing.worktree,
127
+ lastUpdated,
128
+ };
129
+ }
130
+ else {
131
+ cacheData.directories.push({
132
+ worktree: normalizedWorktree,
133
+ lastUpdated,
134
+ });
135
+ }
136
+ dedupeAndTrimDirectories(cacheData);
137
+ return true;
138
+ }
139
+ function buildListParams() {
140
+ const hasWatermark = cacheData.lastSyncedUpdatedAt > 0;
141
+ if (!hasWatermark) {
142
+ return { limit: INITIAL_WARMUP_LIMIT };
143
+ }
144
+ return {
145
+ limit: INCREMENTAL_SYNC_LIMIT,
146
+ start: Math.max(0, cacheData.lastSyncedUpdatedAt - SYNC_SAFETY_WINDOW_MS),
147
+ };
148
+ }
149
+ function createVirtualProjectId(worktree) {
150
+ const hash = createHash("sha1").update(worktree).digest("hex").slice(0, 16);
151
+ return `dir_${hash}`;
152
+ }
153
+ function hasServerUnavailableMarker(value) {
154
+ const lower = value.toLowerCase();
155
+ return SERVER_UNAVAILABLE_ERROR_MARKERS.some((marker) => lower.includes(marker));
156
+ }
157
+ function isServerUnavailableError(error) {
158
+ const queue = [error];
159
+ const seen = new Set();
160
+ while (queue.length > 0) {
161
+ const current = queue.pop();
162
+ if (!current || seen.has(current)) {
163
+ continue;
164
+ }
165
+ seen.add(current);
166
+ if (typeof current === "string") {
167
+ if (hasServerUnavailableMarker(current)) {
168
+ return true;
169
+ }
170
+ continue;
171
+ }
172
+ if (current instanceof Error) {
173
+ if (hasServerUnavailableMarker(`${current.name}: ${current.message}`)) {
174
+ return true;
175
+ }
176
+ const errorWithCause = current;
177
+ if (errorWithCause.cause) {
178
+ queue.push(errorWithCause.cause);
179
+ }
180
+ continue;
181
+ }
182
+ if (typeof current === "object") {
183
+ const value = current;
184
+ if (typeof value.code === "string" && hasServerUnavailableMarker(value.code)) {
185
+ return true;
186
+ }
187
+ if (typeof value.message === "string" && hasServerUnavailableMarker(value.message)) {
188
+ return true;
189
+ }
190
+ if (value.cause) {
191
+ queue.push(value.cause);
192
+ }
193
+ }
194
+ }
195
+ return false;
196
+ }
197
+ async function runSync() {
198
+ await ensureCacheLoaded();
199
+ const params = buildListParams();
200
+ const { data: sessions, error } = await opencodeClient.session.list(params);
201
+ if (error || !sessions) {
202
+ throw error || new Error("No session list received from server");
203
+ }
204
+ let changed = false;
205
+ let maxUpdated = cacheData.lastSyncedUpdatedAt;
206
+ for (const session of sessions) {
207
+ const updatedAt = session.time?.updated ?? Date.now();
208
+ if (upsertDirectory(session.directory, updatedAt)) {
209
+ changed = true;
210
+ }
211
+ if (updatedAt > maxUpdated) {
212
+ maxUpdated = updatedAt;
213
+ }
214
+ }
215
+ if (maxUpdated !== cacheData.lastSyncedUpdatedAt) {
216
+ cacheData.lastSyncedUpdatedAt = maxUpdated;
217
+ changed = true;
218
+ }
219
+ if (changed) {
220
+ await queuePersist();
221
+ }
222
+ logger.debug(`[SessionCache] Synced sessions: fetched=${sessions.length}, directories=${cacheData.directories.length}, lastSyncedUpdatedAt=${cacheData.lastSyncedUpdatedAt}`);
223
+ }
224
+ function getStorageRootCandidates(pathInfo) {
225
+ const candidates = new Set();
226
+ if (pathInfo.home) {
227
+ candidates.add(path.join(pathInfo.home, ".local", "share", "opencode"));
228
+ }
229
+ if (pathInfo.state) {
230
+ const normalizedState = pathInfo.state.replace(/[\\/]+$/, "");
231
+ const lowerState = normalizedState.toLowerCase();
232
+ const marker = `${path.sep}state${path.sep}opencode`;
233
+ const lowerMarker = marker.toLowerCase();
234
+ if (lowerState.endsWith(lowerMarker)) {
235
+ const prefix = normalizedState.slice(0, normalizedState.length - marker.length);
236
+ candidates.add(path.join(prefix, "share", "opencode"));
237
+ }
238
+ }
239
+ return Array.from(candidates);
240
+ }
241
+ function getPathApi() {
242
+ return opencodeClient.path;
243
+ }
244
+ async function getStorageRootsFromApi() {
245
+ const pathApi = getPathApi();
246
+ if (!pathApi?.get) {
247
+ return [];
248
+ }
249
+ const { data: pathInfo, error } = await pathApi.get();
250
+ if (error || !pathInfo) {
251
+ return [];
252
+ }
253
+ return getStorageRootCandidates(pathInfo);
254
+ }
255
+ async function querySessionDirectoriesFromSqlite(dbPath) {
256
+ try {
257
+ const db = new Database(dbPath, {
258
+ readonly: true,
259
+ fileMustExist: true,
260
+ });
261
+ try {
262
+ const rows = db
263
+ .prepare(`
264
+ SELECT directory, MAX(time_updated) AS updated
265
+ FROM session
266
+ GROUP BY directory
267
+ ORDER BY updated DESC
268
+ LIMIT ?
269
+ `)
270
+ .all(SQLITE_FALLBACK_QUERY_LIMIT);
271
+ return rows
272
+ .filter((item) => Boolean(item) && typeof item.directory === "string")
273
+ .map((item) => ({
274
+ worktree: item.directory,
275
+ lastUpdated: typeof item.updated === "number" && Number.isFinite(item.updated) ? item.updated : 0,
276
+ }));
277
+ }
278
+ finally {
279
+ db.close();
280
+ }
281
+ }
282
+ catch (error) {
283
+ logger.debug(`[SessionCache] Failed to read sqlite fallback at ${dbPath}`, error);
284
+ }
285
+ return null;
286
+ }
287
+ async function ingestFromSqliteSessionDatabase() {
288
+ await ensureCacheLoaded();
289
+ const fs = await import("node:fs/promises");
290
+ const roots = await getStorageRootsFromApi();
291
+ for (const root of roots) {
292
+ const dbPath = path.join(root, "opencode.db");
293
+ try {
294
+ await fs.access(dbPath);
295
+ }
296
+ catch {
297
+ continue;
298
+ }
299
+ const rows = await querySessionDirectoriesFromSqlite(dbPath);
300
+ if (!rows || rows.length === 0) {
301
+ continue;
302
+ }
303
+ let changed = false;
304
+ let maxUpdated = cacheData.lastSyncedUpdatedAt;
305
+ for (const row of rows) {
306
+ if (upsertDirectory(row.worktree, row.lastUpdated)) {
307
+ changed = true;
308
+ }
309
+ if (row.lastUpdated > maxUpdated) {
310
+ maxUpdated = row.lastUpdated;
311
+ }
312
+ }
313
+ if (maxUpdated !== cacheData.lastSyncedUpdatedAt) {
314
+ cacheData.lastSyncedUpdatedAt = maxUpdated;
315
+ changed = true;
316
+ }
317
+ if (changed) {
318
+ await queuePersist();
319
+ }
320
+ logger.debug(`[SessionCache] SQLite fallback loaded: db=${dbPath}, rows=${rows.length}, directories=${cacheData.directories.length}`);
321
+ return;
322
+ }
323
+ }
324
+ async function ingestFromGlobalSessionStorage() {
325
+ await ensureCacheLoaded();
326
+ const fs = await import("node:fs/promises");
327
+ const candidates = await getStorageRootsFromApi();
328
+ for (const storageRoot of candidates) {
329
+ const globalDir = path.join(storageRoot, "storage", "session", "global");
330
+ try {
331
+ const entries = await fs.readdir(globalDir, { withFileTypes: true });
332
+ const sessionFiles = entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json"));
333
+ const withMtime = await Promise.all(sessionFiles.map(async (entry) => {
334
+ const fullPath = path.join(globalDir, entry.name);
335
+ const stat = await fs.stat(fullPath);
336
+ return { fullPath, mtimeMs: stat.mtimeMs };
337
+ }));
338
+ const sorted = withMtime
339
+ .sort((a, b) => b.mtimeMs - a.mtimeMs)
340
+ .slice(0, STORAGE_FALLBACK_SCAN_LIMIT);
341
+ let changed = false;
342
+ let maxUpdated = cacheData.lastSyncedUpdatedAt;
343
+ for (const file of sorted) {
344
+ try {
345
+ const raw = await fs.readFile(file.fullPath, "utf-8");
346
+ const session = JSON.parse(raw);
347
+ if (!session.directory) {
348
+ continue;
349
+ }
350
+ const updated = session.time?.updated ?? Math.trunc(file.mtimeMs);
351
+ if (upsertDirectory(session.directory, updated)) {
352
+ changed = true;
353
+ }
354
+ if (updated > maxUpdated) {
355
+ maxUpdated = updated;
356
+ }
357
+ }
358
+ catch {
359
+ // Ignore malformed session files.
360
+ }
361
+ }
362
+ if (maxUpdated !== cacheData.lastSyncedUpdatedAt) {
363
+ cacheData.lastSyncedUpdatedAt = maxUpdated;
364
+ changed = true;
365
+ }
366
+ if (changed) {
367
+ await queuePersist();
368
+ }
369
+ logger.debug(`[SessionCache] Storage fallback loaded: root=${storageRoot}, scanned=${sorted.length}, directories=${cacheData.directories.length}`);
370
+ return;
371
+ }
372
+ catch {
373
+ // Try next candidate path.
374
+ }
375
+ }
376
+ }
377
+ export async function warmupSessionDirectoryCache() {
378
+ await syncSessionDirectoryCache({ force: true });
379
+ try {
380
+ await ingestFromSqliteSessionDatabase();
381
+ }
382
+ catch (error) {
383
+ logger.warn("[SessionCache] Failed sqlite fallback warmup", error);
384
+ }
385
+ try {
386
+ await ingestFromGlobalSessionStorage();
387
+ }
388
+ catch (error) {
389
+ logger.warn("[SessionCache] Failed storage fallback warmup", error);
390
+ }
391
+ }
392
+ export async function syncSessionDirectoryCache(options) {
393
+ await ensureCacheLoaded();
394
+ if (!options?.force && Date.now() - lastSyncAttemptAt < SYNC_COOLDOWN_MS) {
395
+ return;
396
+ }
397
+ if (syncInFlight) {
398
+ return syncInFlight;
399
+ }
400
+ syncInFlight = runSync()
401
+ .then(() => {
402
+ lastSyncAttemptAt = Date.now();
403
+ })
404
+ .catch((error) => {
405
+ if (isServerUnavailableError(error)) {
406
+ logger.warn("[SessionCache] OpenCode server is not running. Start it with: opencode serve");
407
+ }
408
+ else {
409
+ logger.warn("[SessionCache] Failed to sync sessions cache", error);
410
+ }
411
+ lastSyncAttemptAt = 0;
412
+ })
413
+ .finally(() => {
414
+ syncInFlight = null;
415
+ });
416
+ return syncInFlight;
417
+ }
418
+ export async function getCachedSessionDirectories() {
419
+ await ensureCacheLoaded();
420
+ return cacheData.directories.map((item) => ({ ...item }));
421
+ }
422
+ export async function getCachedSessionProjects() {
423
+ const directories = await getCachedSessionDirectories();
424
+ return directories.map((item) => ({
425
+ id: createVirtualProjectId(item.worktree),
426
+ worktree: item.worktree,
427
+ name: item.worktree,
428
+ lastUpdated: item.lastUpdated,
429
+ }));
430
+ }
431
+ export async function upsertSessionDirectory(worktree, lastUpdated = Date.now()) {
432
+ await ensureCacheLoaded();
433
+ if (!upsertDirectory(worktree, lastUpdated)) {
434
+ return;
435
+ }
436
+ if (lastUpdated > cacheData.lastSyncedUpdatedAt) {
437
+ cacheData.lastSyncedUpdatedAt = lastUpdated;
438
+ }
439
+ await queuePersist();
440
+ }
441
+ export async function ingestSessionInfoForCache(session) {
442
+ const directory = session.directory;
443
+ if (!directory) {
444
+ return;
445
+ }
446
+ const updated = session.time?.updated ?? Date.now();
447
+ await upsertSessionDirectory(directory, updated);
448
+ }
449
+ export function __resetSessionDirectoryCacheForTests() {
450
+ cacheData = createEmptyCacheData();
451
+ cacheLoaded = false;
452
+ syncInFlight = null;
453
+ lastSyncAttemptAt = 0;
454
+ persistQueue = Promise.resolve();
455
+ }
@@ -0,0 +1,10 @@
1
+ import { getCurrentSession as getSettingsSession, setCurrentSession as setSettingsSession, clearSession as clearSettingsSession, } from "../settings/manager.js";
2
+ export function setCurrentSession(sessionInfo) {
3
+ setSettingsSession(sessionInfo);
4
+ }
5
+ export function getCurrentSession() {
6
+ return getSettingsSession() ?? null;
7
+ }
8
+ export function clearSession() {
9
+ clearSettingsSession();
10
+ }
@@ -0,0 +1,158 @@
1
+ import { cloneScheduledTask } from "../scheduled-task/types.js";
2
+ import path from "node:path";
3
+ import { getRuntimePaths } from "../runtime/paths.js";
4
+ import { logger } from "../utils/logger.js";
5
+ function cloneScheduledTasks(tasks) {
6
+ return tasks?.map((task) => cloneScheduledTask(task));
7
+ }
8
+ function getSettingsFilePath() {
9
+ return getRuntimePaths().settingsFilePath;
10
+ }
11
+ async function readSettingsFile() {
12
+ try {
13
+ const fs = await import("fs/promises");
14
+ const content = await fs.readFile(getSettingsFilePath(), "utf-8");
15
+ return JSON.parse(content);
16
+ }
17
+ catch (error) {
18
+ if (error.code !== "ENOENT") {
19
+ logger.error("[SettingsManager] Error reading settings file:", error);
20
+ }
21
+ return {};
22
+ }
23
+ }
24
+ let settingsWriteQueue = Promise.resolve();
25
+ function writeSettingsFile(settings) {
26
+ settingsWriteQueue = settingsWriteQueue
27
+ .catch(() => {
28
+ // Keep write queue alive after failed writes.
29
+ })
30
+ .then(async () => {
31
+ try {
32
+ const fs = await import("fs/promises");
33
+ const settingsFilePath = getSettingsFilePath();
34
+ await fs.mkdir(path.dirname(settingsFilePath), { recursive: true });
35
+ await fs.writeFile(settingsFilePath, JSON.stringify(settings, null, 2));
36
+ }
37
+ catch (err) {
38
+ logger.error("[SettingsManager] Error writing settings file:", err);
39
+ }
40
+ });
41
+ return settingsWriteQueue;
42
+ }
43
+ let currentSettings = {};
44
+ export function getCurrentProject() {
45
+ return currentSettings.currentProject;
46
+ }
47
+ export function setCurrentProject(projectInfo) {
48
+ currentSettings.currentProject = projectInfo;
49
+ void writeSettingsFile(currentSettings);
50
+ }
51
+ export function clearProject() {
52
+ currentSettings.currentProject = undefined;
53
+ void writeSettingsFile(currentSettings);
54
+ }
55
+ export function getCurrentSession() {
56
+ return currentSettings.currentSession;
57
+ }
58
+ export function setCurrentSession(sessionInfo) {
59
+ currentSettings.currentSession = sessionInfo;
60
+ void writeSettingsFile(currentSettings);
61
+ }
62
+ export function clearSession() {
63
+ currentSettings.currentSession = undefined;
64
+ void writeSettingsFile(currentSettings);
65
+ }
66
+ export function getCurrentAgent() {
67
+ return currentSettings.currentAgent;
68
+ }
69
+ export function setCurrentAgent(agentName) {
70
+ currentSettings.currentAgent = agentName;
71
+ void writeSettingsFile(currentSettings);
72
+ }
73
+ export function clearCurrentAgent() {
74
+ currentSettings.currentAgent = undefined;
75
+ void writeSettingsFile(currentSettings);
76
+ }
77
+ export function getCurrentModel() {
78
+ return currentSettings.currentModel;
79
+ }
80
+ export function setCurrentModel(modelInfo) {
81
+ currentSettings.currentModel = modelInfo;
82
+ void writeSettingsFile(currentSettings);
83
+ }
84
+ export function clearCurrentModel() {
85
+ currentSettings.currentModel = undefined;
86
+ void writeSettingsFile(currentSettings);
87
+ }
88
+ export function getCurrentTtsVoice() {
89
+ return currentSettings.currentTtsVoice;
90
+ }
91
+ export function setCurrentTtsVoice(voice) {
92
+ currentSettings.currentTtsVoice = voice;
93
+ void writeSettingsFile(currentSettings);
94
+ }
95
+ export function clearTtsVoice() {
96
+ currentSettings.currentTtsVoice = undefined;
97
+ void writeSettingsFile(currentSettings);
98
+ }
99
+ export function isTtsEnabled() {
100
+ return currentSettings.ttsEnabled ?? false;
101
+ }
102
+ export function setTtsEnabled(enabled) {
103
+ currentSettings.ttsEnabled = enabled;
104
+ void writeSettingsFile(currentSettings);
105
+ }
106
+ export function getPinnedMessageId() {
107
+ return currentSettings.pinnedMessageId;
108
+ }
109
+ export function setPinnedMessageId(messageId) {
110
+ currentSettings.pinnedMessageId = messageId;
111
+ void writeSettingsFile(currentSettings);
112
+ }
113
+ export function clearPinnedMessageId() {
114
+ currentSettings.pinnedMessageId = undefined;
115
+ void writeSettingsFile(currentSettings);
116
+ }
117
+ export function getServerProcess() {
118
+ return currentSettings.serverProcess;
119
+ }
120
+ export function setServerProcess(processInfo) {
121
+ currentSettings.serverProcess = processInfo;
122
+ void writeSettingsFile(currentSettings);
123
+ }
124
+ export function clearServerProcess() {
125
+ currentSettings.serverProcess = undefined;
126
+ void writeSettingsFile(currentSettings);
127
+ }
128
+ export function getSessionDirectoryCache() {
129
+ return currentSettings.sessionDirectoryCache;
130
+ }
131
+ export function setSessionDirectoryCache(cache) {
132
+ currentSettings.sessionDirectoryCache = cache;
133
+ return writeSettingsFile(currentSettings);
134
+ }
135
+ export function clearSessionDirectoryCache() {
136
+ currentSettings.sessionDirectoryCache = undefined;
137
+ void writeSettingsFile(currentSettings);
138
+ }
139
+ export function getScheduledTasks() {
140
+ return cloneScheduledTasks(currentSettings.scheduledTasks) ?? [];
141
+ }
142
+ export function setScheduledTasks(tasks) {
143
+ currentSettings.scheduledTasks = cloneScheduledTasks(tasks);
144
+ return writeSettingsFile(currentSettings);
145
+ }
146
+ export function __resetSettingsForTests() {
147
+ currentSettings = {};
148
+ settingsWriteQueue = Promise.resolve();
149
+ }
150
+ export async function loadSettings() {
151
+ const loadedSettings = (await readSettingsFile());
152
+ if ("toolMessagesIntervalSec" in loadedSettings) {
153
+ delete loadedSettings.toolMessagesIntervalSec;
154
+ void writeSettingsFile(loadedSettings);
155
+ }
156
+ currentSettings = loadedSettings;
157
+ currentSettings.scheduledTasks = cloneScheduledTasks(loadedSettings.scheduledTasks) ?? [];
158
+ }