agentgui 1.0.748 → 1.0.750

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/database.js CHANGED
@@ -2,7 +2,6 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import os from 'os';
4
4
  import { createRequire } from 'module';
5
- import { createACPQueries } from './acp-queries.js';
6
5
 
7
6
  const require = createRequire(import.meta.url);
8
7
 
@@ -626,1341 +625,8 @@ function generateId(prefix) {
626
625
  return `${prefix}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
627
626
  }
628
627
 
629
- export const queries = {
630
- _db: db,
631
- createConversation(agentType, title = null, workingDirectory = null, model = null, subAgent = null) {
632
- const id = generateId('conv');
633
- const now = Date.now();
634
- const stmt = prep(
635
- `INSERT INTO conversations (id, agentId, agentType, title, created_at, updated_at, status, workingDirectory, model, subAgent) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
636
- );
637
- stmt.run(id, agentType, agentType, title, now, now, 'active', workingDirectory, model, subAgent);
638
-
639
- return {
640
- id,
641
- agentType,
642
- title,
643
- workingDirectory,
644
- model,
645
- subAgent,
646
- created_at: now,
647
- updated_at: now,
648
- status: 'active'
649
- };
650
- },
651
-
652
- getConversation(id) {
653
- const stmt = prep('SELECT * FROM conversations WHERE id = ?');
654
- return stmt.get(id);
655
- },
656
-
657
- getAllConversations() {
658
- const stmt = prep('SELECT * FROM conversations WHERE status != ? ORDER BY updated_at DESC');
659
- return stmt.all('deleted');
660
- },
661
-
662
- getConversationsList() {
663
- const stmt = prep(
664
- 'SELECT id, agentId, title, agentType, created_at, updated_at, messageCount, workingDirectory, isStreaming, model, subAgent, pinned FROM conversations WHERE status != ? ORDER BY pinned DESC, updated_at DESC'
665
- );
666
- return stmt.all('deleted');
667
- },
668
-
669
- getConversations() {
670
- const stmt = prep('SELECT * FROM conversations WHERE status != ? ORDER BY pinned DESC, updated_at DESC');
671
- return stmt.all('deleted');
672
- },
673
-
674
- updateConversation(id, data) {
675
- const conv = this.getConversation(id);
676
- if (!conv) return null;
677
-
678
- const now = Date.now();
679
- const title = data.title !== undefined ? data.title : conv.title;
680
- const status = data.status !== undefined ? data.status : conv.status;
681
- const agentId = data.agentId !== undefined ? data.agentId : conv.agentId;
682
- const agentType = data.agentType !== undefined ? data.agentType : conv.agentType;
683
- const model = data.model !== undefined ? data.model : conv.model;
684
- const subAgent = data.subAgent !== undefined ? data.subAgent : conv.subAgent;
685
- const pinned = data.pinned !== undefined ? (data.pinned ? 1 : 0) : (conv.pinned || 0);
686
- const tags = data.tags !== undefined ? (Array.isArray(data.tags) ? data.tags.join(',') : data.tags) : (conv.tags || null);
687
-
688
- const stmt = prep(
689
- `UPDATE conversations SET title = ?, status = ?, agentId = ?, agentType = ?, model = ?, subAgent = ?, pinned = ?, tags = ?, updated_at = ? WHERE id = ?`
690
- );
691
- stmt.run(title, status, agentId, agentType, model, subAgent, pinned, tags, now, id);
692
-
693
- return {
694
- ...conv,
695
- title,
696
- status,
697
- agentId,
698
- agentType,
699
- model,
700
- subAgent,
701
- pinned,
702
- tags,
703
- updated_at: now
704
- };
705
- },
706
-
707
- setClaudeSessionId(conversationId, claudeSessionId, sessionId = null) {
708
- const stmt = prep('UPDATE conversations SET claudeSessionId = ?, updated_at = ? WHERE id = ?');
709
- stmt.run(claudeSessionId, Date.now(), conversationId);
710
- // Also track on the current AgentGUI session so we can clean up all sessions on delete
711
- if (sessionId) {
712
- prep('UPDATE sessions SET claudeSessionId = ? WHERE id = ?').run(claudeSessionId, sessionId);
713
- }
714
- },
715
-
716
- getClaudeSessionId(conversationId) {
717
- const stmt = prep('SELECT claudeSessionId FROM conversations WHERE id = ?');
718
- const row = stmt.get(conversationId);
719
- return row?.claudeSessionId || null;
720
- },
721
-
722
- setIsStreaming(conversationId, isStreaming) {
723
- const stmt = prep('UPDATE conversations SET isStreaming = ?, updated_at = ? WHERE id = ?');
724
- stmt.run(isStreaming ? 1 : 0, Date.now(), conversationId);
725
- },
726
-
727
- getIsStreaming(conversationId) {
728
- const stmt = prep('SELECT isStreaming FROM conversations WHERE id = ?');
729
- const row = stmt.get(conversationId);
730
- return row?.isStreaming === 1;
731
- },
732
-
733
- getStreamingConversations() {
734
- const stmt = prep('SELECT id, title, claudeSessionId, agentId, agentType, model, subAgent FROM conversations WHERE isStreaming = 1');
735
- return stmt.all();
736
- },
737
-
738
- getResumableConversations(withinMs = 600000) {
739
- // Get conversations with incomplete sessions that can be resumed.
740
- // Only includes sessions with status: active, pending, interrupted.
741
- // Excludes complete and error sessions - agents that finished should not be resumed.
742
- // Only resumes sessions that started within the last withinMs (default 10 minutes).
743
- const cutoff = Date.now() - withinMs;
744
- const stmt = prep(
745
- `SELECT DISTINCT c.id, c.title, c.claudeSessionId, c.agentId, c.agentType, c.workingDirectory, c.model, c.subAgent
746
- FROM conversations c
747
- WHERE EXISTS (
748
- SELECT 1 FROM sessions s
749
- WHERE s.conversationId = c.id
750
- AND s.status IN ('active', 'pending', 'interrupted')
751
- AND s.started_at > ?
752
- )`
753
- );
754
- return stmt.all(cutoff);
755
- },
756
-
757
- clearAllStreamingFlags() {
758
- const stmt = prep('UPDATE conversations SET isStreaming = 0 WHERE isStreaming = 1');
759
- return stmt.run().changes;
760
- },
761
-
762
- markSessionIncomplete(sessionId, errorMsg) {
763
- const stmt = prep('UPDATE sessions SET status = ?, error = ?, completed_at = ? WHERE id = ?');
764
- stmt.run('incomplete', errorMsg || 'unknown', Date.now(), sessionId);
765
- },
766
-
767
- getSessionsProcessingLongerThan(minutes) {
768
- const cutoff = Date.now() - (minutes * 60 * 1000);
769
- const stmt = prep("SELECT * FROM sessions WHERE status IN ('active', 'pending') AND started_at < ?");
770
- return stmt.all(cutoff);
771
- },
772
-
773
- cleanupOrphanedSessions(days) {
774
- const cutoff = Date.now() - (days * 24 * 60 * 60 * 1000);
775
- const stmt = prep("DELETE FROM sessions WHERE status IN ('active', 'pending') AND started_at < ?");
776
- const result = stmt.run(cutoff);
777
- return result.changes || 0;
778
- },
779
-
780
- createMessage(conversationId, role, content, idempotencyKey = null) {
781
- if (idempotencyKey) {
782
- const cached = this.getIdempotencyKey(idempotencyKey);
783
- if (cached) return JSON.parse(cached);
784
- }
785
-
786
- const id = generateId('msg');
787
- const now = Date.now();
788
- const storedContent = typeof content === 'string' ? content : JSON.stringify(content);
789
-
790
- const stmt = prep(
791
- `INSERT INTO messages (id, conversationId, role, content, created_at) VALUES (?, ?, ?, ?, ?)`
792
- );
793
- stmt.run(id, conversationId, role, storedContent, now);
794
-
795
- try { prep('INSERT INTO messages_fts(rowid, content, conversationId, role) VALUES ((SELECT rowid FROM messages WHERE id = ?), ?, ?, ?)').run(id, storedContent, conversationId, role); } catch (_) {}
796
-
797
- const updateConvStmt = prep('UPDATE conversations SET updated_at = ? WHERE id = ?');
798
- updateConvStmt.run(now, conversationId);
799
-
800
- const message = {
801
- id,
802
- conversationId,
803
- role,
804
- content,
805
- created_at: now
806
- };
807
-
808
- if (idempotencyKey) {
809
- this.setIdempotencyKey(idempotencyKey, message);
810
- }
811
-
812
- return message;
813
- },
814
-
815
- getMessage(id) {
816
- const stmt = prep('SELECT * FROM messages WHERE id = ?');
817
- const msg = stmt.get(id);
818
- if (msg && typeof msg.content === 'string') {
819
- try {
820
- msg.content = JSON.parse(msg.content);
821
- } catch (_) {
822
- // If it's not JSON, leave it as string
823
- }
824
- }
825
- return msg;
826
- },
827
-
828
- getConversationMessages(conversationId) {
829
- const stmt = prep(
830
- 'SELECT * FROM messages WHERE conversationId = ? ORDER BY created_at ASC'
831
- );
832
- const messages = stmt.all(conversationId);
833
- return messages.map(msg => {
834
- if (typeof msg.content === 'string') {
835
- try {
836
- msg.content = JSON.parse(msg.content);
837
- } catch (_) {
838
- // If it's not JSON, leave it as string
839
- }
840
- }
841
- return msg;
842
- });
843
- },
844
-
845
- getLastUserMessage(conversationId) {
846
- const stmt = prep(
847
- "SELECT * FROM messages WHERE conversationId = ? AND role = 'user' ORDER BY created_at DESC LIMIT 1"
848
- );
849
- const msg = stmt.get(conversationId);
850
- if (msg && typeof msg.content === 'string') {
851
- try { msg.content = JSON.parse(msg.content); } catch (_) {}
852
- }
853
- return msg || null;
854
- },
855
-
856
- getPaginatedMessages(conversationId, limit = 50, offset = 0) {
857
- const countStmt = prep('SELECT COUNT(*) as count FROM messages WHERE conversationId = ?');
858
- const total = countStmt.get(conversationId).count;
859
-
860
- const stmt = prep(
861
- 'SELECT * FROM messages WHERE conversationId = ? ORDER BY created_at ASC LIMIT ? OFFSET ?'
862
- );
863
- const messages = stmt.all(conversationId, limit, offset);
864
-
865
- return {
866
- messages: messages.map(msg => {
867
- if (typeof msg.content === 'string') {
868
- try {
869
- msg.content = JSON.parse(msg.content);
870
- } catch (_) {
871
- // If it's not JSON, leave it as string
872
- }
873
- }
874
- return msg;
875
- }),
876
- total,
877
- limit,
878
- offset,
879
- hasMore: offset + limit < total
880
- };
881
- },
882
-
883
- createSession(conversationId) {
884
- const id = generateId('sess');
885
- const now = Date.now();
886
-
887
- const stmt = prep(
888
- `INSERT INTO sessions (id, conversationId, status, started_at, completed_at, response, error) VALUES (?, ?, ?, ?, ?, ?, ?)`
889
- );
890
- stmt.run(id, conversationId, 'pending', now, null, null, null);
891
-
892
- return {
893
- id,
894
- conversationId,
895
- status: 'pending',
896
- started_at: now,
897
- completed_at: null,
898
- response: null,
899
- error: null
900
- };
901
- },
902
-
903
- getSession(id) {
904
- const stmt = prep('SELECT * FROM sessions WHERE id = ?');
905
- return stmt.get(id);
906
- },
907
-
908
- getConversationSessions(conversationId) {
909
- const stmt = prep(
910
- 'SELECT * FROM sessions WHERE conversationId = ? ORDER BY started_at DESC'
911
- );
912
- return stmt.all(conversationId);
913
- },
914
-
915
- updateSession(id, data) {
916
- const session = this.getSession(id);
917
- if (!session) return null;
918
-
919
- const status = data.status !== undefined ? data.status : session.status;
920
- const rawResponse = data.response !== undefined ? data.response : session.response;
921
- const response = rawResponse && typeof rawResponse === 'object' ? JSON.stringify(rawResponse) : rawResponse;
922
- const error = data.error !== undefined ? data.error : session.error;
923
- const completed_at = data.completed_at !== undefined ? data.completed_at : session.completed_at;
924
-
925
- const stmt = prep(
926
- `UPDATE sessions SET status = ?, response = ?, error = ?, completed_at = ? WHERE id = ?`
927
- );
928
-
929
- try {
930
- stmt.run(status, response, error, completed_at, id);
931
- return {
932
- ...session,
933
- status,
934
- response,
935
- error,
936
- completed_at
937
- };
938
- } catch (e) {
939
- throw e;
940
- }
941
- },
942
-
943
- getLatestSession(conversationId) {
944
- const stmt = prep(
945
- 'SELECT * FROM sessions WHERE conversationId = ? ORDER BY started_at DESC LIMIT 1'
946
- );
947
- return stmt.get(conversationId) || null;
948
- },
949
-
950
- getSessionsByStatus(conversationId, status) {
951
- const stmt = prep(
952
- 'SELECT * FROM sessions WHERE conversationId = ? AND status = ? ORDER BY started_at DESC'
953
- );
954
- return stmt.all(conversationId, status);
955
- },
956
-
957
- getActiveSessions() {
958
- const stmt = prep(
959
- "SELECT * FROM sessions WHERE status IN ('active', 'pending') ORDER BY started_at DESC"
960
- );
961
- return stmt.all();
962
- },
963
-
964
- getActiveSessionConversationIds() {
965
- const stmt = prep(
966
- "SELECT DISTINCT conversationId FROM sessions WHERE status IN ('active', 'pending')"
967
- );
968
- return stmt.all().map(r => r.conversationId);
969
- },
970
-
971
- getSessionsByConversation(conversationId, limit = 10, offset = 0) {
972
- const stmt = prep(
973
- 'SELECT * FROM sessions WHERE conversationId = ? ORDER BY started_at DESC LIMIT ? OFFSET ?'
974
- );
975
- return stmt.all(conversationId, limit, offset);
976
- },
977
-
978
- getAllSessions(limit = 100) {
979
- const stmt = prep(
980
- 'SELECT * FROM sessions ORDER BY started_at DESC LIMIT ?'
981
- );
982
- return stmt.all(limit);
983
- },
984
-
985
- deleteSession(id) {
986
- const stmt = prep('DELETE FROM sessions WHERE id = ?');
987
- const result = stmt.run(id);
988
- prep('DELETE FROM chunks WHERE sessionId = ?').run(id);
989
- prep('DELETE FROM events WHERE sessionId = ?').run(id);
990
- return result.changes || 0;
991
- },
992
-
993
- createEvent(type, data, conversationId = null, sessionId = null, idempotencyKey = null) {
994
- if (idempotencyKey) {
995
- const cached = this.getIdempotencyKey(idempotencyKey);
996
- if (cached) {
997
- console.log(`[event-idempotency] Event already exists for key ${idempotencyKey}, returning cached`);
998
- return JSON.parse(cached);
999
- }
1000
- }
1001
-
1002
- const id = generateId('evt');
1003
- const now = Date.now();
1004
-
1005
- const stmt = prep(
1006
- `INSERT INTO events (id, type, conversationId, sessionId, data, created_at) VALUES (?, ?, ?, ?, ?, ?)`
1007
- );
1008
- stmt.run(id, type, conversationId, sessionId, JSON.stringify(data), now);
1009
-
1010
- const event = {
1011
- id,
1012
- type,
1013
- conversationId,
1014
- sessionId,
1015
- data,
1016
- created_at: now
1017
- };
1018
-
1019
- if (idempotencyKey) {
1020
- this.setIdempotencyKey(idempotencyKey, event);
1021
- }
1022
-
1023
- return event;
1024
- },
1025
-
1026
- getEvent(id) {
1027
- const stmt = prep('SELECT * FROM events WHERE id = ?');
1028
- const row = stmt.get(id);
1029
- if (row) {
1030
- return {
1031
- ...row,
1032
- data: JSON.parse(row.data)
1033
- };
1034
- }
1035
- return undefined;
1036
- },
1037
-
1038
- getConversationEvents(conversationId) {
1039
- const stmt = prep(
1040
- 'SELECT * FROM events WHERE conversationId = ? ORDER BY created_at ASC'
1041
- );
1042
- const rows = stmt.all(conversationId);
1043
- return rows.map(row => ({
1044
- ...row,
1045
- data: JSON.parse(row.data)
1046
- }));
1047
- },
1048
-
1049
- getSessionEvents(sessionId) {
1050
- const stmt = prep(
1051
- 'SELECT * FROM events WHERE sessionId = ? ORDER BY created_at ASC'
1052
- );
1053
- const rows = stmt.all(sessionId);
1054
- return rows.map(row => ({
1055
- ...row,
1056
- data: JSON.parse(row.data)
1057
- }));
1058
- },
1059
-
1060
- deleteConversation(id) {
1061
- const conv = this.getConversation(id);
1062
- if (!conv) return false;
1063
-
1064
- // Delete all Claude Code session files for this conversation (all executions)
1065
- const sessionClaudeIds = prep('SELECT DISTINCT claudeSessionId FROM sessions WHERE conversationId = ? AND claudeSessionId IS NOT NULL').all(id).map(r => r.claudeSessionId);
1066
- // Also include the current claudeSessionId on the conversation record
1067
- if (conv.claudeSessionId && !sessionClaudeIds.includes(conv.claudeSessionId)) {
1068
- sessionClaudeIds.push(conv.claudeSessionId);
1069
- }
1070
- for (const csid of sessionClaudeIds) {
1071
- this.deleteClaudeSessionFile(csid);
1072
- }
1073
-
1074
- const deleteStmt = db.transaction(() => {
1075
- const sessionIds = prep('SELECT id FROM sessions WHERE conversationId = ?').all(id).map(r => r.id);
1076
- prep('DELETE FROM stream_updates WHERE conversationId = ?').run(id);
1077
- prep('DELETE FROM chunks WHERE conversationId = ?').run(id);
1078
- prep('DELETE FROM events WHERE conversationId = ?').run(id);
1079
- if (sessionIds.length > 0) {
1080
- const placeholders = sessionIds.map(() => '?').join(',');
1081
- db.prepare(`DELETE FROM stream_updates WHERE sessionId IN (${placeholders})`).run(...sessionIds);
1082
- db.prepare(`DELETE FROM chunks WHERE sessionId IN (${placeholders})`).run(...sessionIds);
1083
- db.prepare(`DELETE FROM events WHERE sessionId IN (${placeholders})`).run(...sessionIds);
1084
- }
1085
- prep('DELETE FROM sessions WHERE conversationId = ?').run(id);
1086
- prep('DELETE FROM messages WHERE conversationId = ?').run(id);
1087
- prep('UPDATE conversations SET status = ?, updated_at = ? WHERE id = ?').run('deleted', Date.now(), id);
1088
- });
1089
-
1090
- deleteStmt();
1091
- return true;
1092
- },
1093
-
1094
- deleteClaudeSessionFile(sessionId) {
1095
- try {
1096
- const claudeDir = path.join(os.homedir(), '.claude');
1097
- const projectsDir = path.join(claudeDir, 'projects');
1098
-
1099
- if (!fs.existsSync(projectsDir)) {
1100
- return false;
1101
- }
1102
-
1103
- // Search for session file in all project directories
1104
- const projects = fs.readdirSync(projectsDir);
1105
- for (const project of projects) {
1106
- const projectPath = path.join(projectsDir, project);
1107
- const sessionFile = path.join(projectPath, `${sessionId}.jsonl`);
1108
-
1109
- if (fs.existsSync(sessionFile)) {
1110
- fs.unlinkSync(sessionFile);
1111
- console.log(`[deleteClaudeSessionFile] Deleted Claude session file: ${sessionFile}`);
1112
-
1113
- // Also remove the entry from sessions-index.json if it exists
1114
- const indexPath = path.join(projectPath, 'sessions-index.json');
1115
- if (fs.existsSync(indexPath)) {
1116
- try {
1117
- const indexContent = fs.readFileSync(indexPath, 'utf8');
1118
- const index = JSON.parse(indexContent);
1119
- if (index.entries && Array.isArray(index.entries)) {
1120
- const originalLength = index.entries.length;
1121
- index.entries = index.entries.filter(entry => entry.sessionId !== sessionId);
1122
- if (index.entries.length < originalLength) {
1123
- fs.writeFileSync(indexPath, JSON.stringify(index, null, 2), { encoding: 'utf8' });
1124
- console.log(`[deleteClaudeSessionFile] Removed session ${sessionId} from sessions-index.json in ${projectPath}`);
1125
- }
1126
- }
1127
- } catch (indexErr) {
1128
- console.error(`[deleteClaudeSessionFile] Failed to update sessions-index.json in ${projectPath}:`, indexErr.message);
1129
- }
1130
- }
1131
-
1132
- // Also delete the session subdirectory (contains subagents/, tool-results/)
1133
- const sessionDir = path.join(projectPath, sessionId);
1134
- if (fs.existsSync(sessionDir)) {
1135
- try {
1136
- fs.rmSync(sessionDir, { recursive: true, force: true });
1137
- console.log(`[deleteClaudeSessionFile] Deleted Claude session dir: ${sessionDir}`);
1138
- } catch (dirErr) {
1139
- console.error(`[deleteClaudeSessionFile] Failed to delete session dir ${sessionDir}:`, dirErr.message);
1140
- }
1141
- }
1142
-
1143
- return true;
1144
- }
1145
- }
1146
-
1147
- return false;
1148
- } catch (err) {
1149
- console.error(`[deleteClaudeSessionFile] Error deleting session ${sessionId}:`, err.message);
1150
- return false;
1151
- }
1152
- },
1153
-
1154
- deleteAllConversations() {
1155
- try {
1156
- // Delete all Claude session files tracked per-session and per-conversation
1157
- const allClaudeSessionIds = prep('SELECT DISTINCT claudeSessionId FROM sessions WHERE claudeSessionId IS NOT NULL').all().map(r => r.claudeSessionId);
1158
- const convClaudeIds = prep('SELECT DISTINCT claudeSessionId FROM conversations WHERE claudeSessionId IS NOT NULL').all().map(r => r.claudeSessionId);
1159
- for (const csid of convClaudeIds) {
1160
- if (!allClaudeSessionIds.includes(csid)) allClaudeSessionIds.push(csid);
1161
- }
1162
- for (const csid of allClaudeSessionIds) {
1163
- this.deleteClaudeSessionFile(csid);
1164
- }
1165
-
1166
- const deleteAllStmt = db.transaction(() => {
1167
- prep('DELETE FROM stream_updates').run();
1168
- prep('DELETE FROM chunks').run();
1169
- prep('DELETE FROM events').run();
1170
- prep('DELETE FROM voice_cache').run();
1171
- prep('DELETE FROM sessions').run();
1172
- prep('DELETE FROM messages').run();
1173
- prep('DELETE FROM conversations').run();
1174
- });
1175
-
1176
- deleteAllStmt();
1177
- const projectsDir = path.join(os.homedir(), '.claude', 'projects');
1178
- if (fs.existsSync(projectsDir)) {
1179
- for (const project of fs.readdirSync(projectsDir)) {
1180
- const pdir = path.join(projectsDir, project);
1181
- try {
1182
- if (!fs.statSync(pdir).isDirectory()) continue;
1183
- for (const entry of fs.readdirSync(pdir, { withFileTypes: true })) {
1184
- if (entry.isFile() && entry.name.endsWith('.jsonl')) {
1185
- fs.unlinkSync(path.join(pdir, entry.name));
1186
- } else if (entry.isDirectory()) {
1187
- fs.rmSync(path.join(pdir, entry.name), { recursive: true, force: true });
1188
- }
1189
- }
1190
- } catch (err) {
1191
- console.error('[deleteAllConversations] Failed to clean project dir:', pdir, err.message);
1192
- }
1193
- }
1194
- }
1195
- console.log('[deleteAllConversations] Deleted all conversations and associated Claude Code files');
1196
- return true;
1197
- } catch (err) {
1198
- console.error('[deleteAllConversations] Error deleting all conversations:', err.message);
1199
- return false;
1200
- }
1201
- },
1202
-
1203
- cleanup() {
1204
- const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000);
1205
- const now = Date.now();
1206
-
1207
- const cleanupStmt = db.transaction(() => {
1208
- prep('DELETE FROM events WHERE created_at < ?').run(thirtyDaysAgo);
1209
- prep('DELETE FROM sessions WHERE completed_at IS NOT NULL AND completed_at < ?').run(thirtyDaysAgo);
1210
- prep('DELETE FROM idempotencyKeys WHERE (created_at + ttl) < ?').run(now);
1211
- });
1212
-
1213
- cleanupStmt();
1214
- },
1215
-
1216
- setIdempotencyKey(key, value) {
1217
- const now = Date.now();
1218
- const ttl = 24 * 60 * 60 * 1000;
1219
-
1220
- const stmt = prep(
1221
- 'INSERT OR REPLACE INTO idempotencyKeys (key, value, created_at, ttl) VALUES (?, ?, ?, ?)'
1222
- );
1223
- stmt.run(key, JSON.stringify(value), now, ttl);
1224
- },
1225
-
1226
- getIdempotencyKey(key) {
1227
- const stmt = prep('SELECT * FROM idempotencyKeys WHERE key = ?');
1228
- const entry = stmt.get(key);
1229
-
1230
- if (!entry) return null;
1231
-
1232
- const isExpired = Date.now() - entry.created_at > entry.ttl;
1233
- if (isExpired) {
1234
- db.run('DELETE FROM idempotencyKeys WHERE key = ?', [key]);
1235
- return null;
1236
- }
1237
-
1238
- return entry.value;
1239
- },
1240
-
1241
- clearIdempotencyKey(key) {
1242
- db.run('DELETE FROM idempotencyKeys WHERE key = ?', [key]);
1243
- },
1244
-
1245
- discoverClaudeCodeConversations() {
1246
- const projectsDir = path.join(os.homedir(), '.claude', 'projects');
1247
- if (!fs.existsSync(projectsDir)) return [];
1248
-
1249
- const discovered = [];
1250
- try {
1251
- const dirs = fs.readdirSync(projectsDir, { withFileTypes: true });
1252
- for (const dir of dirs) {
1253
- if (!dir.isDirectory()) continue;
1254
- const dirPath = path.join(projectsDir, dir.name);
1255
- const indexPath = path.join(dirPath, 'sessions-index.json');
1256
- if (!fs.existsSync(indexPath)) continue;
1257
-
1258
- try {
1259
- const index = JSON.parse(fs.readFileSync(indexPath, 'utf-8'));
1260
- const projectPath = index.originalPath || dir.name.replace(/^-/, '/').replace(/-/g, '/');
1261
- for (const entry of (index.entries || [])) {
1262
- if (!entry.sessionId || entry.messageCount === 0) continue;
1263
- discovered.push({
1264
- id: entry.sessionId,
1265
- jsonlPath: entry.fullPath || path.join(dirPath, `${entry.sessionId}.jsonl`),
1266
- title: entry.summary || entry.firstPrompt || 'Claude Code Session',
1267
- projectPath,
1268
- created: entry.created ? new Date(entry.created).getTime() : entry.fileMtime,
1269
- modified: entry.modified ? new Date(entry.modified).getTime() : entry.fileMtime,
1270
- messageCount: entry.messageCount,
1271
- gitBranch: entry.gitBranch,
1272
- source: 'claude-code'
1273
- });
1274
- }
1275
- } catch (e) {
1276
- console.error(`Error reading index ${indexPath}:`, e.message);
1277
- }
1278
- }
1279
- } catch (e) {
1280
- console.error('Error discovering Claude Code conversations:', e.message);
1281
- }
1282
-
1283
- return discovered;
1284
- },
1285
-
1286
- parseJsonlMessages(jsonlPath) {
1287
- if (!fs.existsSync(jsonlPath)) return [];
1288
- const messages = [];
1289
- try {
1290
- const lines = fs.readFileSync(jsonlPath, 'utf-8').split('\n');
1291
- for (const line of lines) {
1292
- if (!line.trim()) continue;
1293
- try {
1294
- const obj = JSON.parse(line);
1295
- if (obj.type === 'user' && obj.message?.content) {
1296
- const content = typeof obj.message.content === 'string'
1297
- ? obj.message.content
1298
- : Array.isArray(obj.message.content)
1299
- ? obj.message.content.filter(c => c.type === 'text').map(c => c.text).join('\n')
1300
- : JSON.stringify(obj.message.content);
1301
- if (content && !content.startsWith('[{"tool_use_id"')) {
1302
- messages.push({ id: obj.uuid || generateId('msg'), role: 'user', content, created_at: new Date(obj.timestamp).getTime() });
1303
- }
1304
- } else if (obj.type === 'assistant' && obj.message?.content) {
1305
- let text = '';
1306
- const content = obj.message.content;
1307
- if (Array.isArray(content)) {
1308
- // CRITICAL FIX: Join text blocks with newlines to preserve separation
1309
- const textBlocks = [];
1310
- for (const c of content) {
1311
- if (c.type === 'text' && c.text) {
1312
- textBlocks.push(c.text);
1313
- }
1314
- }
1315
- // Join with double newline to preserve logical separation
1316
- text = textBlocks.join('\n\n');
1317
- } else if (typeof content === 'string') {
1318
- text = content;
1319
- }
1320
- if (text) {
1321
- messages.push({ id: obj.uuid || generateId('msg'), role: 'assistant', content: text, created_at: new Date(obj.timestamp).getTime() });
1322
- }
1323
- }
1324
- } catch (_) {}
1325
- }
1326
- } catch (e) {
1327
- console.error(`Error parsing JSONL ${jsonlPath}:`, e.message);
1328
- }
1329
- return messages;
1330
- },
1331
-
1332
- importClaudeCodeConversations() {
1333
- const discovered = this.discoverClaudeCodeConversations();
1334
- const imported = [];
1335
-
1336
- for (const conv of discovered) {
1337
- try {
1338
- const existingConv = prep('SELECT id, status FROM conversations WHERE id = ? OR externalId = ?').get(conv.id, conv.id);
1339
- if (existingConv) {
1340
- imported.push({ id: conv.id, status: 'skipped', reason: existingConv.status === 'deleted' ? 'deleted' : 'exists' });
1341
- continue;
1342
- }
1343
-
1344
- const projectName = conv.projectPath ? path.basename(conv.projectPath) : '';
1345
- const title = conv.title || 'Claude Code Session';
1346
- const displayTitle = projectName ? `[${projectName}] ${title}` : title;
1347
-
1348
- const messages = this.parseJsonlMessages(conv.jsonlPath);
1349
-
1350
- const importStmt = db.transaction(() => {
1351
- prep(
1352
- `INSERT INTO conversations (id, agentId, title, created_at, updated_at, status, claudeSessionId) VALUES (?, ?, ?, ?, ?, ?, ?)`
1353
- ).run(conv.id, 'claude-code', displayTitle, conv.created, conv.modified, 'active', conv.id);
1354
-
1355
- for (const msg of messages) {
1356
- try {
1357
- prep(
1358
- `INSERT INTO messages (id, conversationId, role, content, created_at) VALUES (?, ?, ?, ?, ?)`
1359
- ).run(msg.id, conv.id, msg.role, msg.content, msg.created_at);
1360
- } catch (_) {}
1361
- }
1362
- });
1363
-
1364
- importStmt();
1365
- imported.push({ id: conv.id, status: 'imported', title: displayTitle, messages: messages.length });
1366
- } catch (e) {
1367
- imported.push({ id: conv.id, status: 'error', error: e.message });
1368
- }
1369
- }
1370
-
1371
- return imported;
1372
- },
1373
-
1374
- createStreamUpdate(sessionId, conversationId, updateType, content) {
1375
- const id = generateId('upd');
1376
- const now = Date.now();
1377
-
1378
- // Use transaction to ensure atomic sequence number assignment
1379
- const transaction = db.transaction(() => {
1380
- const maxSequence = prep(
1381
- 'SELECT MAX(sequence) as max FROM stream_updates WHERE sessionId = ?'
1382
- ).get(sessionId);
1383
- const sequence = (maxSequence?.max || -1) + 1;
1384
-
1385
- prep(
1386
- `INSERT INTO stream_updates (id, sessionId, conversationId, updateType, content, sequence, created_at)
1387
- VALUES (?, ?, ?, ?, ?, ?, ?)`
1388
- ).run(id, sessionId, conversationId, updateType, JSON.stringify(content), sequence, now);
1389
-
1390
- return sequence;
1391
- });
1392
-
1393
- const sequence = transaction();
1394
-
1395
- return {
1396
- id,
1397
- sessionId,
1398
- conversationId,
1399
- updateType,
1400
- content,
1401
- sequence,
1402
- created_at: now
1403
- };
1404
- },
1405
-
1406
- getSessionStreamUpdates(sessionId) {
1407
- const stmt = prep(
1408
- `SELECT id, sessionId, conversationId, updateType, content, sequence, created_at
1409
- FROM stream_updates WHERE sessionId = ? ORDER BY sequence ASC`
1410
- );
1411
- const rows = stmt.all(sessionId);
1412
- return rows.map(row => ({
1413
- ...row,
1414
- content: JSON.parse(row.content)
1415
- }));
1416
- },
1417
-
1418
- clearSessionStreamUpdates(sessionId) {
1419
- const stmt = prep('DELETE FROM stream_updates WHERE sessionId = ?');
1420
- stmt.run(sessionId);
1421
- },
1422
-
1423
- createImportedConversation(data) {
1424
- const id = generateId('conv');
1425
- const now = Date.now();
1426
- const stmt = prep(
1427
- `INSERT INTO conversations (
1428
- id, agentId, title, created_at, updated_at, status,
1429
- agentType, source, externalId, firstPrompt, messageCount,
1430
- projectPath, gitBranch, sourcePath, lastSyncedAt
1431
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
1432
- );
1433
- stmt.run(
1434
- id,
1435
- data.externalId || id,
1436
- data.title,
1437
- data.created || now,
1438
- data.modified || now,
1439
- 'active',
1440
- data.agentType || 'claude-code',
1441
- data.source || 'imported',
1442
- data.externalId,
1443
- data.firstPrompt,
1444
- data.messageCount || 0,
1445
- data.projectPath,
1446
- data.gitBranch,
1447
- data.sourcePath,
1448
- now
1449
- );
1450
- return { id, ...data };
1451
- },
1452
-
1453
- getConversationByExternalId(agentType, externalId) {
1454
- const stmt = prep(
1455
- 'SELECT * FROM conversations WHERE agentType = ? AND externalId = ?'
1456
- );
1457
- return stmt.get(agentType, externalId);
1458
- },
1459
-
1460
- getConversationsByAgentType(agentType) {
1461
- const stmt = prep(
1462
- 'SELECT * FROM conversations WHERE agentType = ? AND status != ? ORDER BY updated_at DESC'
1463
- );
1464
- return stmt.all(agentType, 'deleted');
1465
- },
1466
-
1467
- getImportedConversations() {
1468
- const stmt = prep(
1469
- 'SELECT * FROM conversations WHERE source = ? AND status != ? ORDER BY updated_at DESC'
1470
- );
1471
- return stmt.all('imported', 'deleted');
1472
- },
1473
-
1474
- createChunk(sessionId, conversationId, sequence, type, data) {
1475
- const id = generateId('chunk');
1476
- const now = Date.now();
1477
- const dataBlob = typeof data === 'string' ? data : JSON.stringify(data);
1478
-
1479
- const stmt = prep(
1480
- `INSERT INTO chunks (id, sessionId, conversationId, sequence, type, data, created_at)
1481
- VALUES (?, ?, ?, ?, ?, ?, ?)`
1482
- );
1483
- stmt.run(id, sessionId, conversationId, sequence, type, dataBlob, now);
1484
-
1485
- return {
1486
- id,
1487
- sessionId,
1488
- conversationId,
1489
- sequence,
1490
- type,
1491
- data,
1492
- created_at: now
1493
- };
1494
- },
1495
-
1496
- getChunk(id) {
1497
- const stmt = prep(
1498
- `SELECT id, sessionId, conversationId, sequence, type, data, created_at FROM chunks WHERE id = ?`
1499
- );
1500
- const row = stmt.get(id);
1501
- if (!row) return null;
1502
-
1503
- try {
1504
- return {
1505
- ...row,
1506
- data: typeof row.data === 'string' ? JSON.parse(row.data) : row.data
1507
- };
1508
- } catch (e) {
1509
- return row;
1510
- }
1511
- },
1512
-
1513
- getSessionChunks(sessionId) {
1514
- const stmt = prep(
1515
- `SELECT id, sessionId, conversationId, sequence, type, data, created_at
1516
- FROM chunks WHERE sessionId = ? ORDER BY sequence ASC`
1517
- );
1518
- const rows = stmt.all(sessionId);
1519
- return rows.map(row => {
1520
- try {
1521
- return {
1522
- ...row,
1523
- data: typeof row.data === 'string' ? JSON.parse(row.data) : row.data
1524
- };
1525
- } catch (e) {
1526
- return row;
1527
- }
1528
- });
1529
- },
1530
-
1531
- getConversationChunkCount(conversationId) {
1532
- const stmt = prep('SELECT COUNT(*) as count FROM chunks WHERE conversationId = ?');
1533
- return stmt.get(conversationId).count;
1534
- },
1535
-
1536
- getConversationChunks(conversationId) {
1537
- const stmt = prep(
1538
- `SELECT id, sessionId, conversationId, sequence, type, data, created_at
1539
- FROM chunks WHERE conversationId = ? ORDER BY created_at ASC`
1540
- );
1541
- const rows = stmt.all(conversationId);
1542
- return rows.map(row => {
1543
- try {
1544
- return {
1545
- ...row,
1546
- data: typeof row.data === 'string' ? JSON.parse(row.data) : row.data
1547
- };
1548
- } catch (e) {
1549
- return row;
1550
- }
1551
- });
1552
- },
1553
-
1554
- getConversationChunksSince(conversationId, since) {
1555
- const stmt = prep(
1556
- `SELECT id, sessionId, conversationId, sequence, type, data, created_at
1557
- FROM chunks WHERE conversationId = ? AND created_at > ? ORDER BY created_at ASC`
1558
- );
1559
- const rows = stmt.all(conversationId, since);
1560
- return rows.map(row => {
1561
- try { return { ...row, data: typeof row.data === 'string' ? JSON.parse(row.data) : row.data }; }
1562
- catch (e) { return row; }
1563
- });
1564
- },
1565
-
1566
- getRecentConversationChunks(conversationId, limit = 500) {
1567
- const sessions = prep(
1568
- `SELECT sessionId, COUNT(*) as n FROM chunks WHERE conversationId = ?
1569
- GROUP BY sessionId ORDER BY MAX(created_at) DESC`
1570
- ).all(conversationId);
1571
- if (!sessions.length) return [];
1572
- const included = [];
1573
- let total = 0;
1574
- for (const s of sessions) {
1575
- if (total + s.n > limit && included.length > 0) break;
1576
- included.unshift(s.sessionId);
1577
- total += s.n;
1578
- }
1579
- const placeholders = included.map(() => '?').join(',');
1580
- const rows = prep(
1581
- `SELECT id, sessionId, conversationId, sequence, type, data, created_at
1582
- FROM chunks WHERE conversationId = ? AND sessionId IN (${placeholders})
1583
- ORDER BY created_at ASC`
1584
- ).all(conversationId, ...included);
1585
- return rows.map(row => {
1586
- try { return { ...row, data: typeof row.data === 'string' ? JSON.parse(row.data) : row.data }; }
1587
- catch (e) { return row; }
1588
- });
1589
- },
1590
-
1591
- getRecentMessages(conversationId, limit = 20) {
1592
- const countStmt = prep('SELECT COUNT(*) as count FROM messages WHERE conversationId = ?');
1593
- const total = countStmt.get(conversationId).count;
1594
-
1595
- const stmt = prep(
1596
- 'SELECT * FROM messages WHERE conversationId = ? ORDER BY created_at DESC LIMIT ? '
1597
- );
1598
- const messages = stmt.all(conversationId, limit).reverse();
1599
-
1600
- return {
1601
- messages: messages.map(msg => {
1602
- if (typeof msg.content === 'string') {
1603
- try {
1604
- msg.content = JSON.parse(msg.content);
1605
- } catch (_) {}
1606
- }
1607
- return msg;
1608
- }),
1609
- total,
1610
- limit,
1611
- offset: Math.max(0, total - limit),
1612
- hasMore: total > limit
1613
- };
1614
- },
1615
-
1616
- getMessagesBefore(conversationId, beforeId, limit = 50) {
1617
- const countStmt = prep('SELECT COUNT(*) as count FROM messages WHERE conversationId = ?');
1618
- const total = countStmt.get(conversationId).count;
1619
-
1620
- const stmt = prep(`
1621
- SELECT * FROM messages
1622
- WHERE conversationId = ? AND id < (SELECT id FROM messages WHERE id = ?)
1623
- ORDER BY created_at DESC LIMIT ?
1624
- `);
1625
- const messages = stmt.all(conversationId, beforeId, limit).reverse();
1626
-
1627
- return {
1628
- messages: messages.map(msg => {
1629
- if (typeof msg.content === 'string') {
1630
- try {
1631
- msg.content = JSON.parse(msg.content);
1632
- } catch (_) {}
1633
- }
1634
- return msg;
1635
- }),
1636
- total,
1637
- limit,
1638
- hasMore: total > (limit + 1)
1639
- };
1640
- },
1641
-
1642
- getChunksBefore(conversationId, beforeTimestamp, limit = 500) {
1643
- const countStmt = prep('SELECT COUNT(*) as count FROM chunks WHERE conversationId = ?');
1644
- const total = countStmt.get(conversationId).count;
1645
-
1646
- const stmt = prep(`
1647
- SELECT id, sessionId, conversationId, sequence, type, data, created_at
1648
- FROM chunks
1649
- WHERE conversationId = ? AND created_at < ?
1650
- ORDER BY created_at DESC LIMIT ?
1651
- `);
1652
- const rows = stmt.all(conversationId, beforeTimestamp, limit);
1653
- rows.reverse();
1654
-
1655
- return {
1656
- chunks: rows.map(row => {
1657
- try {
1658
- return {
1659
- ...row,
1660
- data: typeof row.data === 'string' ? JSON.parse(row.data) : row.data
1661
- };
1662
- } catch (e) {
1663
- return row;
1664
- }
1665
- }),
1666
- total,
1667
- limit,
1668
- hasMore: total > (limit + 1)
1669
- };
1670
- },
1671
-
1672
- getChunksSince(sessionId, timestamp) {
1673
- const stmt = prep(
1674
- `SELECT id, sessionId, conversationId, sequence, type, data, created_at
1675
- FROM chunks WHERE sessionId = ? AND created_at > ? ORDER BY sequence ASC`
1676
- );
1677
- const rows = stmt.all(sessionId, timestamp);
1678
- return rows.map(row => {
1679
- try {
1680
- return {
1681
- ...row,
1682
- data: typeof row.data === 'string' ? JSON.parse(row.data) : row.data
1683
- };
1684
- } catch (e) {
1685
- return row;
1686
- }
1687
- });
1688
- },
1689
-
1690
- getChunksSinceSeq(sessionId, sinceSeq) {
1691
- const stmt = prep(
1692
- `SELECT id, sessionId, conversationId, sequence, type, data, created_at
1693
- FROM chunks WHERE sessionId = ? AND sequence > ? ORDER BY sequence ASC`
1694
- );
1695
- const rows = stmt.all(sessionId, sinceSeq);
1696
- return rows.map(row => {
1697
- try {
1698
- return {
1699
- ...row,
1700
- data: typeof row.data === 'string' ? JSON.parse(row.data) : row.data
1701
- };
1702
- } catch (e) {
1703
- return row;
1704
- }
1705
- });
1706
- },
1707
-
1708
- deleteSessionChunks(sessionId) {
1709
- const stmt = prep('DELETE FROM chunks WHERE sessionId = ?');
1710
- const result = stmt.run(sessionId);
1711
- return result.changes || 0;
1712
- },
1713
-
1714
- getMaxSequence(sessionId) {
1715
- const stmt = prep('SELECT MAX(sequence) as max FROM chunks WHERE sessionId = ?');
1716
- const result = stmt.get(sessionId);
1717
- return result?.max ?? -1;
1718
- },
1719
-
1720
- getEmptyConversations() {
1721
- const stmt = prep(`
1722
- SELECT c.* FROM conversations c
1723
- LEFT JOIN messages m ON c.id = m.conversationId
1724
- WHERE c.status != 'deleted'
1725
- GROUP BY c.id
1726
- HAVING COUNT(m.id) = 0
1727
- `);
1728
- return stmt.all();
1729
- },
1730
-
1731
- permanentlyDeleteConversation(id) {
1732
- return this.deleteConversation(id);
1733
- },
1734
-
1735
- cleanupEmptyConversations() {
1736
- const emptyConvs = this.getEmptyConversations();
1737
- let deletedCount = 0;
1738
-
1739
- for (const conv of emptyConvs) {
1740
- console.log(`[cleanup] Deleting empty conversation: ${conv.id} (${conv.title || 'Untitled'})`);
1741
- if (this.permanentlyDeleteConversation(conv.id)) {
1742
- deletedCount++;
1743
- }
1744
- }
1745
-
1746
- if (deletedCount > 0) {
1747
- console.log(`[cleanup] Deleted ${deletedCount} empty conversation(s)`);
1748
- }
1749
-
1750
- return deletedCount;
1751
- },
1752
-
1753
-
1754
- getDownloadsByStatus(status) {
1755
- const stmt = prep('SELECT * FROM WHERE status = ? ORDER BY started_at DESC');
1756
- return stmt.all(status);
1757
- },
1758
-
1759
- updateDownloadResume(downloadId, currentSize, attempts, lastAttempt, status) {
1760
- const stmt = prep(`
1761
- UPDATE
1762
- SET downloaded_bytes = ?, attempts = ?, lastAttempt = ?, status = ?
1763
- WHERE id = ?
1764
- `);
1765
- stmt.run(currentSize, attempts, lastAttempt, status, downloadId);
1766
- },
1767
-
1768
- updateDownloadHash(downloadId, hash) {
1769
- const stmt = prep('UPDATE SET hash = ? WHERE id = ?');
1770
- stmt.run(hash, downloadId);
1771
- },
1772
-
1773
- markDownloadResuming(downloadId) {
1774
- const stmt = prep('UPDATE SET status = ?, lastAttempt = ? WHERE id = ?');
1775
- stmt.run('resuming', Date.now(), downloadId);
1776
- },
1777
-
1778
- markDownloadPaused(downloadId, errorMessage) {
1779
- const stmt = prep('UPDATE SET status = ?, error_message = ?, lastAttempt = ? WHERE id = ?');
1780
- stmt.run('paused', errorMessage, Date.now(), downloadId);
1781
- },
1782
-
1783
- saveVoiceCache(conversationId, text, audioBlob, ttlMs = 3600000) {
1784
- const id = generateId('vcache');
1785
- const now = Date.now();
1786
- const expiresAt = now + ttlMs;
1787
- const byteSize = audioBlob ? Buffer.byteLength(audioBlob) : 0;
1788
- const stmt = prep(`
1789
- INSERT INTO voice_cache (id, conversationId, text, audioBlob, byteSize, created_at, expires_at)
1790
- VALUES (?, ?, ?, ?, ?, ?, ?)
1791
- `);
1792
- stmt.run(id, conversationId, text, audioBlob || null, byteSize, now, expiresAt);
1793
- return { id, conversationId, text, byteSize, created_at: now, expires_at: expiresAt };
1794
- },
1795
-
1796
- getVoiceCache(conversationId, text) {
1797
- const now = Date.now();
1798
- const stmt = prep(`
1799
- SELECT id, conversationId, text, audioBlob, byteSize, created_at, expires_at
1800
- FROM voice_cache
1801
- WHERE conversationId = ? AND text = ? AND expires_at > ?
1802
- LIMIT 1
1803
- `);
1804
- return stmt.get(conversationId, text, now) || null;
1805
- },
1806
-
1807
- cleanExpiredVoiceCache() {
1808
- const now = Date.now();
1809
- const stmt = prep('DELETE FROM voice_cache WHERE expires_at <= ?');
1810
- return stmt.run(now).changes;
1811
- },
1812
-
1813
- getVoiceCacheSize(conversationId) {
1814
- const now = Date.now();
1815
- const stmt = prep(`
1816
- SELECT COALESCE(SUM(byteSize), 0) as totalSize
1817
- FROM voice_cache
1818
- WHERE conversationId = ? AND expires_at > ?
1819
- `);
1820
- return stmt.get(conversationId, now).totalSize || 0;
1821
- },
1822
-
1823
- deleteOldestVoiceCache(conversationId, neededBytes) {
1824
- const stmt = prep(`
1825
- SELECT id FROM voice_cache
1826
- WHERE conversationId = ?
1827
- ORDER BY created_at ASC
1828
- LIMIT (SELECT COUNT(*) FROM voice_cache WHERE conversationId = ? AND byteSize > ?)
1829
- `);
1830
- const oldest = stmt.all(conversationId, conversationId, neededBytes);
1831
- const deleteStmt = prep('DELETE FROM voice_cache WHERE id = ?');
1832
- for (const row of oldest) {
1833
- deleteStmt.run(row.id);
1834
- }
1835
- return oldest.length;
1836
- },
1837
-
1838
- initializeToolInstallations(tools) {
1839
- const now = Date.now();
1840
- for (const tool of tools) {
1841
- const stmt = prep(`
1842
- INSERT OR IGNORE INTO tool_installations
1843
- (id, tool_id, status, created_at, updated_at)
1844
- VALUES (?, ?, ?, ?, ?)
1845
- `);
1846
- stmt.run(generateId('tinst'), tool.id, 'not_installed', now, now);
1847
- }
1848
- },
1849
-
1850
- getToolStatus(toolId) {
1851
- const stmt = prep(`
1852
- SELECT id, tool_id, version, installed_at, status, last_check_at,
1853
- error_message, update_available, latest_version, created_at, updated_at
1854
- FROM tool_installations
1855
- WHERE tool_id = ?
1856
- `);
1857
- return stmt.get(toolId) || null;
1858
- },
1859
-
1860
- getAllToolStatuses() {
1861
- const stmt = prep(`
1862
- SELECT id, tool_id, version, installed_at, status, last_check_at,
1863
- error_message, update_available, latest_version, created_at, updated_at
1864
- FROM tool_installations
1865
- ORDER BY tool_id
1866
- `);
1867
- return stmt.all();
1868
- },
1869
-
1870
- insertToolInstallation(toolId, data) {
1871
- const now = Date.now();
1872
- const stmt = prep(`
1873
- INSERT OR IGNORE INTO tool_installations
1874
- (id, tool_id, version, installed_at, status, last_check_at, error_message, update_available, latest_version, created_at, updated_at)
1875
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1876
- `);
1877
- stmt.run(
1878
- generateId('ti'),
1879
- toolId,
1880
- data.version || null,
1881
- data.installed_at || null,
1882
- data.status || 'not_installed',
1883
- now,
1884
- data.error_message || null,
1885
- 0,
1886
- null,
1887
- now,
1888
- now
1889
- );
1890
- },
1891
-
1892
- updateToolStatus(toolId, data) {
1893
- const now = Date.now();
1894
- const stmt = prep(`
1895
- UPDATE tool_installations
1896
- SET version = ?, installed_at = ?, status = ?, last_check_at = ?,
1897
- error_message = ?, update_available = ?, latest_version = ?, updated_at = ?
1898
- WHERE tool_id = ?
1899
- `);
1900
- stmt.run(
1901
- data.version || null,
1902
- data.installed_at || null,
1903
- data.status || 'not_installed',
1904
- data.last_check_at || now,
1905
- data.error_message || null,
1906
- data.update_available ? 1 : 0,
1907
- data.latest_version || null,
1908
- now,
1909
- toolId
1910
- );
1911
- },
1912
-
1913
- addToolInstallHistory(toolId, action, status, error) {
1914
- const now = Date.now();
1915
- const stmt = prep(`
1916
- INSERT INTO tool_install_history
1917
- (id, tool_id, action, started_at, completed_at, status, error_message, created_at)
1918
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1919
- `);
1920
- stmt.run(generateId('thist'), toolId, action, now, now, status, error || null, now);
1921
- },
1922
-
1923
- getToolInstallHistory(toolId, limit = 20, offset = 0) {
1924
- const stmt = prep(`
1925
- SELECT id, tool_id, action, started_at, completed_at, status, error_message, created_at
1926
- FROM tool_install_history
1927
- WHERE tool_id = ?
1928
- ORDER BY created_at DESC
1929
- LIMIT ? OFFSET ?
1930
- `);
1931
- return stmt.all(toolId, limit, offset);
1932
- },
1933
-
1934
- pruneToolInstallHistory(toolId, keepCount = 100) {
1935
- const stmt = prep(`
1936
- DELETE FROM tool_install_history
1937
- WHERE tool_id = ? AND id NOT IN (
1938
- SELECT id FROM tool_install_history
1939
- WHERE tool_id = ?
1940
- ORDER BY created_at DESC
1941
- LIMIT ?
1942
- )
1943
- `);
1944
- return stmt.run(toolId, toolId, keepCount).changes;
1945
- },
1946
-
1947
- searchMessages(query, limit = 50) {
1948
- try {
1949
- const stmt = prep(`
1950
- SELECT m.id, m.conversationId, m.role, m.content, m.created_at,
1951
- c.title as conversationTitle, c.agentType
1952
- FROM messages_fts fts
1953
- JOIN messages m ON m.rowid = fts.rowid
1954
- JOIN conversations c ON c.id = m.conversationId
1955
- WHERE messages_fts MATCH ?
1956
- ORDER BY m.created_at DESC LIMIT ?
1957
- `);
1958
- return stmt.all(query, limit);
1959
- } catch (_) { return []; }
1960
- },
1961
-
1962
- // ============ ACP-COMPATIBLE QUERIES ============
1963
- ...createACPQueries(db, prep)
1964
- };
628
+ import { createQueries } from './lib/db-queries.js';
629
+
630
+ export const queries = createQueries(db, prep, generateId);
1965
631
 
1966
632
  export default { queries };