@meechi-ai/core 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 (116) hide show
  1. package/LICENSE +624 -0
  2. package/README.md +59 -0
  3. package/dist/components/CalendarView.d.ts +3 -0
  4. package/dist/components/CalendarView.js +72 -0
  5. package/dist/components/ChatInterface.d.ts +6 -0
  6. package/dist/components/ChatInterface.js +105 -0
  7. package/dist/components/FileExplorer.d.ts +9 -0
  8. package/dist/components/FileExplorer.js +757 -0
  9. package/dist/components/Icon.d.ts +9 -0
  10. package/dist/components/Icon.js +44 -0
  11. package/dist/components/SourceEditor.d.ts +13 -0
  12. package/dist/components/SourceEditor.js +50 -0
  13. package/dist/components/ThemeProvider.d.ts +5 -0
  14. package/dist/components/ThemeProvider.js +105 -0
  15. package/dist/components/ThemeSwitcher.d.ts +1 -0
  16. package/dist/components/ThemeSwitcher.js +16 -0
  17. package/dist/components/voice/VoiceInputArea.d.ts +14 -0
  18. package/dist/components/voice/VoiceInputArea.js +190 -0
  19. package/dist/components/voice/VoiceOverlay.d.ts +7 -0
  20. package/dist/components/voice/VoiceOverlay.js +71 -0
  21. package/dist/hooks/useMeechi.d.ts +16 -0
  22. package/dist/hooks/useMeechi.js +461 -0
  23. package/dist/hooks/useSync.d.ts +8 -0
  24. package/dist/hooks/useSync.js +87 -0
  25. package/dist/index.d.ts +14 -0
  26. package/dist/index.js +22 -0
  27. package/dist/lib/ai/embeddings.d.ts +15 -0
  28. package/dist/lib/ai/embeddings.js +128 -0
  29. package/dist/lib/ai/gpu-lock.d.ts +19 -0
  30. package/dist/lib/ai/gpu-lock.js +43 -0
  31. package/dist/lib/ai/llm.worker.d.ts +1 -0
  32. package/dist/lib/ai/llm.worker.js +7 -0
  33. package/dist/lib/ai/local-llm.d.ts +30 -0
  34. package/dist/lib/ai/local-llm.js +211 -0
  35. package/dist/lib/ai/manager.d.ts +20 -0
  36. package/dist/lib/ai/manager.js +51 -0
  37. package/dist/lib/ai/parsing.d.ts +12 -0
  38. package/dist/lib/ai/parsing.js +56 -0
  39. package/dist/lib/ai/prompts.d.ts +2 -0
  40. package/dist/lib/ai/prompts.js +2 -0
  41. package/dist/lib/ai/providers/gemini.d.ts +6 -0
  42. package/dist/lib/ai/providers/gemini.js +88 -0
  43. package/dist/lib/ai/providers/groq.d.ts +6 -0
  44. package/dist/lib/ai/providers/groq.js +42 -0
  45. package/dist/lib/ai/registry.d.ts +29 -0
  46. package/dist/lib/ai/registry.js +52 -0
  47. package/dist/lib/ai/tools.d.ts +2 -0
  48. package/dist/lib/ai/tools.js +106 -0
  49. package/dist/lib/ai/types.d.ts +22 -0
  50. package/dist/lib/ai/types.js +1 -0
  51. package/dist/lib/ai/worker.d.ts +1 -0
  52. package/dist/lib/ai/worker.js +60 -0
  53. package/dist/lib/audio/input.d.ts +13 -0
  54. package/dist/lib/audio/input.js +121 -0
  55. package/dist/lib/audio/stt.d.ts +13 -0
  56. package/dist/lib/audio/stt.js +119 -0
  57. package/dist/lib/audio/tts.d.ts +12 -0
  58. package/dist/lib/audio/tts.js +128 -0
  59. package/dist/lib/audio/vad.d.ts +18 -0
  60. package/dist/lib/audio/vad.js +117 -0
  61. package/dist/lib/colors.d.ts +16 -0
  62. package/dist/lib/colors.js +67 -0
  63. package/dist/lib/extensions.d.ts +35 -0
  64. package/dist/lib/extensions.js +24 -0
  65. package/dist/lib/hooks/use-voice-loop.d.ts +13 -0
  66. package/dist/lib/hooks/use-voice-loop.js +313 -0
  67. package/dist/lib/mcp/McpClient.d.ts +19 -0
  68. package/dist/lib/mcp/McpClient.js +42 -0
  69. package/dist/lib/mcp/McpRegistry.d.ts +47 -0
  70. package/dist/lib/mcp/McpRegistry.js +117 -0
  71. package/dist/lib/mcp/native/GroqVoiceNative.d.ts +21 -0
  72. package/dist/lib/mcp/native/GroqVoiceNative.js +29 -0
  73. package/dist/lib/mcp/native/LocalSyncNative.d.ts +19 -0
  74. package/dist/lib/mcp/native/LocalSyncNative.js +26 -0
  75. package/dist/lib/mcp/native/LocalVoiceNative.d.ts +19 -0
  76. package/dist/lib/mcp/native/LocalVoiceNative.js +27 -0
  77. package/dist/lib/mcp/native/MeechiNativeCore.d.ts +25 -0
  78. package/dist/lib/mcp/native/MeechiNativeCore.js +209 -0
  79. package/dist/lib/mcp/native/index.d.ts +10 -0
  80. package/dist/lib/mcp/native/index.js +10 -0
  81. package/dist/lib/mcp/types.d.ts +35 -0
  82. package/dist/lib/mcp/types.js +1 -0
  83. package/dist/lib/pdf.d.ts +10 -0
  84. package/dist/lib/pdf.js +142 -0
  85. package/dist/lib/settings.d.ts +48 -0
  86. package/dist/lib/settings.js +87 -0
  87. package/dist/lib/storage/db.d.ts +57 -0
  88. package/dist/lib/storage/db.js +45 -0
  89. package/dist/lib/storage/local.d.ts +28 -0
  90. package/dist/lib/storage/local.js +534 -0
  91. package/dist/lib/storage/migrate.d.ts +3 -0
  92. package/dist/lib/storage/migrate.js +122 -0
  93. package/dist/lib/storage/types.d.ts +66 -0
  94. package/dist/lib/storage/types.js +1 -0
  95. package/dist/lib/sync/client-drive.d.ts +9 -0
  96. package/dist/lib/sync/client-drive.js +69 -0
  97. package/dist/lib/sync/engine.d.ts +18 -0
  98. package/dist/lib/sync/engine.js +517 -0
  99. package/dist/lib/sync/google-drive.d.ts +52 -0
  100. package/dist/lib/sync/google-drive.js +183 -0
  101. package/dist/lib/sync/merge.d.ts +1 -0
  102. package/dist/lib/sync/merge.js +68 -0
  103. package/dist/lib/yjs/YjsProvider.d.ts +11 -0
  104. package/dist/lib/yjs/YjsProvider.js +33 -0
  105. package/dist/lib/yjs/graph.d.ts +11 -0
  106. package/dist/lib/yjs/graph.js +7 -0
  107. package/dist/lib/yjs/hooks.d.ts +7 -0
  108. package/dist/lib/yjs/hooks.js +37 -0
  109. package/dist/lib/yjs/store.d.ts +4 -0
  110. package/dist/lib/yjs/store.js +19 -0
  111. package/dist/lib/yjs/syncGraph.d.ts +1 -0
  112. package/dist/lib/yjs/syncGraph.js +38 -0
  113. package/dist/providers/theme-provider.d.ts +3 -0
  114. package/dist/providers/theme-provider.js +18 -0
  115. package/dist/tsconfig.lib.tsbuildinfo +1 -0
  116. package/package.json +69 -0
