@melandlabs/memory-store 0.1.1 → 0.1.3

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.
@@ -4,7 +4,8 @@ import { Hono } from 'hono';
4
4
  import { buildMemoryRecordEmbeddingDocument } from '@melandlabs/ai/memory';
5
5
  import { ChromaVectorStore } from '@melandlabs/rag';
6
6
  import { mkdirSync } from 'fs';
7
- import { dirname } from 'path';
7
+ import { homedir } from 'os';
8
+ import { join, dirname } from 'path';
8
9
  import { SQLiteRawMessageManager } from '@melandlabs/sqlite';
9
10
 
10
11
  // src/contracts.ts
@@ -149,16 +150,14 @@ function hasPostgresFactory() {
149
150
  return factory !== null;
150
151
  }
151
152
  var manager = null;
152
- function isSQLiteRawMessageStorageAvailable(env) {
153
- return resolveEnv(env).isTauriMode();
153
+ var DEFAULT_SQLITE_PATH = join(homedir(), ".opencontext", "memory", "store.db");
154
+ function resolveSQLiteRawMessageDbPath() {
155
+ const fromEnv = process.env.MEMORY_STORE_DB_PATH?.trim();
156
+ return fromEnv && fromEnv.length > 0 ? fromEnv : DEFAULT_SQLITE_PATH;
154
157
  }
155
- async function getSQLiteRawMessageManager(env) {
156
- const e = resolveEnv(env);
157
- if (!e.isTauriMode()) {
158
- throw new Error("SQLite raw message storage is only available in Tauri mode.");
159
- }
158
+ async function getSQLiteRawMessageManager(_env) {
160
159
  if (!manager) {
161
- const dbPath = e.getTauriDbPath?.() ?? process.env.TAURI_DB_PATH ?? "";
160
+ const dbPath = resolveSQLiteRawMessageDbPath();
162
161
  mkdirSync(dirname(dbPath), { recursive: true });
163
162
  manager = new SQLiteRawMessageManager({
164
163
  dbPath,
@@ -175,49 +174,39 @@ async function closeSQLiteRawMessageManager() {
175
174
  await manager.close();
176
175
  manager = null;
177
176
  }
178
- function resolveEnv(env) {
179
- if (env) return env;
180
- return {
181
- isTauriMode: () => process.env.IS_TAURI === "true" || typeof process.env.TAURI_MODE === "string",
182
- getTauriDbPath: () => process.env.TAURI_DB_PATH ?? "",
183
- getTauriDataDir: () => process.env.TAURI_DATA_DIR ?? ""
184
- };
185
- }
186
177
 
187
178
  // src/storage/raw-message-store.ts
188
- function resolveEnv2(env) {
189
- if (env) return env;
190
- return {
191
- isTauriMode: () => process.env.IS_TAURI === "true" || typeof process.env.TAURI_MODE === "string",
192
- getTauriDbPath: () => process.env.TAURI_DB_PATH ?? "",
193
- getTauriDataDir: () => process.env.TAURI_DATA_DIR ?? ""
194
- };
179
+ function resolveBackend(env) {
180
+ const explicit = process.env.OPENCONTEXT_MEMORY_STORE_BACKEND?.trim().toLowerCase();
181
+ if (explicit === "postgres") return "postgres";
182
+ return "sqlite";
183
+ }
184
+ function resolveEnv(_env) {
185
+ return {};
195
186
  }
196
187
  function createRawMessageStore(options = {}) {
197
- const env = resolveEnv2(options.env);
188
+ resolveEnv(options.env);
198
189
  return {
199
190
  getBackend() {
200
- return isSQLiteRawMessageStorageAvailable(env) ? "sqlite" : "postgres";
191
+ return resolveBackend();
201
192
  },
202
193
  isAvailable() {
203
- return isSQLiteRawMessageStorageAvailable(env) || hasPostgresFactory();
194
+ return resolveBackend() === "sqlite" || hasPostgresFactory();
204
195
  },
205
196
  async getManager() {
206
- if (isSQLiteRawMessageStorageAvailable(env)) {
207
- return await getSQLiteRawMessageManager(env);
208
- }
209
- const pg = await resolvePostgresFactory();
210
- if (!pg) {
211
- throw new Error(
212
- "No raw-message backend available. SQLite is disabled (not Tauri) and no Postgres factory is registered."
213
- );
197
+ if (resolveBackend() === "postgres") {
198
+ const pg = await resolvePostgresFactory();
199
+ if (!pg) {
200
+ throw new Error(
201
+ "OPENCONTEXT_MEMORY_STORE_BACKEND=postgres but no Postgres factory is registered. Call registerPostgresFactory() at startup, or unset the env var to use the default SQLite backend."
202
+ );
203
+ }
204
+ return pg;
214
205
  }
215
- return pg;
206
+ return await getSQLiteRawMessageManager();
216
207
  },
217
208
  async close() {
218
- if (isSQLiteRawMessageStorageAvailable(env)) {
219
- await closeSQLiteRawMessageManager();
220
- }
209
+ await closeSQLiteRawMessageManager();
221
210
  }
222
211
  };
223
212
  }
@@ -473,6 +462,34 @@ function createUnifiedSearch(deps = {}) {
473
462
  }
474
463
 
475
464
  // src/http.ts
465
+ async function embedMissingMessages(messages, deps) {
466
+ if (typeof deps.embedQuery !== "function") {
467
+ return messages;
468
+ }
469
+ const out = [];
470
+ for (const message of messages) {
471
+ if (Array.isArray(message.embedding) && message.embedding.length > 0) {
472
+ out.push(message);
473
+ continue;
474
+ }
475
+ if (typeof message.content !== "string" || message.content.length === 0) {
476
+ out.push(message);
477
+ continue;
478
+ }
479
+ const vector = await deps.embedQuery({
480
+ userId: message.userId,
481
+ query: message.content
482
+ });
483
+ out.push({
484
+ ...message,
485
+ embedding: vector,
486
+ embeddingModel: message.embeddingModel ?? "server",
487
+ embeddingDimensions: vector.length,
488
+ embeddingUpdatedAt: Date.now()
489
+ });
490
+ }
491
+ return out;
492
+ }
476
493
  async function startHttpServer(options = {}) {
477
494
  const port = options.port ?? Number.parseInt(process.env.MEMORY_HTTP_PORT ?? "7421", 10);
478
495
  const host = options.host ?? process.env.MEMORY_HTTP_HOST ?? "127.0.0.1";
@@ -508,16 +525,34 @@ async function startHttpServer(options = {}) {
508
525
  return c.json({ error: "userId and messages[] required" }, 400);
509
526
  }
510
527
  const manager2 = await rawStore.getManager();
511
- const result = await manager2.upsertRawMessages?.({
512
- userId: body.userId,
513
- messages: body.messages
514
- });
528
+ const incoming = body.messages;
529
+ const messages = body.embedOnInsert === true ? await embedMissingMessages(
530
+ incoming.map((m) => ({ ...m, userId: m.userId ?? body.userId })),
531
+ options.unified ?? {}
532
+ ) : incoming.map((m) => ({ ...m, userId: m.userId ?? body.userId }));
533
+ let result;
534
+ if (typeof manager2.upsertRawMessages === "function") {
535
+ result = await manager2.upsertRawMessages({
536
+ userId: body.userId,
537
+ messages
538
+ });
539
+ } else if (typeof manager2.storeMessages === "function") {
540
+ const ids = await manager2.storeMessages(messages);
541
+ result = { inserted: ids.length, ids };
542
+ } else {
543
+ return c.json(
544
+ {
545
+ error: "active raw-message manager exposes neither upsertRawMessages nor storeMessages"
546
+ },
547
+ 500
548
+ );
549
+ }
515
550
  try {
516
- await upsertRawMessagesToChroma(body.messages);
551
+ await upsertRawMessagesToChroma(messages);
517
552
  } catch (error) {
518
553
  console.warn("[memory-store/http] chroma upsert failed:", error);
519
554
  }
520
- return c.json({ ok: true, result });
555
+ return c.json({ ok: true, count: messages.length, result });
521
556
  });
522
557
  app.get("/v1/raw-messages/:id", async (c) => {
523
558
  const id = c.req.param("id");
@@ -539,33 +574,352 @@ async function startHttpServer(options = {}) {
539
574
  };
540
575
  }
541
576
 
577
+ // src/storage/sqlite-vector-index.ts
578
+ function getMemoryVectorStoreBackend() {
579
+ const backend = (process.env.INSIGHT_VECTOR_STORE_BACKEND || process.env.MEMORY_VECTOR_STORE_BACKEND || process.env.VECTOR_STORE_BACKEND || "sqlite-vec").toLowerCase();
580
+ return backend === "chroma" ? "chroma" : "sqlite-vec";
581
+ }
582
+ function isInsightSQLiteVecEnabled(_env) {
583
+ return getMemoryVectorStoreBackend() === "sqlite-vec";
584
+ }
585
+ async function getInsightSQLiteVecStore(_env) {
586
+ const dbPath = resolveSQLiteRawMessageDbPath();
587
+ const { getSQLiteVecStore } = await import('@melandlabs/rag/sqlite-vec-store');
588
+ return await getSQLiteVecStore(dbPath, void 0, {
589
+ collectionName: process.env.SQLITE_VEC_INSIGHTS_COLLECTION || "opencontext_insights"
590
+ });
591
+ }
592
+ async function searchInsightsWithSQLiteVec(input, env) {
593
+ const store = await getInsightSQLiteVecStore();
594
+ const overfetchLimit = Math.max(input.limit * 8, input.limit);
595
+ const results = await store.similaritySearchWithOptions(input.queryEmbedding, {
596
+ limit: overfetchLimit,
597
+ filter: { userId: input.userId }
598
+ });
599
+ return results.filter((result) => result.score >= input.threshold).filter((result) => {
600
+ const metadata = result.metadata ?? {};
601
+ if (input.botIds?.length && !input.botIds.includes(String(metadata.botId ?? ""))) {
602
+ return false;
603
+ }
604
+ return input.includeArchived || metadata.archived !== true;
605
+ }).slice(0, input.limit);
606
+ }
607
+
608
+ // src/server/cli-shared.ts
609
+ async function loadAiRag() {
610
+ try {
611
+ const [local, chroma] = await Promise.all([
612
+ import('@melandlabs/ai-rag/local-transformers-embedding-provider'),
613
+ import('@melandlabs/ai-rag/chroma-store')
614
+ ]);
615
+ return {
616
+ LocalTransformersEmbeddingProvider: local.LocalTransformersEmbeddingProvider,
617
+ ChromaVectorStore: chroma.ChromaVectorStore
618
+ };
619
+ } catch (error) {
620
+ throw new Error(
621
+ `--embedding-provider local / --*-backend=chroma require @melandlabs/ai-rag to be installed. Run \`pnpm add @melandlabs/ai-rag\` and try again. (${error.message})`
622
+ );
623
+ }
624
+ }
625
+ function parseUnifiedArgs(argv) {
626
+ const env = process.env;
627
+ const args = {
628
+ embeddingProvider: env.EMBEDDING_PROVIDER ?? "none",
629
+ embeddingModel: env.EMBEDDING_MODEL,
630
+ chromaUrl: env.CHROMA_URL,
631
+ memoryBackend: env.MEMORY_BACKEND ?? "none",
632
+ insightsBackend: env.INSIGHTS_BACKEND ?? "none",
633
+ insightsCollection: env.INSIGHTS_COLLECTION ?? "opencontext_insights",
634
+ knowledgeBackend: env.KNOWLEDGE_BACKEND ?? "none",
635
+ knowledgeCollection: env.KNOWLEDGE_COLLECTION ?? "opencontext_knowledge"
636
+ };
637
+ for (let i = 0; i < argv.length; i += 1) {
638
+ const arg = argv[i];
639
+ const next = argv[i + 1];
640
+ const takeValue = () => {
641
+ if (next === void 0) throw new Error(`[memory-store/http] ${arg} requires a value`);
642
+ i += 1;
643
+ return next;
644
+ };
645
+ switch (arg) {
646
+ case "--embedding-provider":
647
+ args.embeddingProvider = takeValue();
648
+ break;
649
+ case "--embedding-model":
650
+ args.embeddingModel = takeValue();
651
+ break;
652
+ case "--chroma-url":
653
+ args.chromaUrl = takeValue();
654
+ break;
655
+ case "--memory-backend":
656
+ args.memoryBackend = takeValue();
657
+ break;
658
+ case "--insights-backend":
659
+ args.insightsBackend = takeValue();
660
+ break;
661
+ case "--insights-collection":
662
+ args.insightsCollection = takeValue();
663
+ break;
664
+ case "--knowledge-backend":
665
+ args.knowledgeBackend = takeValue();
666
+ break;
667
+ case "--knowledge-collection":
668
+ args.knowledgeCollection = takeValue();
669
+ break;
670
+ }
671
+ }
672
+ for (const [name, value] of [
673
+ ["--embedding-provider", args.embeddingProvider],
674
+ ["--memory-backend", args.memoryBackend],
675
+ ["--insights-backend", args.insightsBackend],
676
+ ["--knowledge-backend", args.knowledgeBackend]
677
+ ]) {
678
+ const allowed = ["local", "openrouter", "none", "chroma", "sqlite-vec"];
679
+ if (!allowed.includes(value)) {
680
+ throw new Error(`[memory-store/cli] ${name} must be one of: ${allowed.join(", ")} (got "${value}")`);
681
+ }
682
+ }
683
+ return args;
684
+ }
685
+ async function buildUnified(args) {
686
+ const unified = {};
687
+ const log = (msg) => console.warn(`[memory-store/cli] ${msg}`);
688
+ const wantsAiRag = args.embeddingProvider === "local" || args.memoryBackend === "chroma" || args.insightsBackend === "chroma" || args.knowledgeBackend === "chroma";
689
+ const aiRag = wantsAiRag ? await loadAiRag() : null;
690
+ if (args.embeddingProvider === "local") {
691
+ const provider = new aiRag.LocalTransformersEmbeddingProvider({ modelName: args.embeddingModel });
692
+ unified.embedQuery = async ({ query }) => provider.embedQuery(query);
693
+ log(
694
+ `embedQuery wired via LocalTransformersEmbeddingProvider (model=${args.embeddingModel ?? "Xenova/all-MiniLM-L6-v2"})`
695
+ );
696
+ } else if (args.embeddingProvider === "openrouter") {
697
+ const apiKey = process.env.OPENROUTER_API_KEY;
698
+ if (!apiKey) {
699
+ throw new Error("--embedding-provider openrouter requires OPENROUTER_API_KEY in the environment");
700
+ }
701
+ const model = args.embeddingModel ?? "text-embedding-3-small";
702
+ const baseURL = "https://openrouter.ai/api/v1";
703
+ unified.embedQuery = async ({ query }) => {
704
+ const res = await fetch(`${baseURL}/embeddings`, {
705
+ method: "POST",
706
+ headers: {
707
+ "content-type": "application/json",
708
+ authorization: `Bearer ${apiKey}`,
709
+ "HTTP-Referer": process.env.NEXT_PUBLIC_APP_URL ?? "https://opencontext.ai",
710
+ "X-Title": "opencontext AI"
711
+ },
712
+ body: JSON.stringify({ model, input: query })
713
+ });
714
+ if (!res.ok) {
715
+ throw new Error(`openrouter embeddings ${res.status}: ${await res.text()}`);
716
+ }
717
+ const body = await res.json();
718
+ const embedding = body.data?.[0]?.embedding;
719
+ if (!embedding) throw new Error("openrouter embeddings response missing data[0].embedding");
720
+ return embedding;
721
+ };
722
+ log(`embedQuery wired via openrouter (model=${model})`);
723
+ }
724
+ if (args.memoryBackend === "sqlite-vec") {
725
+ const store = createRawMessageStore({ env: void 0 });
726
+ const manager2 = await store.getManager();
727
+ if (typeof manager2.searchMessagesSemantically !== "function") {
728
+ throw new Error(
729
+ "the active raw-message manager does not implement searchMessagesSemantically; sqlite-vec ANN for the memory source is unavailable in this build"
730
+ );
731
+ }
732
+ unified.searchRawMessagesAnn = async ({ userId, queryEmbedding, limit, threshold, botId }) => {
733
+ const rows = await manager2.searchMessagesSemantically({
734
+ userId,
735
+ queryEmbedding,
736
+ limit,
737
+ threshold,
738
+ botId
739
+ });
740
+ return rows.map((r) => ({
741
+ id: r.id,
742
+ content: r.content,
743
+ similarity: r.similarity,
744
+ metadata: r.metadata ?? {}
745
+ }));
746
+ };
747
+ log("memory backend wired via sqlite-vec (uses the manager's searchMessagesSemantically)");
748
+ } else if (args.memoryBackend === "chroma") {
749
+ if (!args.chromaUrl)
750
+ throw new Error("--memory-backend=chroma requires --chroma-url <url> (or CHROMA_URL env)");
751
+ const store = new aiRag.ChromaVectorStore({
752
+ url: args.chromaUrl,
753
+ collectionName: "opencontext_raw_messages"
754
+ });
755
+ unified.searchRawMessagesAnn = async ({ queryEmbedding, limit, threshold }) => {
756
+ const results = await store.similaritySearchWithOptions(queryEmbedding, { limit: limit + 1 });
757
+ return results.filter((r) => r.score >= threshold).map((r) => ({ id: r.id, content: r.content, similarity: r.score, metadata: r.metadata ?? {} }));
758
+ };
759
+ log("memory backend wired via chroma (collection=opencontext_raw_messages)");
760
+ }
761
+ if (args.insightsBackend === "sqlite-vec" && isInsightSQLiteVecEnabled()) {
762
+ if (!unified.embedQuery) {
763
+ throw new Error("--insights-backend=sqlite-vec requires --embedding-provider local|openrouter");
764
+ }
765
+ const embedQuery = unified.embedQuery;
766
+ unified.searchInsights = async ({ userId, query, limit, threshold, botIds, includeArchived }) => {
767
+ const emb = await embedQuery({ userId, query });
768
+ const rows = await searchInsightsWithSQLiteVec({
769
+ userId,
770
+ queryEmbedding: emb,
771
+ limit,
772
+ threshold,
773
+ botIds,
774
+ includeArchived
775
+ });
776
+ return rows.map((r) => ({
777
+ id: r.id,
778
+ content: r.content,
779
+ similarity: r.score,
780
+ metadata: r.metadata ?? {}
781
+ }));
782
+ };
783
+ log("insights backend wired via sqlite-vec");
784
+ } else if (args.insightsBackend === "chroma") {
785
+ if (!args.chromaUrl)
786
+ throw new Error("--insights-backend=chroma requires --chroma-url <url> (or CHROMA_URL env)");
787
+ if (!unified.embedQuery)
788
+ throw new Error("--insights-backend=chroma requires --embedding-provider local|openrouter");
789
+ const store = new aiRag.ChromaVectorStore({
790
+ url: args.chromaUrl,
791
+ collectionName: args.insightsCollection
792
+ });
793
+ const embedQuery = unified.embedQuery;
794
+ unified.searchInsights = async ({ query, limit, threshold }) => {
795
+ const emb = await embedQuery({ userId: "", query });
796
+ const results = await store.similaritySearchWithOptions(emb, { limit: limit + 1 });
797
+ return results.filter((r) => r.score >= threshold).map((r) => ({ id: r.id, content: r.content, similarity: r.score, metadata: r.metadata ?? {} }));
798
+ };
799
+ log(`insights backend wired via chroma (collection=${args.insightsCollection})`);
800
+ }
801
+ if (args.knowledgeBackend === "chroma") {
802
+ if (!args.chromaUrl)
803
+ throw new Error("--knowledge-backend=chroma requires --chroma-url <url> (or CHROMA_URL env)");
804
+ if (!unified.embedQuery)
805
+ throw new Error("--knowledge-backend=chroma requires --embedding-provider local|openrouter");
806
+ const store = new aiRag.ChromaVectorStore({
807
+ url: args.chromaUrl,
808
+ collectionName: args.knowledgeCollection
809
+ });
810
+ const embedQuery = unified.embedQuery;
811
+ unified.searchKnowledge = async ({ query, options }) => {
812
+ const emb = await embedQuery({ userId: "", query });
813
+ const results = await store.similaritySearchWithOptions(emb, { limit: options.limit + 1 });
814
+ return results.filter((r) => r.score >= options.threshold).map((r, i) => ({
815
+ chunkId: r.id,
816
+ documentId: r.documentId || String(r.metadata?.documentId ?? r.id),
817
+ documentName: String(r.metadata?.documentName ?? ""),
818
+ content: r.content,
819
+ similarity: r.score,
820
+ chunkIndex: i
821
+ }));
822
+ };
823
+ log(`knowledge backend wired via chroma (collection=${args.knowledgeCollection})`);
824
+ }
825
+ return unified;
826
+ }
827
+ function printUnifiedHelp() {
828
+ console.log(`Embedding (wires unified.embedQuery):
829
+ --embedding-provider <name> local | openrouter | none
830
+ (env: EMBEDDING_PROVIDER, default: none)
831
+ --embedding-model <name> Model name
832
+ (env: EMBEDDING_MODEL; local \u2192 Xenova/all-MiniLM-L6-v2,
833
+ openrouter \u2192 text-embedding-3-small)
834
+ Note: "local" dynamically imports @melandlabs/ai-rag (a peer install).
835
+
836
+ Cross-source search (wires unified.searchKnowledge / searchInsights / searchRawMessagesAnn):
837
+ --chroma-url <url> Chroma server URL
838
+ (env: CHROMA_URL; required when any *-backend=chroma)
839
+ --memory-backend <name> sqlite-vec | chroma | none
840
+ (env: MEMORY_BACKEND, default: none)
841
+ --insights-backend <name> sqlite-vec | chroma | none
842
+ (env: INSIGHTS_BACKEND, default: none)
843
+ --insights-collection <name> Chroma collection (default: opencontext_insights)
844
+ --knowledge-backend <name> chroma | none
845
+ (env: KNOWLEDGE_BACKEND, default: none)
846
+ --knowledge-collection <name> Chroma collection (default: opencontext_knowledge)
847
+ Note: the chroma backends dynamically import @melandlabs/ai-rag.`);
848
+ }
849
+
542
850
  // src/server/cli-http.ts
543
851
  function parseArgs(argv) {
852
+ const env = process.env;
853
+ const unified = parseUnifiedArgs(argv);
544
854
  const args = {
545
- port: Number.parseInt(process.env.MEMORY_HTTP_PORT ?? "7421", 10),
546
- host: process.env.MEMORY_HTTP_HOST ?? "127.0.0.1"
855
+ ...unified,
856
+ port: Number.parseInt(env.MEMORY_HTTP_PORT ?? "7421", 10),
857
+ host: env.MEMORY_HTTP_HOST ?? "127.0.0.1"
547
858
  };
548
859
  for (let i = 0; i < argv.length; i += 1) {
549
860
  const arg = argv[i];
550
861
  const next = argv[i + 1];
551
- if (arg === "--port" && next) {
552
- args.port = Number.parseInt(next, 10);
553
- i += 1;
554
- } else if (arg === "--host" && next) {
555
- args.host = next;
862
+ const takeValue = () => {
863
+ if (next === void 0) throw new Error(`[memory-store/http] ${arg} requires a value`);
556
864
  i += 1;
557
- } else if (arg === "--help" || arg === "-h") {
558
- console.log("Usage: opencontext-memory-http [--port <port>] [--host <host>]");
559
- process.exit(0);
865
+ return next;
866
+ };
867
+ switch (arg) {
868
+ case "--port":
869
+ args.port = Number.parseInt(takeValue(), 10);
870
+ break;
871
+ case "--host":
872
+ args.host = takeValue();
873
+ break;
874
+ case "--help":
875
+ case "-h":
876
+ printHelp();
877
+ process.exit(0);
560
878
  }
561
879
  }
880
+ if (!Number.isFinite(args.port) || args.port <= 0) {
881
+ throw new Error(`[memory-store/http] invalid --port: ${args.port}`);
882
+ }
562
883
  return args;
563
884
  }
885
+ function printHelp() {
886
+ console.log(`opencontext-memory-http \u2014 standalone memory-store HTTP daemon.
887
+
888
+ Usage:
889
+ opencontext-memory-http [options]
890
+
891
+ Server:
892
+ --port <port> Port to listen on (default: 7421, env: MEMORY_HTTP_PORT)
893
+ --host <host> Host to bind (default: 127.0.0.1, env: MEMORY_HTTP_HOST)
894
+ `);
895
+ printUnifiedHelp();
896
+ console.log(`
897
+ Examples:
898
+ # Minimal \u2014 same as before, all three *not_configured warnings remain
899
+ opencontext-memory-http
900
+
901
+ # Local ONNX embedder + sqlite-vec ANN for the memory source (no API key,
902
+ # no extra services). Removes embedQuery + memory warnings; the
903
+ # insights/knowledge sources still need a backend of their own.
904
+ opencontext-memory-http \\
905
+ --embedding-provider local \\
906
+ --memory-backend sqlite-vec
907
+
908
+ # Wire everything via a running Chroma server
909
+ opencontext-memory-http \\
910
+ --embedding-provider openrouter \\
911
+ --chroma-url http://127.0.0.1:8000 \\
912
+ --memory-backend chroma \\
913
+ --insights-backend chroma \\
914
+ --knowledge-backend chroma`);
915
+ }
564
916
  async function main() {
565
917
  const args = parseArgs(process.argv.slice(2));
566
- const { url, port, stop } = await startHttpServer({
918
+ const unified = await buildUnified(args);
919
+ const { url, stop } = await startHttpServer({
567
920
  port: args.port,
568
- host: args.host
921
+ host: args.host,
922
+ unified
569
923
  });
570
924
  console.log(`[memory-store/http] listening at ${url}`);
571
925
  const shutdown = async (signal) => {