unforgit 0.6.0 → 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.
@@ -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 EMBEDDING_MODEL = "text-embedding-3-small";
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-your-api-key-here" && key.startsWith("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 pass apiKey option. Semantic search features are disabled. Unforgit will use FTS-only search."
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 = config2?.model ?? EMBEDDING_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
- model: "text-embedding-3-small",
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(orgId, repoId);
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' AND e.memory_id IS NULL`
3318
- ).all(orgId, repoId);
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-CKUDYQYP.js.map
3795
+ //# sourceMappingURL=chunk-TGDR7Y7T.js.map