unforgit 0.5.6 → 0.7.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/{chunk-CKUDYQYP.js → chunk-TGDR7Y7T.js} +161 -18
- package/dist/chunk-TGDR7Y7T.js.map +1 -0
- package/dist/index.js +425 -210
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +1 -1
- package/package.json +3 -3
- package/dist/chunk-CKUDYQYP.js.map +0 -1
|
@@ -8,6 +8,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
8
8
|
|
|
9
9
|
// ../../packages/core/dist/index.js
|
|
10
10
|
import OpenAI from "openai";
|
|
11
|
+
import { createHash } from "crypto";
|
|
11
12
|
import OpenAI2 from "openai";
|
|
12
13
|
var DEFAULT_TTL_SECONDS_BY_TYPE = {
|
|
13
14
|
episodic: 30 * 24 * 60 * 60,
|
|
@@ -93,6 +94,8 @@ var LifecycleScheduler = class {
|
|
|
93
94
|
this.runner = runner;
|
|
94
95
|
this.options = options;
|
|
95
96
|
}
|
|
97
|
+
runner;
|
|
98
|
+
options;
|
|
96
99
|
states = /* @__PURE__ */ new Map();
|
|
97
100
|
schedule(orgId, repoId) {
|
|
98
101
|
const key = `${orgId}:${repoId}`;
|
|
@@ -229,17 +232,65 @@ function resolveVisibility(input) {
|
|
|
229
232
|
}
|
|
230
233
|
return { visibility: "private", suggestion: "promote" };
|
|
231
234
|
}
|
|
232
|
-
var
|
|
235
|
+
var OPENAI_EMBEDDING_MODEL = "text-embedding-3-small";
|
|
236
|
+
var OPENAI_EMBEDDING_DIMENSIONS = 1536;
|
|
237
|
+
var LOCAL_EMBEDDING_MODEL = "local-hash-multilingual-v1";
|
|
238
|
+
var LOCAL_EMBEDDING_DIMENSIONS = 384;
|
|
233
239
|
var cachedClient = null;
|
|
234
240
|
function isOpenAIConfigured(apiKey) {
|
|
235
241
|
const key = apiKey ?? process.env.OPENAI_API_KEY;
|
|
236
|
-
return !!key && key !== "sk-
|
|
242
|
+
return !!key && key !== "sk-you...here" && key.startsWith("sk-");
|
|
243
|
+
}
|
|
244
|
+
function resolveEmbeddingProvider(config2) {
|
|
245
|
+
const requested = config2?.provider ?? "auto";
|
|
246
|
+
const openaiAvailable = isOpenAIConfigured(config2?.apiKey);
|
|
247
|
+
if (requested === "disabled") {
|
|
248
|
+
return {
|
|
249
|
+
provider: "disabled",
|
|
250
|
+
model: config2?.model ?? "disabled",
|
|
251
|
+
dimensions: 0,
|
|
252
|
+
available: false,
|
|
253
|
+
reason: "Embeddings are disabled by configuration."
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
if (requested === "openai") {
|
|
257
|
+
const model = config2?.model ?? OPENAI_EMBEDDING_MODEL;
|
|
258
|
+
return {
|
|
259
|
+
provider: "openai",
|
|
260
|
+
model,
|
|
261
|
+
dimensions: getEmbeddingDimensions(model),
|
|
262
|
+
available: openaiAvailable,
|
|
263
|
+
reason: openaiAvailable ? void 0 : "OPENAI_API_KEY is not configured."
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
if (requested === "local") {
|
|
267
|
+
return {
|
|
268
|
+
provider: "local",
|
|
269
|
+
model: config2?.model ?? LOCAL_EMBEDDING_MODEL,
|
|
270
|
+
dimensions: LOCAL_EMBEDDING_DIMENSIONS,
|
|
271
|
+
available: true
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
if (openaiAvailable && config2?.model && config2.model.startsWith("text-embedding-")) {
|
|
275
|
+
return {
|
|
276
|
+
provider: "openai",
|
|
277
|
+
model: config2.model,
|
|
278
|
+
dimensions: getEmbeddingDimensions(config2.model),
|
|
279
|
+
available: true
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
provider: "local",
|
|
284
|
+
model: config2?.model && !config2.model.startsWith("text-embedding-") ? config2.model : LOCAL_EMBEDDING_MODEL,
|
|
285
|
+
dimensions: LOCAL_EMBEDDING_DIMENSIONS,
|
|
286
|
+
available: true
|
|
287
|
+
};
|
|
237
288
|
}
|
|
238
289
|
function getClient(apiKey) {
|
|
239
290
|
const key = apiKey ?? process.env.OPENAI_API_KEY;
|
|
240
291
|
if (!key) {
|
|
241
292
|
throw new Error(
|
|
242
|
-
"OpenAI API key not configured. Set OPENAI_API_KEY environment variable or
|
|
293
|
+
"OpenAI API key not configured. Set OPENAI_API_KEY environment variable or use embeddings.provider=local. Unforgit can generate local embeddings without cloud credentials."
|
|
243
294
|
);
|
|
244
295
|
}
|
|
245
296
|
if (!cachedClient || apiKey) {
|
|
@@ -247,9 +298,55 @@ function getClient(apiKey) {
|
|
|
247
298
|
}
|
|
248
299
|
return cachedClient;
|
|
249
300
|
}
|
|
301
|
+
function hashToUint32(value) {
|
|
302
|
+
const digest = createHash("sha256").update(value).digest();
|
|
303
|
+
return digest.readUInt32LE(0);
|
|
304
|
+
}
|
|
305
|
+
function normalizeToken(token) {
|
|
306
|
+
return token.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
|
|
307
|
+
}
|
|
308
|
+
function featuresForText(text) {
|
|
309
|
+
const words = text.split(/[^\p{L}\p{N}_-]+/u).map(normalizeToken).filter((token) => token.length > 1);
|
|
310
|
+
const features = [];
|
|
311
|
+
for (const word of words) {
|
|
312
|
+
features.push(`w:${word}`);
|
|
313
|
+
if (word.length > 4) {
|
|
314
|
+
for (let i = 0; i <= word.length - 3; i++) {
|
|
315
|
+
features.push(`g:${word.slice(i, i + 3)}`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
for (let i = 0; i < words.length - 1; i++) {
|
|
320
|
+
features.push(`b:${words[i]} ${words[i + 1]}`);
|
|
321
|
+
}
|
|
322
|
+
return features.length > 0 ? features : ["empty"];
|
|
323
|
+
}
|
|
324
|
+
function generateLocalEmbedding(text, model = LOCAL_EMBEDDING_MODEL) {
|
|
325
|
+
const vector = new Array(LOCAL_EMBEDDING_DIMENSIONS).fill(0);
|
|
326
|
+
for (const feature of featuresForText(text.trim().slice(0, 8e3))) {
|
|
327
|
+
const bucket = hashToUint32(`${model}:bucket:${feature}`) % LOCAL_EMBEDDING_DIMENSIONS;
|
|
328
|
+
const sign = hashToUint32(`${model}:sign:${feature}`) % 2 === 0 ? 1 : -1;
|
|
329
|
+
vector[bucket] += sign;
|
|
330
|
+
}
|
|
331
|
+
const magnitude = Math.sqrt(vector.reduce((sum, value) => sum + value * value, 0));
|
|
332
|
+
const embedding = magnitude > 0 ? vector.map((value) => value / magnitude) : vector;
|
|
333
|
+
return {
|
|
334
|
+
embedding,
|
|
335
|
+
model,
|
|
336
|
+
provider: "local",
|
|
337
|
+
tokensUsed: 0
|
|
338
|
+
};
|
|
339
|
+
}
|
|
250
340
|
async function generateEmbedding(text, config2) {
|
|
341
|
+
const resolved = resolveEmbeddingProvider(config2);
|
|
342
|
+
if (resolved.provider === "disabled") {
|
|
343
|
+
throw new Error("Embeddings are disabled by configuration.");
|
|
344
|
+
}
|
|
345
|
+
if (resolved.provider === "local") {
|
|
346
|
+
return generateLocalEmbedding(text, resolved.model);
|
|
347
|
+
}
|
|
251
348
|
const client = getClient(config2?.apiKey);
|
|
252
|
-
const model =
|
|
349
|
+
const model = resolved.model;
|
|
253
350
|
const cleanText = text.trim().slice(0, 8e3);
|
|
254
351
|
const response = await client.embeddings.create({
|
|
255
352
|
model,
|
|
@@ -262,6 +359,7 @@ async function generateEmbedding(text, config2) {
|
|
|
262
359
|
return {
|
|
263
360
|
embedding: data.embedding,
|
|
264
361
|
model,
|
|
362
|
+
provider: "openai",
|
|
265
363
|
tokensUsed: response.usage?.total_tokens ?? 0
|
|
266
364
|
};
|
|
267
365
|
}
|
|
@@ -298,6 +396,15 @@ function deserializeEmbedding(buffer) {
|
|
|
298
396
|
}
|
|
299
397
|
return embedding;
|
|
300
398
|
}
|
|
399
|
+
var EMBEDDING_DIMENSIONS_MAP = {
|
|
400
|
+
[LOCAL_EMBEDDING_MODEL]: LOCAL_EMBEDDING_DIMENSIONS,
|
|
401
|
+
"text-embedding-3-small": 1536,
|
|
402
|
+
"text-embedding-3-large": 3072,
|
|
403
|
+
"text-embedding-ada-002": 1536
|
|
404
|
+
};
|
|
405
|
+
function getEmbeddingDimensions(model) {
|
|
406
|
+
return EMBEDDING_DIMENSIONS_MAP[model] ?? (model.startsWith("local-") ? LOCAL_EMBEDDING_DIMENSIONS : OPENAI_EMBEDDING_DIMENSIONS);
|
|
407
|
+
}
|
|
301
408
|
var CONSOLIDATION_PROMPT = `You are consolidating multiple related memories into a single unified memory.
|
|
302
409
|
|
|
303
410
|
Source memories:
|
|
@@ -1373,6 +1480,7 @@ var syncConfigSchema = z.object({
|
|
|
1373
1480
|
});
|
|
1374
1481
|
var embeddingConfigSchema = z.object({
|
|
1375
1482
|
enabled: z.boolean(),
|
|
1483
|
+
provider: z.enum(["auto", "local", "openai", "disabled"]).optional(),
|
|
1376
1484
|
model: z.string(),
|
|
1377
1485
|
autoGenerate: z.boolean()
|
|
1378
1486
|
});
|
|
@@ -1625,7 +1733,8 @@ function defaultConfig() {
|
|
|
1625
1733
|
},
|
|
1626
1734
|
embeddings: {
|
|
1627
1735
|
enabled: true,
|
|
1628
|
-
|
|
1736
|
+
provider: "auto",
|
|
1737
|
+
model: "local-hash-multilingual-v1",
|
|
1629
1738
|
autoGenerate: true
|
|
1630
1739
|
},
|
|
1631
1740
|
lifecycle: resolveLifecycleConfig()
|
|
@@ -1643,6 +1752,7 @@ var RemoteClient = class {
|
|
|
1643
1752
|
this.apiKey = apiKey || process.env.UNFORGIT_API_KEY;
|
|
1644
1753
|
this.timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
1645
1754
|
}
|
|
1755
|
+
baseUrl;
|
|
1646
1756
|
apiKey;
|
|
1647
1757
|
timeoutMs;
|
|
1648
1758
|
getHeaders() {
|
|
@@ -1998,6 +2108,8 @@ CREATE TABLE IF NOT EXISTS memory_embeddings (
|
|
|
1998
2108
|
memory_id TEXT PRIMARY KEY REFERENCES memories(id) ON DELETE CASCADE,
|
|
1999
2109
|
embedding BLOB NOT NULL,
|
|
2000
2110
|
model TEXT NOT NULL,
|
|
2111
|
+
provider TEXT,
|
|
2112
|
+
dimensions INTEGER,
|
|
2001
2113
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2002
2114
|
);
|
|
2003
2115
|
|
|
@@ -2184,10 +2296,21 @@ var LocalStore = class {
|
|
|
2184
2296
|
memory_id TEXT PRIMARY KEY REFERENCES memories(id) ON DELETE CASCADE,
|
|
2185
2297
|
embedding BLOB NOT NULL,
|
|
2186
2298
|
model TEXT NOT NULL,
|
|
2299
|
+
provider TEXT,
|
|
2300
|
+
dimensions INTEGER,
|
|
2187
2301
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2188
2302
|
);
|
|
2189
2303
|
`);
|
|
2190
2304
|
}
|
|
2305
|
+
const embeddingColumns = this.db.prepare("PRAGMA table_info(memory_embeddings)").all();
|
|
2306
|
+
const embeddingColumnNames = embeddingColumns.map((c) => c.name);
|
|
2307
|
+
if (!embeddingColumnNames.includes("provider")) {
|
|
2308
|
+
this.db.exec("ALTER TABLE memory_embeddings ADD COLUMN provider TEXT");
|
|
2309
|
+
}
|
|
2310
|
+
if (!embeddingColumnNames.includes("dimensions")) {
|
|
2311
|
+
this.db.exec("ALTER TABLE memory_embeddings ADD COLUMN dimensions INTEGER");
|
|
2312
|
+
this.db.prepare("UPDATE memory_embeddings SET dimensions = length(embedding) / 4 WHERE dimensions IS NULL").run();
|
|
2313
|
+
}
|
|
2191
2314
|
const usageTables = this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='memory_usage'").all();
|
|
2192
2315
|
if (usageTables.length === 0) {
|
|
2193
2316
|
this.db.exec(`
|
|
@@ -3274,18 +3397,19 @@ var LocalStore = class {
|
|
|
3274
3397
|
).run();
|
|
3275
3398
|
return result.changes;
|
|
3276
3399
|
}
|
|
3277
|
-
async storeEmbedding(memoryId, embedding, model) {
|
|
3400
|
+
async storeEmbedding(memoryId, embedding, model, provider) {
|
|
3278
3401
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3279
3402
|
const blob = serializeEmbedding(embedding);
|
|
3403
|
+
const dimensions = embedding.length || getEmbeddingDimensions(model);
|
|
3280
3404
|
this.db.prepare(
|
|
3281
|
-
`INSERT OR REPLACE INTO memory_embeddings (memory_id, embedding, model, created_at)
|
|
3282
|
-
VALUES (?, ?, ?, ?)`
|
|
3283
|
-
).run(memoryId, blob, model, now);
|
|
3405
|
+
`INSERT OR REPLACE INTO memory_embeddings (memory_id, embedding, model, provider, dimensions, created_at)
|
|
3406
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
3407
|
+
).run(memoryId, blob, model, provider ?? null, dimensions, now);
|
|
3284
3408
|
}
|
|
3285
3409
|
async generateAndStoreEmbedding(memoryId, text, config2) {
|
|
3286
3410
|
try {
|
|
3287
3411
|
const result = await generateEmbedding(text, config2);
|
|
3288
|
-
await this.storeEmbedding(memoryId, result.embedding, result.model);
|
|
3412
|
+
await this.storeEmbedding(memoryId, result.embedding, result.model, result.provider);
|
|
3289
3413
|
} catch (error) {
|
|
3290
3414
|
console.error(`Failed to generate embedding for ${memoryId}:`, error);
|
|
3291
3415
|
}
|
|
@@ -3299,23 +3423,40 @@ var LocalStore = class {
|
|
|
3299
3423
|
const row = this.db.prepare("SELECT 1 FROM memory_embeddings WHERE memory_id = ?").get(memoryId);
|
|
3300
3424
|
return !!row;
|
|
3301
3425
|
}
|
|
3302
|
-
getAllEmbeddings(orgId, repoId) {
|
|
3426
|
+
getAllEmbeddings(orgId, repoId, dimensions) {
|
|
3427
|
+
const dimensionClause = dimensions ? " AND e.dimensions = ?" : "";
|
|
3428
|
+
const params = dimensions ? [orgId, repoId, dimensions] : [orgId, repoId];
|
|
3303
3429
|
const rows = this.db.prepare(
|
|
3304
3430
|
`SELECT e.memory_id, e.embedding FROM memory_embeddings e
|
|
3305
3431
|
JOIN memories m ON e.memory_id = m.id
|
|
3306
|
-
WHERE m.org_id = ? AND m.repo_id = ? AND m.status = 'active'`
|
|
3307
|
-
).all(
|
|
3432
|
+
WHERE m.org_id = ? AND m.repo_id = ? AND m.status = 'active'${dimensionClause}`
|
|
3433
|
+
).all(...params);
|
|
3308
3434
|
return rows.map((row) => ({
|
|
3309
3435
|
memoryId: row.memory_id,
|
|
3310
3436
|
embedding: deserializeEmbedding(row.embedding)
|
|
3311
3437
|
}));
|
|
3312
3438
|
}
|
|
3313
|
-
getMemoriesWithoutEmbeddings(orgId, repoId) {
|
|
3439
|
+
getMemoriesWithoutEmbeddings(orgId, repoId, expected) {
|
|
3440
|
+
const compatibilityClauses = ["e.memory_id IS NULL"];
|
|
3441
|
+
const params = [orgId, repoId];
|
|
3442
|
+
if (expected?.model) {
|
|
3443
|
+
compatibilityClauses.push("e.model != ?");
|
|
3444
|
+
params.push(expected.model);
|
|
3445
|
+
}
|
|
3446
|
+
if (expected?.provider) {
|
|
3447
|
+
compatibilityClauses.push("e.provider IS NULL OR e.provider != ?");
|
|
3448
|
+
params.push(expected.provider);
|
|
3449
|
+
}
|
|
3450
|
+
if (expected?.dimensions) {
|
|
3451
|
+
compatibilityClauses.push("e.dimensions IS NULL OR e.dimensions != ?");
|
|
3452
|
+
params.push(expected.dimensions);
|
|
3453
|
+
}
|
|
3314
3454
|
const rows = this.db.prepare(
|
|
3315
3455
|
`SELECT m.* FROM memories m
|
|
3316
3456
|
LEFT JOIN memory_embeddings e ON m.id = e.memory_id
|
|
3317
|
-
WHERE m.org_id = ? AND m.repo_id = ? AND m.status = 'active'
|
|
3318
|
-
|
|
3457
|
+
WHERE m.org_id = ? AND m.repo_id = ? AND m.status = 'active'
|
|
3458
|
+
AND (${compatibilityClauses.map((clause) => `(${clause})`).join(" OR ")})`
|
|
3459
|
+
).all(...params);
|
|
3319
3460
|
return rows.map(rowToMemory);
|
|
3320
3461
|
}
|
|
3321
3462
|
async recallWithEmbeddings(query, queryEmbedding) {
|
|
@@ -3323,7 +3464,7 @@ var LocalStore = class {
|
|
|
3323
3464
|
if (!queryEmbedding) {
|
|
3324
3465
|
return ftsResults;
|
|
3325
3466
|
}
|
|
3326
|
-
const allEmbeddings = this.getAllEmbeddings(query.orgId, query.repoId);
|
|
3467
|
+
const allEmbeddings = this.getAllEmbeddings(query.orgId, query.repoId, queryEmbedding.length);
|
|
3327
3468
|
if (allEmbeddings.length === 0) {
|
|
3328
3469
|
return ftsResults;
|
|
3329
3470
|
}
|
|
@@ -3616,6 +3757,8 @@ export {
|
|
|
3616
3757
|
LifecycleScheduler,
|
|
3617
3758
|
mergeAndRank,
|
|
3618
3759
|
resolveVisibility,
|
|
3760
|
+
isOpenAIConfigured,
|
|
3761
|
+
resolveEmbeddingProvider,
|
|
3619
3762
|
generateEmbedding,
|
|
3620
3763
|
computeRepositoryHealth,
|
|
3621
3764
|
generateSuggestions,
|
|
@@ -3649,4 +3792,4 @@ export {
|
|
|
3649
3792
|
RemoteClient,
|
|
3650
3793
|
LocalStore
|
|
3651
3794
|
};
|
|
3652
|
-
//# sourceMappingURL=chunk-
|
|
3795
|
+
//# sourceMappingURL=chunk-TGDR7Y7T.js.map
|