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.
package/dist/index.js CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  getDbPath,
18
18
  getTemplate,
19
19
  isInitialized,
20
+ isOpenAIConfigured,
20
21
  loadConfig,
21
22
  mergeAndRank,
22
23
  parseConfidence,
@@ -24,12 +25,13 @@ import {
24
25
  parseThreshold,
25
26
  parseTtl,
26
27
  persistReviewableSuggestions,
28
+ resolveEmbeddingProvider,
27
29
  resolveLifecycleConfig,
28
30
  resolveVisibility,
29
31
  runLocalLifecycleMaintenance,
30
32
  saveConfig,
31
33
  validateMemoryType
32
- } from "./chunk-CKUDYQYP.js";
34
+ } from "./chunk-TGDR7Y7T.js";
33
35
 
34
36
  // src/index.ts
35
37
  import { Command as Command31 } from "commander";
@@ -2922,6 +2924,7 @@ function buildBackfillJsonPayload(options) {
2922
2924
  return {
2923
2925
  dryRun: options.dryRun,
2924
2926
  model: options.model,
2927
+ ...options.provider ? { provider: options.provider } : {},
2925
2928
  planned: options.planned,
2926
2929
  processed: options.processed,
2927
2930
  errors: failures.length,
@@ -2938,16 +2941,21 @@ function buildBackfillJsonPayload(options) {
2938
2941
  };
2939
2942
  }
2940
2943
  var embeddingsCommand = new Command24("embeddings").description("Manage memory embeddings for semantic search");
2941
- embeddingsCommand.command("backfill").description("Generate embeddings for memories that don't have them").option("--batch-size <n>", "Number of memories to process in parallel", "5").option("--delay <ms>", "Delay between batches (ms)", "500").option("--dry-run", "Show what would be done without making changes").option("--model <model>", "OpenAI embedding model", "text-embedding-3-small").action(async (opts) => {
2944
+ embeddingsCommand.command("backfill").description("Generate embeddings for memories that don't have them").option("--batch-size <n>", "Number of memories to process in parallel", "5").option("--delay <ms>", "Delay between batches (ms)", "500").option("--dry-run", "Show what would be done without making changes").option("--provider <provider>", "Embedding provider: auto, local, openai, disabled").option("--model <model>", "Embedding model (defaults to configured provider model)").action(async (opts) => {
2942
2945
  if (!isInitialized(cwd3)) {
2943
2946
  logger.error("Unforgit not initialized. Run 'unforgit init' first.");
2944
2947
  process.exit(EXIT_CONFIG_ERROR);
2945
2948
  }
2946
2949
  const config = loadConfig(cwd3);
2947
- const apiKey = process.env.OPENAI_API_KEY;
2948
- if (!apiKey && !opts.dryRun) {
2949
- logger.error("OpenAI API key not set.");
2950
- logger.error("Set the OPENAI_API_KEY environment variable.");
2950
+ const embeddingConfig = {
2951
+ provider: opts.provider ?? config.embeddings?.provider ?? "auto",
2952
+ model: opts.model ?? config.embeddings?.model,
2953
+ apiKey: process.env.OPENAI_API_KEY
2954
+ };
2955
+ const provider = resolveEmbeddingProvider(embeddingConfig);
2956
+ if (!provider.available && !opts.dryRun) {
2957
+ logger.error(provider.reason ?? "Embedding provider is not available.");
2958
+ logger.error("Use 'unforgit config set embeddings.provider local' for no-key local embeddings.");
2951
2959
  process.exit(EXIT_ERROR);
2952
2960
  }
2953
2961
  const dbPath = getDbPath(cwd3);
@@ -2955,12 +2963,17 @@ embeddingsCommand.command("backfill").description("Generate embeddings for memor
2955
2963
  const orgId = config.remote.orgId || "local";
2956
2964
  const repoId = config.remote.repoId || "local";
2957
2965
  try {
2958
- const memories = store.getMemoriesWithoutEmbeddings(orgId, repoId);
2966
+ const memories = store.getMemoriesWithoutEmbeddings(orgId, repoId, {
2967
+ model: provider.model,
2968
+ provider: provider.provider,
2969
+ dimensions: provider.dimensions
2970
+ });
2959
2971
  const stats = store.getEmbeddingStats(orgId, repoId);
2960
2972
  if (isJsonMode() && opts.dryRun) {
2961
2973
  outputJson(buildBackfillJsonPayload({
2962
2974
  dryRun: true,
2963
- model: opts.model,
2975
+ model: provider.model,
2976
+ provider: provider.provider,
2964
2977
  statsBefore: stats,
2965
2978
  planned: memories.length,
2966
2979
  processed: 0,
@@ -2981,7 +2994,8 @@ embeddingsCommand.command("backfill").description("Generate embeddings for memor
2981
2994
  if (isJsonMode()) {
2982
2995
  outputJson(buildBackfillJsonPayload({
2983
2996
  dryRun: Boolean(opts.dryRun),
2984
- model: opts.model,
2997
+ model: provider.model,
2998
+ provider: provider.provider,
2985
2999
  statsBefore: stats,
2986
3000
  planned: 0,
2987
3001
  processed: 0
@@ -2992,6 +3006,8 @@ embeddingsCommand.command("backfill").description("Generate embeddings for memor
2992
3006
  return;
2993
3007
  }
2994
3008
  logger.info(`Found ${memories.length} memories without embeddings.`);
3009
+ logger.info(`Embedding provider: ${provider.provider}`);
3010
+ logger.info(`Embedding model: ${provider.model}`);
2995
3011
  if (opts.dryRun) {
2996
3012
  logger.info("\n[Dry run - no changes made]");
2997
3013
  logger.info("Would generate embeddings for:");
@@ -3015,10 +3031,9 @@ Generating embeddings (batch size: ${batchSize}, delay: ${delay}ms)...`);
3015
3031
  batch.map(async (memory) => {
3016
3032
  try {
3017
3033
  const result = await generateEmbedding(memory.text, {
3018
- apiKey,
3019
- model: opts.model
3034
+ ...embeddingConfig
3020
3035
  });
3021
- await store.storeEmbedding(memory.id, result.embedding, result.model);
3036
+ await store.storeEmbedding(memory.id, result.embedding, result.model, result.provider);
3022
3037
  processed++;
3023
3038
  logger.progress(processed, memories.length, "embeddings");
3024
3039
  } catch (err) {
@@ -3044,7 +3059,8 @@ Backfill complete:`);
3044
3059
  if (isJsonMode()) {
3045
3060
  outputJson(buildBackfillJsonPayload({
3046
3061
  dryRun: false,
3047
- model: opts.model,
3062
+ model: provider.model,
3063
+ provider: provider.provider,
3048
3064
  statsBefore: stats,
3049
3065
  statsAfter,
3050
3066
  planned: memories.length,
@@ -3368,17 +3384,39 @@ var doctorCommand = new Command27("doctor").description("Check system health and
3368
3384
  status: "ok",
3369
3385
  message: `${memoryStats.total} memories (${memoryStats.byType.episodic} episodic, ${memoryStats.byType.semantic} semantic, ${memoryStats.byType.procedural} procedural)`
3370
3386
  });
3387
+ const provider = resolveEmbeddingProvider({
3388
+ provider: config.embeddings?.provider ?? "auto",
3389
+ model: config.embeddings?.model,
3390
+ apiKey: process.env.OPENAI_API_KEY
3391
+ });
3392
+ results.push({
3393
+ check: "embedding-provider",
3394
+ status: provider.available ? "ok" : "warn",
3395
+ message: `${provider.provider} embeddings (${provider.model}, ${provider.dimensions} dimensions)${provider.reason ? `: ${provider.reason}` : ""}`,
3396
+ details: {
3397
+ provider: provider.provider,
3398
+ model: provider.model,
3399
+ dimensions: provider.dimensions,
3400
+ available: provider.available
3401
+ }
3402
+ });
3371
3403
  const stats = store.getEmbeddingStats(orgId, repoId);
3404
+ const incompatibleOrMissing = store.getMemoriesWithoutEmbeddings(orgId, repoId, {
3405
+ model: provider.model,
3406
+ provider: provider.provider,
3407
+ dimensions: provider.dimensions
3408
+ }).length;
3372
3409
  if (stats.total === 0) {
3373
3410
  results.push({ check: "embeddings", status: "ok", message: "No memories yet" });
3374
- } else if (stats.withoutEmbedding === 0) {
3375
- results.push({ check: "embeddings", status: "ok", message: `All ${stats.total} memories have embeddings` });
3411
+ } else if (incompatibleOrMissing === 0) {
3412
+ results.push({ check: "embeddings", status: "ok", message: `All ${stats.total} memories have compatible embeddings` });
3376
3413
  } else {
3377
- const pct = (stats.withEmbedding / stats.total * 100).toFixed(1);
3414
+ const compatible = stats.total - incompatibleOrMissing;
3415
+ const pct = (compatible / stats.total * 100).toFixed(1);
3378
3416
  results.push({
3379
3417
  check: "embeddings",
3380
3418
  status: "warn",
3381
- message: `${stats.withoutEmbedding}/${stats.total} memories lack embeddings (${pct}% coverage). Run 'unforgit embeddings backfill'`,
3419
+ message: `${incompatibleOrMissing}/${stats.total} memories lack compatible embeddings for ${provider.provider}/${provider.model} (${pct}% coverage). Run 'unforgit embeddings backfill'`,
3382
3420
  fix: "Run 'unforgit embeddings backfill'."
3383
3421
  });
3384
3422
  }
@@ -3464,11 +3502,12 @@ var doctorCommand = new Command27("doctor").description("Check system health and
3464
3502
  });
3465
3503
  }
3466
3504
  } catch (err) {
3505
+ const localhostRemote = isLocalhostUrl(config.remote.url);
3467
3506
  results.push({
3468
3507
  check: "remote",
3469
3508
  status: "error",
3470
- message: `Cannot connect to ${config.remote.url}: ${err instanceof Error ? err.message : err}`,
3471
- fix: "Start the Unforgit API server or update remote.url in unforgit.yaml."
3509
+ message: localhostRemote ? `Cannot connect to local remote API at ${config.remote.url}: ${err instanceof Error ? err.message : err}. Local memory and local embeddings still work; only remote sync is unavailable.` : `Cannot connect to ${config.remote.url}: ${err instanceof Error ? err.message : err}`,
3510
+ fix: localhostRemote ? "remote.url points to localhost. Start the Unforgit API server on this machine/port, update remote.url, or disable sync if this repository is intentionally local-only." : "Start the Unforgit API server or update remote.url in unforgit.yaml."
3472
3511
  });
3473
3512
  }
3474
3513
  } else {
@@ -3479,15 +3518,19 @@ var doctorCommand = new Command27("doctor").description("Check system health and
3479
3518
  fix: "Run 'unforgit remote add origin <url>' if this repo should sync remotely."
3480
3519
  });
3481
3520
  }
3482
- const openaiKey = process.env.OPENAI_API_KEY;
3483
- if (openaiKey) {
3484
- results.push({ check: "openai", status: "ok", message: "OpenAI API key configured ([REDACTED])" });
3521
+ if (isOpenAIConfigured()) {
3522
+ results.push({ check: "openai", status: "ok", message: "OpenAI API key configured ([REDACTED]) for optional cloud AI features" });
3485
3523
  } else {
3524
+ const embeddingProvider = resolveEmbeddingProvider({
3525
+ provider: config.embeddings?.provider ?? "auto",
3526
+ model: config.embeddings?.model
3527
+ });
3528
+ const message = embeddingProvider.provider === "local" ? "No OpenAI API key configured; local embeddings are active. OpenAI is only needed for OpenAI-backed embeddings or AI consolidation." : "No OpenAI API key. OpenAI-backed embeddings and AI consolidation require it.";
3486
3529
  results.push({
3487
3530
  check: "openai",
3488
- status: "warn",
3489
- message: "No OpenAI API key. Auto-consolidation and embeddings backfill require it.",
3490
- fix: "Set OPENAI_API_KEY before running embedding backfill or AI consolidation."
3531
+ status: embeddingProvider.provider === "local" ? "ok" : "warn",
3532
+ message,
3533
+ fix: embeddingProvider.provider === "local" ? void 0 : "Set OPENAI_API_KEY or configure embeddings.provider=local."
3491
3534
  });
3492
3535
  }
3493
3536
  } catch (err) {
@@ -3516,6 +3559,14 @@ function summarize(results) {
3516
3559
  function buildPayload(results) {
3517
3560
  return { summary: summarize(results), results };
3518
3561
  }
3562
+ function isLocalhostUrl(value) {
3563
+ try {
3564
+ const url = new URL(value);
3565
+ return ["localhost", "127.0.0.1", "::1", "[::1]"].includes(url.hostname);
3566
+ } catch {
3567
+ return false;
3568
+ }
3569
+ }
3519
3570
  function exitForResults(results) {
3520
3571
  if (results.some((r) => r.status === "error")) {
3521
3572
  process.exit(EXIT_ERROR);