@spark-agents/engram 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/sync.js ADDED
@@ -0,0 +1,516 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { watch } from "chokidar";
5
+ import { chunkMarkdown } from "./chunker.js";
6
+ import { MEDIA_EXTENSIONS, MEDIA_MIME_TYPES, } from "./types.js";
7
+ const DEFAULT_MAX_MEDIA_FILE_BYTES = 10 * 1024 * 1024;
8
+ function toRelPath(baseDir, absPath) {
9
+ return path.relative(baseDir, absPath).split(path.sep).join("/");
10
+ }
11
+ function walkDir(dir, baseDir, out) {
12
+ const entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
13
+ for (const entry of entries) {
14
+ const abs = path.join(dir, entry.name);
15
+ if (entry.isDirectory()) {
16
+ walkDir(abs, baseDir, out);
17
+ }
18
+ else if (entry.isFile() && entry.name.endsWith(".md")) {
19
+ out.push(toRelPath(baseDir, abs));
20
+ }
21
+ }
22
+ }
23
+ function discoverMemoryFiles(workspaceDir) {
24
+ const files = [];
25
+ if (fs.existsSync(workspaceDir) && fs.statSync(workspaceDir).isDirectory()) {
26
+ const rootEntries = new Set(fs.readdirSync(workspaceDir));
27
+ for (const name of ["MEMORY.md", "memory.md"]) {
28
+ if (rootEntries.has(name)) {
29
+ files.push(name);
30
+ }
31
+ }
32
+ }
33
+ const memoryDir = path.join(workspaceDir, "memory");
34
+ if (fs.existsSync(memoryDir) && fs.statSync(memoryDir).isDirectory()) {
35
+ walkDir(memoryDir, workspaceDir, files);
36
+ }
37
+ return Array.from(new Set(files)).sort((a, b) => a.localeCompare(b));
38
+ }
39
+ function walkMediaDir(dir, baseDir, extensions, maxFileBytes, out) {
40
+ const entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
41
+ for (const entry of entries) {
42
+ const abs = path.join(dir, entry.name);
43
+ if (entry.isDirectory()) {
44
+ walkMediaDir(abs, baseDir, extensions, maxFileBytes, out);
45
+ continue;
46
+ }
47
+ if (!entry.isFile()) {
48
+ continue;
49
+ }
50
+ const ext = path.extname(entry.name).toLowerCase();
51
+ if (!extensions.has(ext)) {
52
+ continue;
53
+ }
54
+ const stat = fs.statSync(abs);
55
+ if (stat.size > maxFileBytes) {
56
+ continue;
57
+ }
58
+ out.push(toRelPath(baseDir, abs));
59
+ }
60
+ }
61
+ function discoverMediaFiles(workspaceDir, modalities, maxFileBytes) {
62
+ const files = [];
63
+ const extensions = new Set();
64
+ for (const modality of modalities) {
65
+ for (const ext of MEDIA_EXTENSIONS[modality] ?? []) {
66
+ extensions.add(ext.toLowerCase());
67
+ }
68
+ }
69
+ if (extensions.size === 0) {
70
+ return files;
71
+ }
72
+ if (fs.existsSync(workspaceDir) && fs.statSync(workspaceDir).isDirectory()) {
73
+ const rootEntries = fs
74
+ .readdirSync(workspaceDir, { withFileTypes: true })
75
+ .sort((a, b) => a.name.localeCompare(b.name));
76
+ for (const entry of rootEntries) {
77
+ if (!entry.isFile()) {
78
+ continue;
79
+ }
80
+ const ext = path.extname(entry.name).toLowerCase();
81
+ if (!extensions.has(ext)) {
82
+ continue;
83
+ }
84
+ const abs = path.join(workspaceDir, entry.name);
85
+ const stat = fs.statSync(abs);
86
+ if (stat.size > maxFileBytes) {
87
+ continue;
88
+ }
89
+ files.push(toRelPath(workspaceDir, abs));
90
+ }
91
+ }
92
+ const memoryDir = path.join(workspaceDir, "memory");
93
+ if (fs.existsSync(memoryDir) && fs.statSync(memoryDir).isDirectory()) {
94
+ walkMediaDir(memoryDir, workspaceDir, extensions, maxFileBytes, files);
95
+ }
96
+ return Array.from(new Set(files)).sort((a, b) => a.localeCompare(b));
97
+ }
98
+ function discoverSessionFiles(sessionsDir) {
99
+ if (!(fs.existsSync(sessionsDir) && fs.statSync(sessionsDir).isDirectory())) {
100
+ return [];
101
+ }
102
+ const files = fs
103
+ .readdirSync(sessionsDir, { withFileTypes: true })
104
+ .sort((a, b) => a.name.localeCompare(b.name))
105
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl"))
106
+ .map((entry) => entry.name);
107
+ return files;
108
+ }
109
+ function hashContent(content) {
110
+ return createHash("sha256").update(content).digest("hex").slice(0, 16);
111
+ }
112
+ function hashBuffer(content) {
113
+ return createHash("sha256").update(content).digest("hex").slice(0, 16);
114
+ }
115
+ function mediaLabelPrefix(mimeType) {
116
+ if (mimeType.startsWith("image/")) {
117
+ return "Image";
118
+ }
119
+ if (mimeType.startsWith("audio/")) {
120
+ return "Audio";
121
+ }
122
+ return "Media";
123
+ }
124
+ function buildWatchPatterns(multimodalEnabled, modalities) {
125
+ const patterns = new Set(["MEMORY.md", "memory.md", "memory/**/*.md"]);
126
+ if (!multimodalEnabled) {
127
+ return Array.from(patterns);
128
+ }
129
+ for (const modality of modalities) {
130
+ for (const ext of MEDIA_EXTENSIONS[modality] ?? []) {
131
+ const normalizedExt = ext.toLowerCase();
132
+ patterns.add(`*${normalizedExt}`);
133
+ patterns.add(`memory/**/*${normalizedExt}`);
134
+ }
135
+ }
136
+ return Array.from(patterns);
137
+ }
138
+ function flattenSessionContent(raw) {
139
+ if (typeof raw === "string") {
140
+ const normalized = raw.replace(/\r?\n+/g, " ").trim();
141
+ return normalized.length > 0 ? normalized : null;
142
+ }
143
+ if (!Array.isArray(raw)) {
144
+ return null;
145
+ }
146
+ const parts = [];
147
+ for (const block of raw) {
148
+ if (typeof block !== "object" || block === null) {
149
+ continue;
150
+ }
151
+ const type = "type" in block ? block.type : undefined;
152
+ const text = "text" in block ? block.text : undefined;
153
+ if (type === "text" && typeof text === "string") {
154
+ const normalized = text.replace(/\r?\n+/g, " ").trim();
155
+ if (normalized.length > 0) {
156
+ parts.push(normalized);
157
+ }
158
+ }
159
+ }
160
+ if (parts.length === 0) {
161
+ return null;
162
+ }
163
+ return parts.join(" ");
164
+ }
165
+ export function flattenSessionJsonl(content) {
166
+ if (content.trim().length === 0) {
167
+ return null;
168
+ }
169
+ const flattenedLines = [];
170
+ const lineMap = [];
171
+ const lines = content.split(/\r?\n/);
172
+ for (let i = 0; i < lines.length; i += 1) {
173
+ const rawLine = lines[i];
174
+ if (rawLine.trim().length === 0) {
175
+ continue;
176
+ }
177
+ let parsed;
178
+ try {
179
+ parsed = JSON.parse(rawLine);
180
+ }
181
+ catch {
182
+ continue;
183
+ }
184
+ if (typeof parsed !== "object" || parsed === null) {
185
+ continue;
186
+ }
187
+ // Support both formats:
188
+ // 1. { role: "user", content: ... } (flat format)
189
+ // 2. { type: "message", message: { role: "user", content: ... } } (OpenClaw session format)
190
+ const inner = "message" in parsed && typeof parsed.message === "object" && parsed.message !== null
191
+ ? parsed.message
192
+ : parsed;
193
+ const role = "role" in inner ? inner.role : undefined;
194
+ if (role === "system") {
195
+ continue;
196
+ }
197
+ let prefix = null;
198
+ if (role === "user") {
199
+ prefix = "User: ";
200
+ }
201
+ else if (role === "assistant") {
202
+ prefix = "Assistant: ";
203
+ }
204
+ if (prefix === null) {
205
+ continue;
206
+ }
207
+ const rawMessage = "content" in inner ? inner.content : undefined;
208
+ const message = flattenSessionContent(rawMessage);
209
+ if (message === null) {
210
+ continue;
211
+ }
212
+ flattenedLines.push(`${prefix}${message}`);
213
+ lineMap.push(i + 1);
214
+ }
215
+ if (flattenedLines.length === 0) {
216
+ return null;
217
+ }
218
+ return {
219
+ text: flattenedLines.join("\n"),
220
+ lineMap,
221
+ };
222
+ }
223
+ export function remapChunkLines(chunks, lineMap) {
224
+ for (const chunk of chunks) {
225
+ const startIdx = chunk.startLine - 1;
226
+ const endIdx = chunk.endLine - 1;
227
+ if (startIdx >= 0 && startIdx < lineMap.length) {
228
+ chunk.startLine = lineMap[startIdx];
229
+ }
230
+ if (endIdx >= 0 && endIdx < lineMap.length) {
231
+ chunk.endLine = lineMap[endIdx];
232
+ }
233
+ }
234
+ }
235
+ export function createSyncManager(params) {
236
+ const { workspaceDir, index, embedding, chunkTokens, chunkOverlap, sessionsDir } = params;
237
+ const multimodalEnabled = params.multimodal?.enabled === true;
238
+ const multimodalModalities = params.multimodal?.modalities ?? [];
239
+ const maxMediaFileBytes = Math.max(1, Math.trunc(params.multimodal?.maxFileBytes ?? DEFAULT_MAX_MEDIA_FILE_BYTES));
240
+ const trackedFiles = new Set();
241
+ const warmedSessions = new Set();
242
+ let dirty = true;
243
+ let closed = false;
244
+ let watcher = null;
245
+ let sessionWatcher = null;
246
+ let syncTimer = null;
247
+ let intervalTimer = null;
248
+ let debounceMs = 1000;
249
+ function markDirty() {
250
+ dirty = true;
251
+ }
252
+ function isDirty() {
253
+ return dirty;
254
+ }
255
+ function clearSyncTimer() {
256
+ if (syncTimer !== null) {
257
+ clearTimeout(syncTimer);
258
+ syncTimer = null;
259
+ }
260
+ }
261
+ function clearIntervalTimer() {
262
+ if (intervalTimer !== null) {
263
+ clearInterval(intervalTimer);
264
+ intervalTimer = null;
265
+ }
266
+ }
267
+ function closeWatcher() {
268
+ if (watcher !== null) {
269
+ void watcher.close().catch(() => { });
270
+ watcher = null;
271
+ }
272
+ if (sessionWatcher !== null) {
273
+ void sessionWatcher.close().catch(() => { });
274
+ sessionWatcher = null;
275
+ }
276
+ }
277
+ async function sync(opts) {
278
+ if (closed) {
279
+ return;
280
+ }
281
+ const progress = opts?.progress;
282
+ const force = opts?.force === true;
283
+ const memoryFiles = discoverMemoryFiles(workspaceDir);
284
+ const mediaFiles = multimodalEnabled && multimodalModalities.length > 0
285
+ ? discoverMediaFiles(workspaceDir, multimodalModalities, maxMediaFileBytes)
286
+ : [];
287
+ const sessionFiles = sessionsDir ? discoverSessionFiles(sessionsDir) : [];
288
+ const seenFiles = new Set();
289
+ let completed = 0;
290
+ const total = memoryFiles.length + mediaFiles.length + sessionFiles.length;
291
+ if (total === 0) {
292
+ progress?.({ completed: 0, total: 0, label: "No memory/session/media files found" });
293
+ }
294
+ try {
295
+ for (const relPath of memoryFiles) {
296
+ seenFiles.add(relPath);
297
+ trackedFiles.add(relPath);
298
+ const absPath = path.join(workspaceDir, relPath);
299
+ const content = fs.readFileSync(absPath, "utf8");
300
+ const contentHash = hashContent(content);
301
+ const existingHash = index.getFileHash(relPath);
302
+ if (!force && existingHash === contentHash) {
303
+ completed += 1;
304
+ progress?.({ completed, total, label: `Skipped ${relPath}` });
305
+ continue;
306
+ }
307
+ const chunks = chunkMarkdown(content, {
308
+ maxTokens: chunkTokens,
309
+ overlapRatio: chunkOverlap,
310
+ });
311
+ const texts = chunks.map((chunk) => chunk.text);
312
+ const vectors = texts.length === 0 ? [] : await embedding.embedBatch(texts, "RETRIEVAL_DOCUMENT");
313
+ const file = {
314
+ fileKey: relPath,
315
+ contentHash,
316
+ source: "memory",
317
+ indexedAt: Date.now(),
318
+ };
319
+ index.indexFile(file, chunks, vectors);
320
+ completed += 1;
321
+ progress?.({ completed, total, label: `Indexed ${relPath}` });
322
+ }
323
+ if (mediaFiles.length > 0) {
324
+ if (!embedding.supportsMultimodal || embedding.embedMedia === undefined) {
325
+ console.warn("Engram: multimodal indexing is enabled, but the current embedding client does not support media. Skipping media files.");
326
+ for (const relPath of mediaFiles) {
327
+ seenFiles.add(relPath);
328
+ trackedFiles.add(relPath);
329
+ completed += 1;
330
+ progress?.({ completed, total, label: `Skipped ${relPath} (multimodal unsupported)` });
331
+ }
332
+ }
333
+ else {
334
+ for (const relPath of mediaFiles) {
335
+ seenFiles.add(relPath);
336
+ trackedFiles.add(relPath);
337
+ const absPath = path.join(workspaceDir, relPath);
338
+ const ext = path.extname(relPath).toLowerCase();
339
+ const mimeType = MEDIA_MIME_TYPES[ext];
340
+ if (!mimeType) {
341
+ completed += 1;
342
+ progress?.({ completed, total, label: `Skipped ${relPath} (unknown mime type)` });
343
+ continue;
344
+ }
345
+ // Enforce size limits before reading file contents.
346
+ const stat = fs.statSync(absPath);
347
+ if (stat.size > maxMediaFileBytes) {
348
+ completed += 1;
349
+ progress?.({ completed, total, label: `Skipped ${relPath} (file too large)` });
350
+ continue;
351
+ }
352
+ const content = fs.readFileSync(absPath);
353
+ const contentHash = hashBuffer(content);
354
+ const existingHash = index.getFileHash(relPath);
355
+ if (!force && existingHash === contentHash) {
356
+ completed += 1;
357
+ progress?.({ completed, total, label: `Skipped ${relPath}` });
358
+ continue;
359
+ }
360
+ const vector = await embedding.embedMedia(content, mimeType);
361
+ const prefix = mediaLabelPrefix(mimeType);
362
+ const chunks = [
363
+ {
364
+ text: `${prefix} file: ${relPath}`,
365
+ startLine: 1,
366
+ endLine: 1,
367
+ hash: contentHash,
368
+ },
369
+ ];
370
+ const file = {
371
+ fileKey: relPath,
372
+ contentHash,
373
+ source: "memory",
374
+ indexedAt: Date.now(),
375
+ };
376
+ index.indexFile(file, chunks, [vector]);
377
+ completed += 1;
378
+ progress?.({ completed, total, label: `Indexed ${relPath}` });
379
+ }
380
+ }
381
+ }
382
+ for (const sessionName of sessionFiles) {
383
+ const relPath = path.posix.join("sessions", sessionName);
384
+ seenFiles.add(relPath);
385
+ trackedFiles.add(relPath);
386
+ const absPath = path.join(sessionsDir ?? "", sessionName);
387
+ const content = fs.readFileSync(absPath, "utf8");
388
+ const contentHash = hashContent(content);
389
+ const existingHash = index.getFileHash(relPath);
390
+ if (!force && existingHash === contentHash) {
391
+ completed += 1;
392
+ progress?.({ completed, total, label: `Skipped ${relPath}` });
393
+ continue;
394
+ }
395
+ const flattened = flattenSessionJsonl(content);
396
+ let chunks = [];
397
+ if (flattened !== null) {
398
+ chunks = chunkMarkdown(flattened.text, {
399
+ maxTokens: chunkTokens,
400
+ overlapRatio: chunkOverlap,
401
+ });
402
+ remapChunkLines(chunks, flattened.lineMap);
403
+ }
404
+ const texts = chunks.map((chunk) => chunk.text);
405
+ const vectors = texts.length === 0 ? [] : await embedding.embedBatch(texts, "RETRIEVAL_DOCUMENT");
406
+ const file = {
407
+ fileKey: relPath,
408
+ contentHash,
409
+ source: "sessions",
410
+ indexedAt: Date.now(),
411
+ };
412
+ index.indexFile(file, chunks, vectors);
413
+ completed += 1;
414
+ progress?.({ completed, total, label: `Indexed ${relPath}` });
415
+ }
416
+ for (const relPath of Array.from(trackedFiles)) {
417
+ if (seenFiles.has(relPath)) {
418
+ continue;
419
+ }
420
+ index.removeFile(relPath);
421
+ trackedFiles.delete(relPath);
422
+ }
423
+ dirty = false;
424
+ }
425
+ catch (error) {
426
+ dirty = true;
427
+ throw error;
428
+ }
429
+ }
430
+ function scheduleDebouncedSync() {
431
+ clearSyncTimer();
432
+ syncTimer = setTimeout(() => {
433
+ void sync({ reason: "file-change" }).catch(() => { });
434
+ }, debounceMs);
435
+ }
436
+ return {
437
+ sync,
438
+ markDirty,
439
+ isDirty,
440
+ startWatching(opts) {
441
+ if (closed) {
442
+ return;
443
+ }
444
+ closeWatcher();
445
+ clearSyncTimer();
446
+ clearIntervalTimer();
447
+ debounceMs = Math.max(0, Math.trunc(opts?.debounceMs ?? 1000));
448
+ const intervalMinutes = Math.max(0, Math.trunc(opts?.intervalMinutes ?? 5));
449
+ const watchPatterns = buildWatchPatterns(multimodalEnabled, multimodalModalities);
450
+ watcher = watch(watchPatterns, {
451
+ cwd: workspaceDir,
452
+ ignoreInitial: true,
453
+ awaitWriteFinish: {
454
+ stabilityThreshold: debounceMs,
455
+ pollInterval: 100,
456
+ },
457
+ });
458
+ const onFsUpdate = () => {
459
+ markDirty();
460
+ scheduleDebouncedSync();
461
+ };
462
+ watcher.on("add", onFsUpdate);
463
+ watcher.on("change", onFsUpdate);
464
+ watcher.on("unlink", onFsUpdate);
465
+ if (sessionsDir && fs.existsSync(sessionsDir)) {
466
+ sessionWatcher = watch("*.jsonl", {
467
+ cwd: sessionsDir,
468
+ ignoreInitial: true,
469
+ awaitWriteFinish: {
470
+ stabilityThreshold: debounceMs,
471
+ pollInterval: 100,
472
+ },
473
+ });
474
+ sessionWatcher.on("add", onFsUpdate);
475
+ sessionWatcher.on("change", onFsUpdate);
476
+ sessionWatcher.on("unlink", onFsUpdate);
477
+ }
478
+ if (intervalMinutes > 0) {
479
+ intervalTimer = setInterval(() => {
480
+ if (isDirty()) {
481
+ void sync({ reason: "interval" }).catch(() => { });
482
+ }
483
+ }, intervalMinutes * 60 * 1000);
484
+ }
485
+ },
486
+ async warmSession(sessionKey) {
487
+ if (closed) {
488
+ return;
489
+ }
490
+ const key = sessionKey ?? "__default__";
491
+ if (warmedSessions.has(key)) {
492
+ return;
493
+ }
494
+ warmedSessions.add(key);
495
+ if (isDirty()) {
496
+ await sync({ reason: `warm-session:${key}` });
497
+ }
498
+ },
499
+ syncIfDirty() {
500
+ if (closed) {
501
+ return;
502
+ }
503
+ if (isDirty()) {
504
+ void sync({ reason: "on-search" }).catch(() => { });
505
+ }
506
+ },
507
+ close() {
508
+ closed = true;
509
+ closeWatcher();
510
+ clearSyncTimer();
511
+ clearIntervalTimer();
512
+ trackedFiles.clear();
513
+ warmedSessions.clear();
514
+ },
515
+ };
516
+ }
@@ -0,0 +1,111 @@
1
+ export type MediaModality = "image" | "audio";
2
+ export declare const MEDIA_EXTENSIONS: Record<MediaModality, string[]>;
3
+ export declare const MEDIA_MIME_TYPES: Record<string, string>;
4
+ export interface EngramConfig {
5
+ geminiApiKey?: string;
6
+ dimensions?: 768 | 1536 | 3072;
7
+ chunkTokens?: number;
8
+ chunkOverlap?: number;
9
+ reranking?: boolean;
10
+ timeDecay?: {
11
+ enabled?: boolean;
12
+ halfLifeDays?: number;
13
+ };
14
+ maxSessionShare?: number;
15
+ multimodal?: {
16
+ enabled?: boolean;
17
+ modalities?: MediaModality[];
18
+ maxFileBytes?: number;
19
+ };
20
+ }
21
+ export declare const DEFAULT_CONFIG: Required<Pick<EngramConfig, "dimensions" | "chunkTokens" | "chunkOverlap" | "maxSessionShare">> & {
22
+ timeDecay: Required<NonNullable<EngramConfig["timeDecay"]>>;
23
+ };
24
+ export type GeminiTaskType = "RETRIEVAL_DOCUMENT" | "RETRIEVAL_QUERY";
25
+ export interface EmbeddingClient {
26
+ embedText(text: string, taskType: GeminiTaskType): Promise<Float32Array>;
27
+ embedBatch(texts: string[], taskType: GeminiTaskType): Promise<Float32Array[]>;
28
+ /** Embed a binary file (image, audio, PDF). Returns a normalized Float32Array. */
29
+ embedMedia?(data: Buffer, mimeType: string): Promise<Float32Array>;
30
+ /** Whether this client supports multimodal embedding. */
31
+ readonly supportsMultimodal: boolean;
32
+ dimensions: number;
33
+ model: string;
34
+ }
35
+ export interface Chunk {
36
+ text: string;
37
+ startLine: number;
38
+ endLine: number;
39
+ hash: string;
40
+ headingContext?: string;
41
+ }
42
+ export interface ChunkerOptions {
43
+ maxTokens: number;
44
+ overlapRatio: number;
45
+ }
46
+ export type MemorySource = "memory" | "sessions";
47
+ export interface StoredFile {
48
+ fileKey: string;
49
+ contentHash: string;
50
+ source: MemorySource;
51
+ indexedAt: number;
52
+ }
53
+ export interface ScoredChunk {
54
+ fileKey: string;
55
+ startLine: number;
56
+ endLine: number;
57
+ text: string;
58
+ score: number;
59
+ source: MemorySource;
60
+ headingContext?: string;
61
+ indexedAt?: number;
62
+ }
63
+ export interface IndexStats {
64
+ files: number;
65
+ chunks: number;
66
+ sources: Array<{
67
+ source: MemorySource;
68
+ files: number;
69
+ chunks: number;
70
+ }>;
71
+ dbPath: string;
72
+ vectorDims: number;
73
+ }
74
+ export interface IndexManager {
75
+ indexFile(file: StoredFile, chunks: Chunk[], vectors: Float32Array[]): void;
76
+ removeFile(fileKey: string): void;
77
+ searchBM25(query: string, topK: number): ScoredChunk[];
78
+ searchVector(queryVec: Float32Array, topK: number): ScoredChunk[];
79
+ getFileHash(fileKey: string): string | null;
80
+ readFileContent(relPath: string, from?: number, lines?: number): {
81
+ text: string;
82
+ path: string;
83
+ } | null;
84
+ stats(): IndexStats;
85
+ close(): void;
86
+ }
87
+ export interface SearchOptions {
88
+ maxResults?: number;
89
+ minScore?: number;
90
+ sessionKey?: string;
91
+ }
92
+ export interface SearchResult {
93
+ path: string;
94
+ startLine: number;
95
+ endLine: number;
96
+ score: number;
97
+ snippet: string;
98
+ source: MemorySource;
99
+ citation?: string;
100
+ }
101
+ export interface SyncProgress {
102
+ completed: number;
103
+ total: number;
104
+ label?: string;
105
+ }
106
+ export interface SyncOptions {
107
+ reason?: string;
108
+ force?: boolean;
109
+ sessionFiles?: string[];
110
+ progress?: (update: SyncProgress) => void;
111
+ }
package/dist/types.js ADDED
@@ -0,0 +1,28 @@
1
+ export const MEDIA_EXTENSIONS = {
2
+ image: [".jpg", ".jpeg", ".png", ".webp", ".gif"],
3
+ audio: [".mp3", ".wav", ".ogg", ".opus", ".m4a", ".aac", ".flac"],
4
+ };
5
+ export const MEDIA_MIME_TYPES = {
6
+ ".jpg": "image/jpeg",
7
+ ".jpeg": "image/jpeg",
8
+ ".png": "image/png",
9
+ ".webp": "image/webp",
10
+ ".gif": "image/gif",
11
+ ".mp3": "audio/mpeg",
12
+ ".wav": "audio/wav",
13
+ ".ogg": "audio/ogg",
14
+ ".opus": "audio/opus",
15
+ ".m4a": "audio/mp4",
16
+ ".aac": "audio/aac",
17
+ ".flac": "audio/flac",
18
+ };
19
+ export const DEFAULT_CONFIG = {
20
+ dimensions: 768,
21
+ chunkTokens: 1024,
22
+ chunkOverlap: 0.15,
23
+ maxSessionShare: 0.4,
24
+ timeDecay: {
25
+ enabled: true,
26
+ halfLifeDays: 30,
27
+ },
28
+ };