brain-cache 0.1.0 → 0.3.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/mcp.js CHANGED
@@ -43,6 +43,9 @@ var logger = pino(
43
43
  function childLogger(component) {
44
44
  return logger.child({ component });
45
45
  }
46
+ function setLogLevel(level) {
47
+ logger.level = level;
48
+ }
46
49
 
47
50
  // src/lib/format.ts
48
51
  function formatTokenSavings(input) {
@@ -640,7 +643,8 @@ function isContextLengthError(err) {
640
643
  }
641
644
  async function embedBatchWithRetry(model, texts, dimension = DEFAULT_EMBEDDING_DIMENSION, attempt = 0) {
642
645
  try {
643
- return await embedBatch(model, texts);
646
+ const embeddings = await embedBatch(model, texts);
647
+ return { embeddings, skipped: 0 };
644
648
  } catch (err) {
645
649
  if (attempt === 0 && isConnectionError(err)) {
646
650
  log6.warn({ model }, "Ollama cold-start suspected, retrying in 5s");
@@ -650,24 +654,21 @@ async function embedBatchWithRetry(model, texts, dimension = DEFAULT_EMBEDDING_D
650
654
  if (isContextLengthError(err)) {
651
655
  log6.warn({ model, batchSize: texts.length }, "Batch exceeded context length, falling back to individual embedding");
652
656
  const results = [];
657
+ let skipped = 0;
653
658
  for (const text of texts) {
654
659
  try {
655
660
  const [vec] = await embedBatch(model, [text]);
656
661
  results.push(vec);
657
662
  } catch (innerErr) {
658
663
  if (isContextLengthError(innerErr)) {
659
- process.stderr.write(
660
- `
661
- brain-cache: chunk too large for embedding model, skipping (${text.length} chars)
662
- `
663
- );
664
+ skipped++;
664
665
  results.push(new Array(dimension).fill(0));
665
666
  } else {
666
667
  throw innerErr;
667
668
  }
668
669
  }
669
670
  }
670
- return results;
671
+ return { embeddings: results, skipped };
671
672
  }
672
673
  throw err;
673
674
  }
@@ -710,201 +711,218 @@ function hashContent(content) {
710
711
  }
711
712
  async function runIndex(targetPath, opts) {
712
713
  const force = opts?.force ?? false;
713
- const rootDir = resolve(targetPath ?? ".");
714
- const profile = await readProfile();
715
- if (profile === null) {
716
- throw new Error("No profile found. Run 'brain-cache init' first.");
717
- }
718
- const running = await isOllamaRunning();
719
- if (!running) {
720
- throw new Error("Ollama is not running. Start it with 'ollama serve' or run 'brain-cache init'.");
721
- }
722
- const dim = EMBEDDING_DIMENSIONS[profile.embeddingModel] ?? DEFAULT_EMBEDDING_DIMENSION;
723
- if (!(profile.embeddingModel in EMBEDDING_DIMENSIONS)) {
724
- process.stderr.write(
725
- `Warning: Unknown embedding model '${profile.embeddingModel}', defaulting to ${DEFAULT_EMBEDDING_DIMENSION} dimensions.
714
+ const previousLogLevel = process.env.BRAIN_CACHE_LOG ?? "warn";
715
+ setLogLevel("silent");
716
+ const originalStderrWrite = process.stderr.write.bind(process.stderr);
717
+ process.stderr.write = ((chunk, ...args) => {
718
+ const str = typeof chunk === "string" ? chunk : chunk.toString();
719
+ if (/^\[[\d\-T:Z]+ WARN lance/.test(str) || /^\[[\d\-T:Z]+ INFO lance/.test(str)) {
720
+ return true;
721
+ }
722
+ return originalStderrWrite(chunk, ...args);
723
+ });
724
+ try {
725
+ const rootDir = resolve(targetPath ?? ".");
726
+ const profile = await readProfile();
727
+ if (profile === null) {
728
+ throw new Error("No profile found. Run 'brain-cache init' first.");
729
+ }
730
+ const running = await isOllamaRunning();
731
+ if (!running) {
732
+ throw new Error("Ollama is not running. Start it with 'ollama serve' or run 'brain-cache init'.");
733
+ }
734
+ const dim = EMBEDDING_DIMENSIONS[profile.embeddingModel] ?? DEFAULT_EMBEDDING_DIMENSION;
735
+ if (!(profile.embeddingModel in EMBEDDING_DIMENSIONS)) {
736
+ process.stderr.write(
737
+ `Warning: Unknown embedding model '${profile.embeddingModel}', defaulting to ${DEFAULT_EMBEDDING_DIMENSION} dimensions.
726
738
  `
727
- );
728
- }
729
- const db = await openDatabase(rootDir);
730
- const table = await openOrCreateChunkTable(db, rootDir, profile.embeddingModel, dim);
731
- const files = await crawlSourceFiles(rootDir);
732
- process.stderr.write(`brain-cache: found ${files.length} source files
739
+ );
740
+ }
741
+ const db = await openDatabase(rootDir);
742
+ const table = await openOrCreateChunkTable(db, rootDir, profile.embeddingModel, dim);
743
+ const files = await crawlSourceFiles(rootDir);
744
+ process.stderr.write(`brain-cache: found ${files.length} source files
733
745
  `);
734
- if (files.length === 0) {
735
- process.stderr.write(`No source files found in ${rootDir}
746
+ if (files.length === 0) {
747
+ process.stderr.write(`No source files found in ${rootDir}
736
748
  `);
737
- return;
738
- }
739
- const contentMap = /* @__PURE__ */ new Map();
740
- const currentHashes = {};
741
- for (let groupStart = 0; groupStart < files.length; groupStart += FILE_READ_CONCURRENCY) {
742
- const group = files.slice(groupStart, groupStart + FILE_READ_CONCURRENCY);
743
- const results = await Promise.all(
744
- group.map(async (filePath) => {
745
- const content = await readFile4(filePath, "utf-8");
746
- return { filePath, content, hash: hashContent(content) };
747
- })
749
+ return;
750
+ }
751
+ const contentMap = /* @__PURE__ */ new Map();
752
+ const currentHashes = {};
753
+ for (let groupStart = 0; groupStart < files.length; groupStart += FILE_READ_CONCURRENCY) {
754
+ const group = files.slice(groupStart, groupStart + FILE_READ_CONCURRENCY);
755
+ const results = await Promise.all(
756
+ group.map(async (filePath) => {
757
+ const content = await readFile4(filePath, "utf-8");
758
+ return { filePath, content, hash: hashContent(content) };
759
+ })
760
+ );
761
+ for (const { filePath, content, hash } of results) {
762
+ contentMap.set(filePath, content);
763
+ currentHashes[filePath] = hash;
764
+ }
765
+ }
766
+ const storedHashes = force ? {} : await readFileHashes(rootDir);
767
+ const crawledSet = new Set(files);
768
+ const newFiles = [];
769
+ const changedFiles = [];
770
+ const removedFiles = [];
771
+ const unchangedFiles = [];
772
+ for (const filePath of files) {
773
+ const currentHash = currentHashes[filePath];
774
+ if (!(filePath in storedHashes)) {
775
+ newFiles.push(filePath);
776
+ } else if (storedHashes[filePath] !== currentHash) {
777
+ changedFiles.push(filePath);
778
+ } else {
779
+ unchangedFiles.push(filePath);
780
+ }
781
+ }
782
+ for (const filePath of Object.keys(storedHashes)) {
783
+ if (!crawledSet.has(filePath)) {
784
+ removedFiles.push(filePath);
785
+ }
786
+ }
787
+ process.stderr.write(
788
+ `brain-cache: incremental index -- ${newFiles.length} new, ${changedFiles.length} changed, ${removedFiles.length} removed (${unchangedFiles.length} unchanged)
789
+ `
748
790
  );
749
- for (const { filePath, content, hash } of results) {
750
- contentMap.set(filePath, content);
751
- currentHashes[filePath] = hash;
791
+ for (const filePath of [...removedFiles, ...changedFiles]) {
792
+ await deleteChunksByFilePath(table, filePath);
752
793
  }
753
- }
754
- const storedHashes = force ? {} : await readFileHashes(rootDir);
755
- const crawledSet = new Set(files);
756
- const newFiles = [];
757
- const changedFiles = [];
758
- const removedFiles = [];
759
- const unchangedFiles = [];
760
- for (const filePath of files) {
761
- const currentHash = currentHashes[filePath];
762
- if (!(filePath in storedHashes)) {
763
- newFiles.push(filePath);
764
- } else if (storedHashes[filePath] !== currentHash) {
765
- changedFiles.push(filePath);
766
- } else {
767
- unchangedFiles.push(filePath);
794
+ const updatedHashes = { ...storedHashes };
795
+ for (const filePath of removedFiles) {
796
+ delete updatedHashes[filePath];
768
797
  }
769
- }
770
- for (const filePath of Object.keys(storedHashes)) {
771
- if (!crawledSet.has(filePath)) {
772
- removedFiles.push(filePath);
798
+ const filesToProcess = [...newFiles, ...changedFiles];
799
+ if (filesToProcess.length === 0) {
800
+ process.stderr.write(`brain-cache: nothing to re-index
801
+ `);
802
+ for (const filePath of files) {
803
+ updatedHashes[filePath] = currentHashes[filePath];
804
+ }
805
+ await writeFileHashes(rootDir, updatedHashes);
806
+ const totalFiles2 = unchangedFiles.length;
807
+ const chunkCount2 = await table.countRows();
808
+ await writeIndexState(rootDir, {
809
+ version: 1,
810
+ embeddingModel: profile.embeddingModel,
811
+ dimension: dim,
812
+ indexedAt: (/* @__PURE__ */ new Date()).toISOString(),
813
+ fileCount: totalFiles2,
814
+ chunkCount: chunkCount2
815
+ });
816
+ process.stderr.write(
817
+ `brain-cache: indexing complete
818
+ Files: ${totalFiles2}
819
+ Chunks: ${chunkCount2}
820
+ Model: ${profile.embeddingModel}
821
+ Stored in: ${rootDir}/.brain-cache/
822
+ `
823
+ );
824
+ return;
773
825
  }
774
- }
775
- process.stderr.write(
776
- `brain-cache: incremental index -- ${newFiles.length} new, ${changedFiles.length} changed, ${removedFiles.length} removed (${unchangedFiles.length} unchanged)
826
+ let totalRawTokens = 0;
827
+ let totalChunkTokens = 0;
828
+ let totalChunks = 0;
829
+ let processedFiles = 0;
830
+ let processedChunks = 0;
831
+ let skippedChunks = 0;
832
+ for (let groupStart = 0; groupStart < filesToProcess.length; groupStart += FILE_READ_CONCURRENCY) {
833
+ const group = filesToProcess.slice(groupStart, groupStart + FILE_READ_CONCURRENCY);
834
+ const groupChunks = [];
835
+ for (const filePath of group) {
836
+ const content = contentMap.get(filePath);
837
+ totalRawTokens += countChunkTokens(content);
838
+ const chunks = chunkFile(filePath, content);
839
+ groupChunks.push(...chunks);
840
+ }
841
+ processedFiles += group.length;
842
+ totalChunks += groupChunks.length;
843
+ if (processedFiles % 10 === 0 || groupStart + FILE_READ_CONCURRENCY >= filesToProcess.length) {
844
+ process.stderr.write(`brain-cache: chunked ${processedFiles}/${filesToProcess.length} files
845
+ `);
846
+ }
847
+ for (let offset = 0; offset < groupChunks.length; offset += DEFAULT_BATCH_SIZE) {
848
+ const batch = groupChunks.slice(offset, offset + DEFAULT_BATCH_SIZE);
849
+ const embeddableBatch = batch.filter((chunk) => {
850
+ const tokens = countChunkTokens(chunk.content);
851
+ if (tokens > EMBED_MAX_TOKENS) {
852
+ skippedChunks++;
853
+ return false;
854
+ }
855
+ return true;
856
+ });
857
+ if (embeddableBatch.length === 0) continue;
858
+ const texts = embeddableBatch.map((chunk) => chunk.content);
859
+ totalChunkTokens += texts.reduce((sum, t) => sum + countChunkTokens(t), 0);
860
+ const { embeddings: vectors, skipped } = await embedBatchWithRetry(profile.embeddingModel, texts, dim);
861
+ skippedChunks += skipped;
862
+ const rows = embeddableBatch.map((chunk, i) => ({
863
+ id: chunk.id,
864
+ file_path: chunk.filePath,
865
+ chunk_type: chunk.chunkType,
866
+ scope: chunk.scope,
867
+ name: chunk.name,
868
+ content: chunk.content,
869
+ start_line: chunk.startLine,
870
+ end_line: chunk.endLine,
871
+ vector: vectors[i]
872
+ }));
873
+ await insertChunks(table, rows);
874
+ processedChunks += batch.length;
875
+ process.stderr.write(
876
+ `brain-cache: embedding ${processedChunks}/${totalChunks} chunks (${Math.round(processedChunks / totalChunks * 100)}%)
777
877
  `
778
- );
779
- for (const filePath of [...removedFiles, ...changedFiles]) {
780
- await deleteChunksByFilePath(table, filePath);
781
- }
782
- const updatedHashes = { ...storedHashes };
783
- for (const filePath of removedFiles) {
784
- delete updatedHashes[filePath];
785
- }
786
- const filesToProcess = [...newFiles, ...changedFiles];
787
- if (filesToProcess.length === 0) {
788
- process.stderr.write(`brain-cache: nothing to re-index
878
+ );
879
+ }
880
+ }
881
+ if (skippedChunks > 0) {
882
+ process.stderr.write(`brain-cache: ${skippedChunks} chunks skipped (too large for model context)
789
883
  `);
790
- for (const filePath of files) {
884
+ }
885
+ process.stderr.write(
886
+ `brain-cache: ${totalChunks} chunks from ${filesToProcess.length} files
887
+ `
888
+ );
889
+ await createVectorIndexIfNeeded(table, profile.embeddingModel);
890
+ for (const filePath of filesToProcess) {
891
+ updatedHashes[filePath] = currentHashes[filePath];
892
+ }
893
+ for (const filePath of unchangedFiles) {
791
894
  updatedHashes[filePath] = currentHashes[filePath];
792
895
  }
793
896
  await writeFileHashes(rootDir, updatedHashes);
794
- const totalFiles2 = unchangedFiles.length;
795
- const chunkCount2 = await table.countRows();
897
+ const totalFiles = files.length;
898
+ const chunkCount = await table.countRows();
796
899
  await writeIndexState(rootDir, {
797
900
  version: 1,
798
901
  embeddingModel: profile.embeddingModel,
799
902
  dimension: dim,
800
903
  indexedAt: (/* @__PURE__ */ new Date()).toISOString(),
801
- fileCount: totalFiles2,
802
- chunkCount: chunkCount2
904
+ fileCount: totalFiles,
905
+ chunkCount
803
906
  });
907
+ const reductionPct = totalRawTokens > 0 ? Math.round((1 - totalChunkTokens / totalRawTokens) * 100) : 0;
908
+ const savingsBlock = formatTokenSavings({
909
+ tokensSent: totalChunkTokens,
910
+ estimatedWithout: totalRawTokens,
911
+ reductionPct
912
+ }).split("\n").map((line) => ` ${line}`).join("\n");
804
913
  process.stderr.write(
805
914
  `brain-cache: indexing complete
806
- Files: ${totalFiles2}
807
- Chunks: ${chunkCount2}
808
- Model: ${profile.embeddingModel}
809
- Stored in: ${rootDir}/.brain-cache/
810
- `
811
- );
812
- return;
813
- }
814
- let totalRawTokens = 0;
815
- let totalChunkTokens = 0;
816
- let totalChunks = 0;
817
- let processedFiles = 0;
818
- let processedChunks = 0;
819
- for (let groupStart = 0; groupStart < filesToProcess.length; groupStart += FILE_READ_CONCURRENCY) {
820
- const group = filesToProcess.slice(groupStart, groupStart + FILE_READ_CONCURRENCY);
821
- const groupChunks = [];
822
- for (const filePath of group) {
823
- const content = contentMap.get(filePath);
824
- totalRawTokens += countChunkTokens(content);
825
- const chunks = chunkFile(filePath, content);
826
- groupChunks.push(...chunks);
827
- }
828
- processedFiles += group.length;
829
- totalChunks += groupChunks.length;
830
- if (processedFiles % 10 === 0 || groupStart + FILE_READ_CONCURRENCY >= filesToProcess.length) {
831
- process.stderr.write(`brain-cache: chunked ${processedFiles}/${filesToProcess.length} files
832
- `);
833
- }
834
- for (let offset = 0; offset < groupChunks.length; offset += DEFAULT_BATCH_SIZE) {
835
- const batch = groupChunks.slice(offset, offset + DEFAULT_BATCH_SIZE);
836
- const embeddableBatch = batch.filter((chunk) => {
837
- const tokens = countChunkTokens(chunk.content);
838
- if (tokens > EMBED_MAX_TOKENS) {
839
- process.stderr.write(
840
- `
841
- brain-cache: skipping oversized chunk (${tokens} tokens > ${EMBED_MAX_TOKENS} limit): ${chunk.filePath} lines ${chunk.startLine}-${chunk.endLine}
842
- `
843
- );
844
- return false;
845
- }
846
- return true;
847
- });
848
- if (embeddableBatch.length === 0) continue;
849
- const texts = embeddableBatch.map((chunk) => chunk.content);
850
- totalChunkTokens += texts.reduce((sum, t) => sum + countChunkTokens(t), 0);
851
- const vectors = await embedBatchWithRetry(profile.embeddingModel, texts, dim);
852
- const rows = embeddableBatch.map((chunk, i) => ({
853
- id: chunk.id,
854
- file_path: chunk.filePath,
855
- chunk_type: chunk.chunkType,
856
- scope: chunk.scope,
857
- name: chunk.name,
858
- content: chunk.content,
859
- start_line: chunk.startLine,
860
- end_line: chunk.endLine,
861
- vector: vectors[i]
862
- }));
863
- await insertChunks(table, rows);
864
- processedChunks += batch.length;
865
- process.stderr.write(
866
- `\rbrain-cache: embedding ${processedChunks}/${totalChunks} chunks (${Math.round(processedChunks / totalChunks * 100)}%)`
867
- );
868
- }
869
- }
870
- process.stderr.write("\n");
871
- process.stderr.write(
872
- `brain-cache: ${totalChunks} chunks from ${filesToProcess.length} files
873
- `
874
- );
875
- await createVectorIndexIfNeeded(table, profile.embeddingModel);
876
- for (const filePath of filesToProcess) {
877
- updatedHashes[filePath] = currentHashes[filePath];
878
- }
879
- for (const filePath of unchangedFiles) {
880
- updatedHashes[filePath] = currentHashes[filePath];
881
- }
882
- await writeFileHashes(rootDir, updatedHashes);
883
- const totalFiles = files.length;
884
- const chunkCount = await table.countRows();
885
- await writeIndexState(rootDir, {
886
- version: 1,
887
- embeddingModel: profile.embeddingModel,
888
- dimension: dim,
889
- indexedAt: (/* @__PURE__ */ new Date()).toISOString(),
890
- fileCount: totalFiles,
891
- chunkCount
892
- });
893
- const reductionPct = totalRawTokens > 0 ? Math.round((1 - totalChunkTokens / totalRawTokens) * 100) : 0;
894
- const savingsBlock = formatTokenSavings({
895
- tokensSent: totalChunkTokens,
896
- estimatedWithout: totalRawTokens,
897
- reductionPct
898
- }).split("\n").map((line) => ` ${line}`).join("\n");
899
- process.stderr.write(
900
- `brain-cache: indexing complete
901
915
  Files: ${totalFiles}
902
916
  Chunks: ${totalChunks}
903
917
  Model: ${profile.embeddingModel}
904
918
  ${savingsBlock}
905
919
  Stored in: ${rootDir}/.brain-cache/
906
920
  `
907
- );
921
+ );
922
+ } finally {
923
+ setLogLevel(previousLogLevel);
924
+ process.stderr.write = originalStderrWrite;
925
+ }
908
926
  }
909
927
 
910
928
  // src/workflows/search.ts
@@ -1128,7 +1146,7 @@ async function runBuildContext(query, opts) {
1128
1146
  }
1129
1147
 
1130
1148
  // src/mcp/index.ts
1131
- var version = "0.1.0";
1149
+ var version = "0.3.0";
1132
1150
  var log9 = childLogger("mcp");
1133
1151
  var server = new McpServer({ name: "brain-cache", version });
1134
1152
  server.registerTool(
@@ -4,21 +4,21 @@ import {
4
4
  classifyQueryIntent,
5
5
  deduplicateChunks,
6
6
  searchChunks
7
- } from "./chunk-ZLB4VJQK.js";
7
+ } from "./chunk-BF5UDEIF.js";
8
8
  import {
9
9
  embedBatchWithRetry
10
- } from "./chunk-WCNMLSL2.js";
10
+ } from "./chunk-GR6QXZ4J.js";
11
11
  import {
12
12
  isOllamaRunning
13
- } from "./chunk-P7WSTGLE.js";
13
+ } from "./chunk-V4ARVFRG.js";
14
14
  import {
15
15
  openDatabase,
16
16
  readIndexState
17
- } from "./chunk-XXWJ57QP.js";
17
+ } from "./chunk-6MACVOTO.js";
18
18
  import {
19
19
  readProfile
20
- } from "./chunk-PA4BZBWS.js";
21
- import "./chunk-PDQXJSH4.js";
20
+ } from "./chunk-MSI4MDIM.js";
21
+ import "./chunk-3SFDFUEX.js";
22
22
 
23
23
  // src/workflows/search.ts
24
24
  import { resolve } from "path";
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  readIndexState
4
- } from "./chunk-XXWJ57QP.js";
4
+ } from "./chunk-6MACVOTO.js";
5
5
  import {
6
6
  readProfile
7
- } from "./chunk-PA4BZBWS.js";
8
- import "./chunk-PDQXJSH4.js";
7
+ } from "./chunk-MSI4MDIM.js";
8
+ import "./chunk-3SFDFUEX.js";
9
9
 
10
10
  // src/workflows/status.ts
11
11
  import { resolve } from "path";