@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.
- package/LICENSE +624 -0
- package/README.md +59 -0
- package/dist/components/CalendarView.d.ts +3 -0
- package/dist/components/CalendarView.js +72 -0
- package/dist/components/ChatInterface.d.ts +6 -0
- package/dist/components/ChatInterface.js +105 -0
- package/dist/components/FileExplorer.d.ts +9 -0
- package/dist/components/FileExplorer.js +757 -0
- package/dist/components/Icon.d.ts +9 -0
- package/dist/components/Icon.js +44 -0
- package/dist/components/SourceEditor.d.ts +13 -0
- package/dist/components/SourceEditor.js +50 -0
- package/dist/components/ThemeProvider.d.ts +5 -0
- package/dist/components/ThemeProvider.js +105 -0
- package/dist/components/ThemeSwitcher.d.ts +1 -0
- package/dist/components/ThemeSwitcher.js +16 -0
- package/dist/components/voice/VoiceInputArea.d.ts +14 -0
- package/dist/components/voice/VoiceInputArea.js +190 -0
- package/dist/components/voice/VoiceOverlay.d.ts +7 -0
- package/dist/components/voice/VoiceOverlay.js +71 -0
- package/dist/hooks/useMeechi.d.ts +16 -0
- package/dist/hooks/useMeechi.js +461 -0
- package/dist/hooks/useSync.d.ts +8 -0
- package/dist/hooks/useSync.js +87 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +22 -0
- package/dist/lib/ai/embeddings.d.ts +15 -0
- package/dist/lib/ai/embeddings.js +128 -0
- package/dist/lib/ai/gpu-lock.d.ts +19 -0
- package/dist/lib/ai/gpu-lock.js +43 -0
- package/dist/lib/ai/llm.worker.d.ts +1 -0
- package/dist/lib/ai/llm.worker.js +7 -0
- package/dist/lib/ai/local-llm.d.ts +30 -0
- package/dist/lib/ai/local-llm.js +211 -0
- package/dist/lib/ai/manager.d.ts +20 -0
- package/dist/lib/ai/manager.js +51 -0
- package/dist/lib/ai/parsing.d.ts +12 -0
- package/dist/lib/ai/parsing.js +56 -0
- package/dist/lib/ai/prompts.d.ts +2 -0
- package/dist/lib/ai/prompts.js +2 -0
- package/dist/lib/ai/providers/gemini.d.ts +6 -0
- package/dist/lib/ai/providers/gemini.js +88 -0
- package/dist/lib/ai/providers/groq.d.ts +6 -0
- package/dist/lib/ai/providers/groq.js +42 -0
- package/dist/lib/ai/registry.d.ts +29 -0
- package/dist/lib/ai/registry.js +52 -0
- package/dist/lib/ai/tools.d.ts +2 -0
- package/dist/lib/ai/tools.js +106 -0
- package/dist/lib/ai/types.d.ts +22 -0
- package/dist/lib/ai/types.js +1 -0
- package/dist/lib/ai/worker.d.ts +1 -0
- package/dist/lib/ai/worker.js +60 -0
- package/dist/lib/audio/input.d.ts +13 -0
- package/dist/lib/audio/input.js +121 -0
- package/dist/lib/audio/stt.d.ts +13 -0
- package/dist/lib/audio/stt.js +119 -0
- package/dist/lib/audio/tts.d.ts +12 -0
- package/dist/lib/audio/tts.js +128 -0
- package/dist/lib/audio/vad.d.ts +18 -0
- package/dist/lib/audio/vad.js +117 -0
- package/dist/lib/colors.d.ts +16 -0
- package/dist/lib/colors.js +67 -0
- package/dist/lib/extensions.d.ts +35 -0
- package/dist/lib/extensions.js +24 -0
- package/dist/lib/hooks/use-voice-loop.d.ts +13 -0
- package/dist/lib/hooks/use-voice-loop.js +313 -0
- package/dist/lib/mcp/McpClient.d.ts +19 -0
- package/dist/lib/mcp/McpClient.js +42 -0
- package/dist/lib/mcp/McpRegistry.d.ts +47 -0
- package/dist/lib/mcp/McpRegistry.js +117 -0
- package/dist/lib/mcp/native/GroqVoiceNative.d.ts +21 -0
- package/dist/lib/mcp/native/GroqVoiceNative.js +29 -0
- package/dist/lib/mcp/native/LocalSyncNative.d.ts +19 -0
- package/dist/lib/mcp/native/LocalSyncNative.js +26 -0
- package/dist/lib/mcp/native/LocalVoiceNative.d.ts +19 -0
- package/dist/lib/mcp/native/LocalVoiceNative.js +27 -0
- package/dist/lib/mcp/native/MeechiNativeCore.d.ts +25 -0
- package/dist/lib/mcp/native/MeechiNativeCore.js +209 -0
- package/dist/lib/mcp/native/index.d.ts +10 -0
- package/dist/lib/mcp/native/index.js +10 -0
- package/dist/lib/mcp/types.d.ts +35 -0
- package/dist/lib/mcp/types.js +1 -0
- package/dist/lib/pdf.d.ts +10 -0
- package/dist/lib/pdf.js +142 -0
- package/dist/lib/settings.d.ts +48 -0
- package/dist/lib/settings.js +87 -0
- package/dist/lib/storage/db.d.ts +57 -0
- package/dist/lib/storage/db.js +45 -0
- package/dist/lib/storage/local.d.ts +28 -0
- package/dist/lib/storage/local.js +534 -0
- package/dist/lib/storage/migrate.d.ts +3 -0
- package/dist/lib/storage/migrate.js +122 -0
- package/dist/lib/storage/types.d.ts +66 -0
- package/dist/lib/storage/types.js +1 -0
- package/dist/lib/sync/client-drive.d.ts +9 -0
- package/dist/lib/sync/client-drive.js +69 -0
- package/dist/lib/sync/engine.d.ts +18 -0
- package/dist/lib/sync/engine.js +517 -0
- package/dist/lib/sync/google-drive.d.ts +52 -0
- package/dist/lib/sync/google-drive.js +183 -0
- package/dist/lib/sync/merge.d.ts +1 -0
- package/dist/lib/sync/merge.js +68 -0
- package/dist/lib/yjs/YjsProvider.d.ts +11 -0
- package/dist/lib/yjs/YjsProvider.js +33 -0
- package/dist/lib/yjs/graph.d.ts +11 -0
- package/dist/lib/yjs/graph.js +7 -0
- package/dist/lib/yjs/hooks.d.ts +7 -0
- package/dist/lib/yjs/hooks.js +37 -0
- package/dist/lib/yjs/store.d.ts +4 -0
- package/dist/lib/yjs/store.js +19 -0
- package/dist/lib/yjs/syncGraph.d.ts +1 -0
- package/dist/lib/yjs/syncGraph.js +38 -0
- package/dist/providers/theme-provider.d.ts +3 -0
- package/dist/providers/theme-provider.js +18 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- 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
|
+
}
|