@@ -0,0 +1,45 @@
1
+ import Dexie from 'dexie';
2
+ export class MeechiDB extends Dexie {
3
+ constructor() {
4
+ super(process.env.NEXT_PUBLIC_DB_NAME || 'meechi-db');
5
+ this.version(1).stores({
6
+ files: 'path, remoteId, type, updatedAt'
7
+ });
8
+ // Add settings table in version 2
9
+ this.version(2).stores({
10
+ settings: 'key'
11
+ });
12
+ // Add dirty/deleted index in version 3
13
+ this.version(3).stores({
14
+ files: 'path, remoteId, type, updatedAt, dirty, deleted'
15
+ }).upgrade(tx => {
16
+ return tx.table("files").toCollection().modify(file => {
17
+ file.dirty = 0;
18
+ file.deleted = 0;
19
+ });
20
+ });
21
+ // Add semantic chunks table in version 4
22
+ this.version(4).stores({
23
+ chunks: '++id, filePath'
24
+ });
25
+ // Add journal table in version 5
26
+ this.version(5).stores({
27
+ journal: '++id, createdAt'
28
+ });
29
+ // Add tags and metadata table in version 6
30
+ this.version(6).stores({
31
+ files: 'path, remoteId, type, updatedAt, dirty, deleted, *tags'
32
+ }).upgrade(tx => {
33
+ return tx.table("files").toCollection().modify(file => {
34
+ file.tags = [];
35
+ file.metadata = {};
36
+ });
37
+ });
38
+ // Add graph edges table in version 7
39
+ this.version(7).stores({
40
+ edges: 'id, source, target, relation'
41
+ });
42
+ }
43
+ }
44
+ // Prevent SSR crash by using a dummy object or conditionally instantiating
45
+ export const db = (typeof window !== 'undefined') ? new MeechiDB() : {};
@@ -0,0 +1,28 @@
1
+ import { StorageProvider, FileMeta } from './types';
2
+ export declare class LocalStorageProvider implements StorageProvider {
3
+ private syncEngine;
4
+ setSyncEngine(engine: any): void;
5
+ init(): Promise<void>;
6
+ indexFile(path: string, content: string): Promise<void>;
7
+ listFiles(prefix: string): Promise<FileMeta[]>;
8
+ getFile(virtualPath: string): Promise<FileMeta | null>;
9
+ readFile(virtualPath: string): Promise<string | Blob | ArrayBuffer | null>;
10
+ saveFile(virtualPath: string, content: string | Blob | ArrayBuffer, remoteId?: string, tags?: string[], metadata?: any): Promise<void>;
11
+ updateMetadata(virtualPath: string, updates: Partial<FileMeta>): Promise<void>;
12
+ getFilesByTag(tag: string): Promise<FileMeta[]>;
13
+ appendFile(virtualPath: string, content: string, skipIndex?: boolean): Promise<void>;
14
+ updateFile(virtualPath: string, newContent: string): Promise<void>;
15
+ getRecentLogs(limitHours: number): Promise<string>;
16
+ private formatDate;
17
+ private filterLogByTime;
18
+ renameFile(oldPath: string, newPath: string): Promise<void>;
19
+ deleteFile(virtualPath: string): Promise<void>;
20
+ createFolder(virtualPath: string): Promise<void>;
21
+ private ensureParent;
22
+ resetSyncState(): Promise<void>;
23
+ factoryReset(): Promise<void>;
24
+ forceSync(): Promise<void>;
25
+ private ensureFolder;
26
+ getKnowledgeContext(query?: string): Promise<string>;
27
+ private getLegacyKnowledgeContext;
28
+ }
@@ -0,0 +1,534 @@
1
+ import { db } from './db';
2
+ import { migrateFromIdbToDexie, migrateJournal } from './migrate';
3
+ import { generateEmbedding, chunkText, cosineSimilarity } from '../ai/embeddings';
4
+ export class LocalStorageProvider {
5
+ setSyncEngine(engine) {
6
+ this.syncEngine = engine;
7
+ }
8
+ async init() {
9
+ // Run migration logic once on init
10
+ await migrateFromIdbToDexie();
11
+ await migrateJournal();
12
+ }
13
+ async indexFile(path, content) {
14
+ var _a;
15
+ // Only index text-based files in knowledge base (misc/ or source files) AND history logs
16
+ const isKnowledge = path.startsWith('misc/') || path.endsWith('.source.md') || path.startsWith('history/');
17
+ if (!isKnowledge)
18
+ return;
19
+ try {
20
+ console.log(`[RAG] Indexing ${path}...`);
21
+ // 2. Fetch metadata to include comments in indexing
22
+ const file = await db.files.get(path);
23
+ const comments = ((_a = file === null || file === void 0 ? void 0 : file.metadata) === null || _a === void 0 ? void 0 : _a.comments) || [];
24
+ let fullText = content;
25
+ if (comments.length > 0) {
26
+ const commentText = comments
27
+ .filter((c) => { var _a; return (_a = c.text) === null || _a === void 0 ? void 0 : _a.trim(); }) // Only include non-empty comments
28
+ .map((c) => c.text)
29
+ .join('\n');
30
+ if (commentText) {
31
+ fullText += "\n\n### User Notes & Comments\n" + commentText;
32
+ }
33
+ }
34
+ // 4. Clear old chunks for this file
35
+ await db.chunks.where('filePath').equals(path).delete();
36
+ // 5. Chunk text
37
+ const chunks = chunkText(fullText);
38
+ // 6. Generate embeddings and save
39
+ for (const text of chunks) {
40
+ const embedding = await generateEmbedding(text);
41
+ await db.chunks.add({
42
+ filePath: path,
43
+ content: text,
44
+ embedding
45
+ });
46
+ }
47
+ console.log(`[RAG] Finished indexing ${path} (${chunks.length} chunks).`);
48
+ }
49
+ catch (e) {
50
+ console.error(`[RAG] Failed to index ${path}`, e);
51
+ }
52
+ }
53
+ async listFiles(prefix) {
54
+ // Dexie 'startsWith' query
55
+ // "misc" -> "misc/"
56
+ // BUT: our paths are 'misc/foo.txt'.
57
+ // If prefix is '', get all.
58
+ let collection;
59
+ if (!prefix || prefix === 'root') {
60
+ collection = db.files.toCollection();
61
+ }
62
+ else {
63
+ // We want all files that START with "{prefix}/" OR equal "{prefix}" (if it's a file)
64
+ // Actually, the File Explorer passes 'misc'.
65
+ // We want 'misc/foo', 'misc/bar'.
66
+ // We want 'misc/foo', 'misc/bar'.
67
+ collection = db.files.where('path').startsWith(prefix);
68
+ }
69
+ const records = await collection.filter(f => !f.deleted).toArray();
70
+ return records.map(r => ({
71
+ id: r.path,
72
+ name: r.path.split('/').pop() || r.path,
73
+ path: r.path,
74
+ updatedAt: r.updatedAt,
75
+ type: r.type,
76
+ remoteId: r.remoteId,
77
+ tags: r.tags,
78
+ metadata: r.metadata
79
+ }));
80
+ }
81
+ async getFile(virtualPath) {
82
+ const item = await db.files.get(virtualPath);
83
+ if (!item || item.deleted)
84
+ return null;
85
+ return {
86
+ id: item.path,
87
+ name: item.path.split('/').pop() || item.path,
88
+ path: item.path,
89
+ type: item.type,
90
+ updatedAt: item.updatedAt,
91
+ remoteId: item.remoteId,
92
+ tags: item.tags,
93
+ metadata: item.metadata
94
+ };
95
+ }
96
+ async readFile(virtualPath) {
97
+ const file = await db.files.get(virtualPath);
98
+ if (!file || file.deleted)
99
+ return null;
100
+ return file.content;
101
+ }
102
+ async saveFile(virtualPath, content, remoteId, tags, metadata) {
103
+ await this.ensureParent(virtualPath);
104
+ // Optimistic Update
105
+ await db.transaction('rw', db.files, async () => {
106
+ const existing = await db.files.get(virtualPath);
107
+ await db.files.put({
108
+ path: virtualPath,
109
+ content: content,
110
+ updatedAt: Date.now(),
111
+ type: virtualPath.endsWith('.source.md') ? 'source' : 'file',
112
+ remoteId: remoteId || (existing === null || existing === void 0 ? void 0 : existing.remoteId), // Preserve remoteId if updating content locally
113
+ dirty: 1, // Mark as dirty (needs sync up)
114
+ deleted: 0,
115
+ tags: tags !== undefined ? tags : (existing === null || existing === void 0 ? void 0 : existing.tags) || [],
116
+ metadata: metadata !== undefined ? metadata : (existing === null || existing === void 0 ? void 0 : existing.metadata) || {}
117
+ });
118
+ });
119
+ if (typeof content === 'string') {
120
+ this.indexFile(virtualPath, content);
121
+ }
122
+ }
123
+ async updateMetadata(virtualPath, updates) {
124
+ console.log('[Storage] updateMetadata called for:', virtualPath);
125
+ console.log('[Storage] Updates:', updates);
126
+ await db.transaction('rw', db.files, async () => {
127
+ const existing = await db.files.get(virtualPath);
128
+ if (!existing)
129
+ throw new Error(`File not found: ${virtualPath}`);
130
+ console.log('[Storage] Existing file found, updating...');
131
+ await db.files.update(virtualPath, Object.assign(Object.assign({}, updates), { updatedAt: Date.now(), dirty: 1 }));
132
+ console.log('[Storage] Database updated successfully');
133
+ });
134
+ }
135
+ async getFilesByTag(tag) {
136
+ const records = await db.files.where('tags').equals(tag).filter(f => !f.deleted).toArray();
137
+ return records.map(r => ({
138
+ id: r.path,
139
+ name: r.path.split('/').pop() || r.path,
140
+ path: r.path,
141
+ updatedAt: r.updatedAt,
142
+ type: r.type,
143
+ remoteId: r.remoteId,
144
+ tags: r.tags,
145
+ metadata: r.metadata
146
+ }));
147
+ }
148
+ async appendFile(virtualPath, content, skipIndex = false) {
149
+ await this.ensureParent(virtualPath);
150
+ let finalContent = "";
151
+ await db.transaction('rw', db.files, async () => {
152
+ const existing = await db.files.get(virtualPath);
153
+ finalContent = (existing && typeof existing.content === 'string')
154
+ ? (existing.content + '\n\n' + content)
155
+ : content;
156
+ await db.files.put({
157
+ path: virtualPath,
158
+ content: finalContent,
159
+ updatedAt: Date.now(),
160
+ type: 'file',
161
+ remoteId: existing === null || existing === void 0 ? void 0 : existing.remoteId,
162
+ dirty: 1,
163
+ deleted: 0
164
+ });
165
+ });
166
+ // Background Indexing (skipped if requested, e.g. for high-speed voice chat)
167
+ if (!skipIndex) {
168
+ this.indexFile(virtualPath, finalContent);
169
+ }
170
+ }
171
+ async updateFile(virtualPath, newContent) {
172
+ // 1. Verify existence (Logic: Can only update what exists)
173
+ const existing = await db.files.get(virtualPath);
174
+ if (!existing || existing.deleted) {
175
+ throw new Error(`File '${virtualPath}' not found. Cannot update.`);
176
+ }
177
+ // 2. Perform Update (Overwrite content)
178
+ await db.transaction('rw', db.files, async () => {
179
+ await db.files.update(virtualPath, {
180
+ content: newContent,
181
+ updatedAt: Date.now(),
182
+ dirty: 1, // Mark dirty for sync
183
+ deleted: 0
184
+ });
185
+ });
186
+ // 3. Re-Index for RAG
187
+ this.indexFile(virtualPath, newContent);
188
+ }
189
+ async getRecentLogs(limitHours) {
190
+ const now = new Date();
191
+ const startTime = new Date(now.getTime() - limitHours * 60 * 60 * 1000);
192
+ // Get Dates for Today and Yesterday (using local time logic implicitly via Date)
193
+ const todayDate = new Date();
194
+ const yesterdayDate = new Date(now.getTime() - 24 * 60 * 60 * 1000);
195
+ const todayStr = this.formatDate(todayDate);
196
+ const yesterdayStr = this.formatDate(yesterdayDate);
197
+ let logs = "";
198
+ // 1. If window crosses midnight (start time is yesterday), read yesterday's log first
199
+ let yesterdayContent = null;
200
+ if (startTime.getDate() !== now.getDate()) {
201
+ yesterdayContent = await this.readFile(`history/${yesterdayStr}.md`);
202
+ if (typeof yesterdayContent === 'string') {
203
+ logs += this.filterLogByTime(yesterdayContent, startTime, yesterdayDate) + "\n";
204
+ }
205
+ }
206
+ // 2. Read Today's log
207
+ const todayContent = await this.readFile(`history/${todayStr}.md`);
208
+ if (typeof todayContent === 'string') {
209
+ logs += this.filterLogByTime(todayContent, startTime, todayDate);
210
+ }
211
+ console.log(`[Storage] getRecentLogs for ${limitHours}h. Files: ${yesterdayStr}(${typeof yesterdayContent}), ${todayStr}(${typeof todayContent}). Total Len: ${logs.length}`);
212
+ return logs || "No recent history.";
213
+ }
214
+ formatDate(date) {
215
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
216
+ }
217
+ filterLogByTime(content, startTime, fileDate) {
218
+ const blocks = content.split('###');
219
+ let result = "";
220
+ for (const block of blocks) {
221
+ if (!block.trim())
222
+ continue;
223
+ // Extract Time: " 10:30:05 PM", " 10:30 PM", or "22:30"
224
+ // Regex: support optional seconds (?::\d{2})?
225
+ const timeMatch = block.match(/^\s*(\d{1,2}:\d{2}(?::\d{2})?\s?(?:AM|PM)?)/i);
226
+ if (timeMatch) {
227
+ const timeStr = timeMatch[1];
228
+ // Parse Time
229
+ const entryDate = new Date(fileDate);
230
+ const [time, modifier] = timeStr.trim().split(' ');
231
+ let [hours, minutes, seconds] = time.split(':').map(Number);
232
+ // Handle optional seconds
233
+ if (isNaN(seconds))
234
+ seconds = 0;
235
+ if (modifier) {
236
+ if (modifier.toUpperCase() === 'PM' && hours < 12)
237
+ hours += 12;
238
+ if (modifier.toUpperCase() === 'AM' && hours === 12)
239
+ hours = 0;
240
+ }
241
+ entryDate.setHours(hours, minutes, seconds);
242
+ if (entryDate >= startTime) {
243
+ result += '###' + block;
244
+ }
245
+ }
246
+ else {
247
+ // Include blocks without timestamp (e.g. continuations or system headers)
248
+ // if they are part of the file we typically assume they are relevant if the file is relevant.
249
+ // But for safety in a rolling window, maybe we skip if we can't date it?
250
+ // Actually, 'Added Source' logs might be system logs with timestamps.
251
+ // If it HAS NO timestamp, it might be garbage or noise. Let's keep it to be safe.
252
+ result += '###' + block;
253
+ }
254
+ }
255
+ return result;
256
+ }
257
+ async renameFile(oldPath, newPath) {
258
+ await this.ensureParent(newPath);
259
+ await db.transaction('rw', db.files, db.chunks, async () => {
260
+ const existing = await db.files.get(oldPath);
261
+ if (!existing)
262
+ throw new Error(`File not found: ${oldPath}`);
263
+ // 1. Rename the item itself
264
+ await db.files.put(Object.assign(Object.assign({}, existing), { path: newPath, updatedAt: Date.now(), dirty: 1, deleted: 0 }));
265
+ await db.files.delete(oldPath);
266
+ // 1b. Migrate Chunks for this file
267
+ await db.chunks.where('filePath').equals(oldPath).modify({ filePath: newPath });
268
+ // 2. If it's a folder, rename all children recursively
269
+ if (existing.type === 'folder') {
270
+ const prefix = oldPath + '/';
271
+ const children = await db.files.where('path').startsWith(prefix).toArray();
272
+ for (const child of children) {
273
+ const childNewPath = child.path.replace(prefix, newPath + '/');
274
+ // Create new child record
275
+ await db.files.put(Object.assign(Object.assign({}, child), { path: childNewPath, updatedAt: Date.now(), dirty: 1, deleted: 0 }));
276
+ // Delete old child record
277
+ await db.files.delete(child.path);
278
+ // Migrate chunks for child
279
+ await db.chunks.where('filePath').equals(child.path).modify({ filePath: childNewPath });
280
+ }
281
+ }
282
+ });
283
+ }
284
+ async deleteFile(virtualPath) {
285
+ // Soft delete for sync
286
+ await db.transaction('rw', db.files, db.chunks, async () => {
287
+ const existing = await db.files.get(virtualPath);
288
+ if (!existing)
289
+ return;
290
+ // 1. Delete the item itself
291
+ await db.files.update(virtualPath, { deleted: 1, dirty: 1 });
292
+ // 2. Delete semantic chunks
293
+ await db.chunks.where('filePath').equals(virtualPath).delete();
294
+ // 3. If it's a folder, soft-delete all children recursively
295
+ if (existing.type === 'folder') {
296
+ const prefix = virtualPath + '/';
297
+ const children = await db.files.where('path').startsWith(prefix).toArray();
298
+ for (const child of children) {
299
+ await db.files.update(child.path, { deleted: 1, dirty: 1 });
300
+ await db.chunks.where('filePath').equals(child.path).delete();
301
+ }
302
+ }
303
+ });
304
+ }
305
+ async createFolder(virtualPath) {
306
+ await this.ensureParent(virtualPath);
307
+ await db.transaction('rw', db.files, async () => {
308
+ const existing = await db.files.get(virtualPath);
309
+ // Always update timestamp and dirty
310
+ await db.files.put({
311
+ path: virtualPath,
312
+ content: '',
313
+ updatedAt: Date.now(),
314
+ type: 'folder',
315
+ remoteId: existing === null || existing === void 0 ? void 0 : existing.remoteId,
316
+ dirty: 1, // Crucial for sync
317
+ deleted: 0
318
+ });
319
+ });
320
+ }
321
+ async ensureParent(path) {
322
+ const parts = path.split('/');
323
+ if (parts.length <= 1)
324
+ return;
325
+ // Try to find parent
326
+ const parentPath = parts.slice(0, -1).join('/');
327
+ // Optimize: check if exists first
328
+ const parent = await db.files.get(parentPath);
329
+ if (parent && !parent.deleted)
330
+ return;
331
+ // Check recursively (grandparent)
332
+ await this.ensureParent(parentPath);
333
+ // Create Parent Folder
334
+ await db.files.put({
335
+ path: parentPath,
336
+ content: '',
337
+ updatedAt: Date.now(),
338
+ type: 'folder',
339
+ dirty: 1,
340
+ deleted: 0
341
+ });
342
+ }
343
+ async resetSyncState() {
344
+ console.log("Reseting sync state...");
345
+ // 1. Clear Settings
346
+ await db.settings.delete('drive_sync_token');
347
+ await db.settings.delete('drive_root_id');
348
+ await db.settings.delete('drive_root_id_writable');
349
+ // 2. Clear Remote IDs & Mark Dirty
350
+ await db.transaction('rw', db.files, async () => {
351
+ const all = await db.files.toArray();
352
+ for (const file of all) {
353
+ await db.files.update(file.path, {
354
+ remoteId: undefined,
355
+ dirty: 1
356
+ });
357
+ }
358
+ });
359
+ console.log("Sync state reset. All files marked dirty.");
360
+ }
361
+ async factoryReset() {
362
+ console.warn("PERFORMING FACTORY RESET...");
363
+ await db.transaction('rw', db.files, db.settings, db.chunks, async () => {
364
+ await db.files.clear();
365
+ await db.settings.clear();
366
+ await db.chunks.clear();
367
+ });
368
+ // Re-init default folders
369
+ await this.ensureFolder('misc');
370
+ await this.ensureFolder('history');
371
+ console.log("Factory Reset Complete.");
372
+ }
373
+ async forceSync() {
374
+ console.log("Force Sync Requested");
375
+ if (this.syncEngine) {
376
+ await this.syncEngine.sync();
377
+ }
378
+ }
379
+ async ensureFolder(path) {
380
+ await db.files.put({
381
+ path,
382
+ content: '',
383
+ updatedAt: Date.now(),
384
+ type: 'folder',
385
+ dirty: 1,
386
+ deleted: 0
387
+ });
388
+ }
389
+ async getKnowledgeContext(query) {
390
+ if (query) {
391
+ console.log(`[RAG] Performing Semantic Search for: "${query}"`);
392
+ try {
393
+ const queryEmbedding = await generateEmbedding(query);
394
+ const allChunks = await db.chunks.toArray();
395
+ if (allChunks.length === 0) {
396
+ return "--- Knowledge Base ---\nNo indexed memory found. Falling back to basic context.\n" + await this.getLegacyKnowledgeContext();
397
+ }
398
+ // Rank chunks by similarity
399
+ const ranked = allChunks.map(chunk => ({
400
+ chunk,
401
+ similarity: cosineSimilarity(queryEmbedding, chunk.embedding)
402
+ })).sort((a, b) => b.similarity - a.similarity);
403
+ // FILENAME & TAG BOOSTING
404
+ // If the user explicitly mentions a file (e.g. "Schema Theory") or a #tag, we MUST include it regardless of vector score.
405
+ const queryLower = query.toLowerCase();
406
+ const boostedChunks = [];
407
+ const seenChunkIds = new Set();
408
+ // Extract #tags from query
409
+ const tagMatches = query.match(/#([\w\-]+)/g) || [];
410
+ const queryTags = tagMatches.map(t => t.substring(1).toLowerCase());
411
+ // 1. Find matched files (by Name OR Tag)
412
+ // Use Dexie's index for tags if possible, or filter.
413
+ let taggedFiles = [];
414
+ if (queryTags.length > 0) {
415
+ // OR logic for tags
416
+ const uniqueTags = Array.from(new Set(queryTags));
417
+ for (const t of uniqueTags) {
418
+ // We need a way to search case-insensitive on tags array?
419
+ // Dexie *tags index is case-sensitive by default usually unless locale.
420
+ // For now, let's just iterate all files or use DB efficient search if possible.
421
+ // Since we are iterating allFiles below anyway for filename match, let's combine.
422
+ }
423
+ }
424
+ const allFiles = await db.files.toArray();
425
+ const matchedFiles = allFiles.filter(f => {
426
+ var _a;
427
+ if (!f.path.startsWith('misc/'))
428
+ return false;
429
+ const filename = ((_a = f.path.split('/').pop()) === null || _a === void 0 ? void 0 : _a.toLowerCase()) || "";
430
+ // A. Tag Match
431
+ if (f.tags && f.tags.some(ft => queryTags.includes(ft.toLowerCase()))) {
432
+ return true;
433
+ }
434
+ // B. Filename Match
435
+ // Boosting Logic: If filename is found in query OR query is found in filename
436
+ // (ignoring very short queries)
437
+ if (queryLower.length > 3 && filename.includes(queryLower))
438
+ return true;
439
+ if (filename.length > 3 && queryLower.includes(filename))
440
+ return true;
441
+ return false;
442
+ });
443
+ if (matchedFiles.length > 0) {
444
+ console.log(`[RAG] Boosted Match Found: ${matchedFiles.map(f => f.path).join(', ')}`);
445
+ // Get all chunks for these files
446
+ for (const file of matchedFiles) {
447
+ const fileChunks = allChunks.filter(c => c.filePath === file.path);
448
+ for (const c of fileChunks) {
449
+ if (!seenChunkIds.has(c.content)) { // simple content dedup
450
+ boostedChunks.push({ chunk: c, similarity: 1.0 }); // Artificially high score
451
+ seenChunkIds.add(c.content);
452
+ }
453
+ }
454
+ }
455
+ }
456
+ // 2. Select Top Chunks (Boosted + Semantic)
457
+ const semanticChunks = ranked.filter(r => r.similarity > 0.1 && !seenChunkIds.has(r.chunk.content));
458
+ // Combine: Boosted first, then highest semantic
459
+ // Total Limit: 8 chunks (approx 2k tokens)
460
+ const combined = [...boostedChunks, ...semanticChunks].slice(0, 8);
461
+ if (combined.length === 0) {
462
+ return "--- Knowledge Base ---\nNo relevant files found for this query.\n";
463
+ }
464
+ let ctx = "--- Relevant Memory (Semantic Search) ---\n";
465
+ for (const item of combined) {
466
+ // Normalize filename for the AI: remove path and extension
467
+ // "misc/Research/Foo.pdf" -> "Foo"
468
+ const rawName = item.chunk.filePath.split('/').pop() || "";
469
+ const cleanName = rawName.replace(/\.(pdf|md|txt)(\.source\.md)?$/i, "");
470
+ // Add [Boosted] tag for debug clarity if it was a filename match
471
+ const tag = item.similarity === 1.0 ? " (Exact Match)" : "";
472
+ ctx += `\n### Source: ${cleanName}${tag}\n${item.chunk.content}\n---\n`;
473
+ }
474
+ return ctx;
475
+ }
476
+ catch (e) {
477
+ console.error("[RAG] Search failed", e);
478
+ return await this.getLegacyKnowledgeContext();
479
+ }
480
+ }
481
+ else {
482
+ return await this.getLegacyKnowledgeContext();
483
+ }
484
+ }
485
+ async getLegacyKnowledgeContext() {
486
+ // Get all files in misc/
487
+ const files = await db.files.where('path').startsWith('misc/').toArray();
488
+ const readableFiles = files.filter(f => !f.deleted && f.type === 'file');
489
+ let ctx = "--- Knowledge Base (Misc Folder) ---\n";
490
+ if (readableFiles.length === 0)
491
+ return ctx + "No files found in Knowledge Base.\n";
492
+ let totalLength = 0;
493
+ // Reducing limit to 8k (~2k tokens) to be extremely safe with Groq Free Tier (12k TPM)
494
+ const CHAR_LIMIT = 8000;
495
+ // 1. Identify Sources to avoid duplication
496
+ const sourcePaths = new Set(readableFiles.map(f => f.path));
497
+ for (const file of readableFiles) {
498
+ if (totalLength > CHAR_LIMIT) {
499
+ ctx += `\n[System] Context limit reached. Some files omitted.\n`;
500
+ break;
501
+ }
502
+ // Check if this is a raw PDF that has a Shadow Source
503
+ const potentialSourcePath = file.path + '.source.md';
504
+ if (file.path.endsWith('.pdf') && sourcePaths.has(potentialSourcePath)) {
505
+ // Skip the raw PDF, we will read the .source.md instead
506
+ continue;
507
+ }
508
+ // Include text-based files and PDFs
509
+ const isText = file.path.endsWith('.md') || file.path.endsWith('.txt');
510
+ const isPdf = file.path.endsWith('.pdf');
511
+ if (isText || isPdf) {
512
+ // Ensure content is string before string operations
513
+ if (typeof file.content !== 'string') {
514
+ // Binary content (e.g. locally stored PDF).
515
+ // We cannot include binary in AI Context.
516
+ // However, we likely have a .source.md for this PDF, so we skipped it above (if source exists).
517
+ // If source does NOT exist (e.g. old file or failed extract), and we have binary, we simply say "Binary Content".
518
+ ctx += `\nFile: ${file.path} (Binary Content - Use Source)\n---\n`;
519
+ continue;
520
+ }
521
+ // Truncate individual massive files (e.g. books) to 10k chars (approx 2.5k tokens)
522
+ const content = file.content.length > 10000
523
+ ? file.content.substring(0, 10000) + "\n...[Content Truncated]..."
524
+ : file.content;
525
+ ctx += `\nFile: ${file.path}\nContent:\n${content}\n---\n`;
526
+ totalLength += content.length;
527
+ }
528
+ else {
529
+ ctx += `\nFile: ${file.path} (Non-text file, content unavailable)\n---\n`;
530
+ }
531
+ }
532
+ return ctx;
533
+ }
534
+ }
@@ -0,0 +1,3 @@
1
+ export declare function migrateFromIdbToDexie(): Promise<void>;
2
+ export declare function migrateFromMichioToMeechi(): Promise<void>;
3
+ export declare function migrateJournal(): Promise<void>;