@locusai/sdk 0.5.0 → 0.5.1

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.
@@ -12,6 +12,6 @@ export declare class CodebaseIndexerService {
12
12
  private deps;
13
13
  private indexer;
14
14
  constructor(deps: CodebaseIndexerServiceDeps);
15
- reindex(): Promise<void>;
15
+ reindex(force?: boolean): Promise<void>;
16
16
  }
17
17
  //# sourceMappingURL=codebase-indexer-service.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"codebase-indexer-service.d.ts","sourceRoot":"","sources":["../../src/agent/codebase-indexer-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACzC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAGhD,MAAM,WAAW,0BAA0B;IACzC,QAAQ,EAAE,QAAQ,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,GAAG,EAAE,KAAK,CAAC;CACZ;AAED;;GAEG;AACH,qBAAa,sBAAsB;IAGrB,OAAO,CAAC,IAAI;IAFxB,OAAO,CAAC,OAAO,CAAkB;gBAEb,IAAI,EAAE,0BAA0B;IAI9C,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAqC/B"}
1
+ {"version":3,"file":"codebase-indexer-service.d.ts","sourceRoot":"","sources":["../../src/agent/codebase-indexer-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACzC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAGhD,MAAM,WAAW,0BAA0B;IACzC,QAAQ,EAAE,QAAQ,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,GAAG,EAAE,KAAK,CAAC;CACZ;AAED;;GAEG;AACH,qBAAa,sBAAsB;IAGrB,OAAO,CAAC,IAAI;IAFxB,OAAO,CAAC,OAAO,CAAkB;gBAEb,IAAI,EAAE,0BAA0B;IAI9C,OAAO,CAAC,KAAK,UAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;CA2C5C"}
@@ -847,6 +847,7 @@ class ArtifactSyncer {
847
847
  }
848
848
 
849
849
  // src/core/indexer.ts
850
+ var import_node_crypto2 = require("node:crypto");
850
851
  var import_node_fs3 = require("node:fs");
851
852
  var import_node_path5 = require("node:path");
852
853
  var import_globby = require("globby");
@@ -854,16 +855,64 @@ var import_globby = require("globby");
854
855
  class CodebaseIndexer {
855
856
  projectPath;
856
857
  indexPath;
858
+ fullReindexRatioThreshold = 0.2;
857
859
  constructor(projectPath) {
858
860
  this.projectPath = projectPath;
859
861
  this.indexPath = import_node_path5.join(projectPath, ".locus", "codebase-index.json");
860
862
  }
861
- async index(onProgress, treeSummarizer) {
863
+ async index(onProgress, treeSummarizer, force = false) {
862
864
  if (!treeSummarizer) {
863
865
  throw new Error("A treeSummarizer is required for this indexing method.");
864
866
  }
865
- if (onProgress)
866
- onProgress("Generating file tree...");
867
+ onProgress?.("Generating file tree...");
868
+ const currentFiles = await this.getFileTree();
869
+ const treeString = currentFiles.join(`
870
+ `);
871
+ const newTreeHash = this.hashTree(treeString);
872
+ const existingIndex = this.loadIndex();
873
+ if (!force && existingIndex?.treeHash === newTreeHash) {
874
+ onProgress?.("No file changes detected, skipping reindex");
875
+ return null;
876
+ }
877
+ const currentHashes = this.computeFileHashes(currentFiles);
878
+ const existingHashes = existingIndex?.fileHashes;
879
+ const canIncremental = !force && existingIndex && existingHashes;
880
+ if (canIncremental) {
881
+ onProgress?.("Performing incremental update");
882
+ const { added, deleted, modified } = this.diffFiles(currentHashes, existingHashes);
883
+ const changedFiles = [...added, ...modified];
884
+ const totalChanges = changedFiles.length + deleted.length;
885
+ const existingFileCount = Object.keys(existingHashes).length;
886
+ onProgress?.(`File changes detected: ${changedFiles.length} changed, ${added.length} added, ${deleted.length} deleted`);
887
+ if (existingFileCount > 0) {
888
+ const changeRatio = totalChanges / existingFileCount;
889
+ if (changeRatio <= this.fullReindexRatioThreshold && changedFiles.length > 0) {
890
+ onProgress?.(`Reindexing ${changedFiles.length} changed files and merging with existing index`);
891
+ const incrementalIndex = await treeSummarizer(changedFiles.join(`
892
+ `));
893
+ const updatedIndex = this.cloneIndex(existingIndex);
894
+ this.removeFilesFromIndex(updatedIndex, [...deleted, ...modified]);
895
+ return this.mergeIndex(updatedIndex, incrementalIndex, currentHashes, newTreeHash);
896
+ }
897
+ if (changedFiles.length === 0 && deleted.length > 0) {
898
+ onProgress?.(`Removing ${deleted.length} deleted files from index`);
899
+ const updatedIndex = this.cloneIndex(existingIndex);
900
+ this.removeFilesFromIndex(updatedIndex, deleted);
901
+ return this.applyIndexMetadata(updatedIndex, currentHashes, newTreeHash);
902
+ }
903
+ if (changedFiles.length === 0 && deleted.length === 0) {
904
+ onProgress?.("No actual file changes, updating hashes only");
905
+ const updatedIndex = this.cloneIndex(existingIndex);
906
+ return this.applyIndexMetadata(updatedIndex, currentHashes, newTreeHash);
907
+ }
908
+ onProgress?.(`Too many changes (${(changeRatio * 100).toFixed(1)}%), performing full reindex`);
909
+ }
910
+ }
911
+ onProgress?.("AI is analyzing codebase structure...");
912
+ const index = await treeSummarizer(treeString);
913
+ return this.applyIndexMetadata(index, currentHashes, newTreeHash);
914
+ }
915
+ async getFileTree() {
867
916
  const gitmodulesPath = import_node_path5.join(this.projectPath, ".gitmodules");
868
917
  const submoduleIgnores = [];
869
918
  if (import_node_fs3.existsSync(gitmodulesPath)) {
@@ -881,7 +930,7 @@ class CodebaseIndexer {
881
930
  }
882
931
  } catch {}
883
932
  }
884
- const files = await import_globby.globby(["**/*"], {
933
+ return import_globby.globby(["**/*"], {
885
934
  cwd: this.projectPath,
886
935
  gitignore: true,
887
936
  ignore: [
@@ -922,13 +971,6 @@ class CodebaseIndexer {
922
971
  "**/*.{png,jpg,jpeg,gif,svg,ico,mp4,webm,wav,mp3,woff,woff2,eot,ttf,otf,pdf,zip,tar.gz,rar}"
923
972
  ]
924
973
  });
925
- const treeString = files.join(`
926
- `);
927
- if (onProgress)
928
- onProgress("AI is analyzing codebase structure...");
929
- const index = await treeSummarizer(treeString);
930
- index.lastIndexed = new Date().toISOString();
931
- return index;
932
974
  }
933
975
  loadIndex() {
934
976
  if (import_node_fs3.existsSync(this.indexPath)) {
@@ -947,6 +989,79 @@ class CodebaseIndexer {
947
989
  }
948
990
  import_node_fs3.writeFileSync(this.indexPath, JSON.stringify(index, null, 2));
949
991
  }
992
+ cloneIndex(index) {
993
+ return JSON.parse(JSON.stringify(index));
994
+ }
995
+ applyIndexMetadata(index, fileHashes, treeHash) {
996
+ index.lastIndexed = new Date().toISOString();
997
+ index.treeHash = treeHash;
998
+ index.fileHashes = fileHashes;
999
+ return index;
1000
+ }
1001
+ hashTree(tree) {
1002
+ return import_node_crypto2.createHash("sha256").update(tree).digest("hex");
1003
+ }
1004
+ hashFile(filePath) {
1005
+ try {
1006
+ const content = import_node_fs3.readFileSync(import_node_path5.join(this.projectPath, filePath), "utf-8");
1007
+ return import_node_crypto2.createHash("sha256").update(content).digest("hex").slice(0, 16);
1008
+ } catch {
1009
+ return null;
1010
+ }
1011
+ }
1012
+ computeFileHashes(files) {
1013
+ const hashes = {};
1014
+ for (const file of files) {
1015
+ const hash = this.hashFile(file);
1016
+ if (hash !== null) {
1017
+ hashes[file] = hash;
1018
+ }
1019
+ }
1020
+ return hashes;
1021
+ }
1022
+ diffFiles(currentHashes, existingHashes) {
1023
+ const currentFiles = Object.keys(currentHashes);
1024
+ const existingFiles = Object.keys(existingHashes);
1025
+ const existingSet = new Set(existingFiles);
1026
+ const currentSet = new Set(currentFiles);
1027
+ const added = currentFiles.filter((f) => !existingSet.has(f));
1028
+ const deleted = existingFiles.filter((f) => !currentSet.has(f));
1029
+ const modified = currentFiles.filter((f) => existingSet.has(f) && currentHashes[f] !== existingHashes[f]);
1030
+ return { added, deleted, modified };
1031
+ }
1032
+ removeFilesFromIndex(index, files) {
1033
+ const fileSet = new Set(files);
1034
+ for (const file of files) {
1035
+ delete index.responsibilities[file];
1036
+ }
1037
+ for (const [symbol, paths] of Object.entries(index.symbols)) {
1038
+ index.symbols[symbol] = paths.filter((p) => !fileSet.has(p));
1039
+ if (index.symbols[symbol].length === 0) {
1040
+ delete index.symbols[symbol];
1041
+ }
1042
+ }
1043
+ }
1044
+ mergeIndex(existing, incremental, newHashes, newTreeHash) {
1045
+ const mergedSymbols = { ...existing.symbols };
1046
+ for (const [symbol, paths] of Object.entries(incremental.symbols)) {
1047
+ if (mergedSymbols[symbol]) {
1048
+ mergedSymbols[symbol] = [
1049
+ ...new Set([...mergedSymbols[symbol], ...paths])
1050
+ ];
1051
+ } else {
1052
+ mergedSymbols[symbol] = paths;
1053
+ }
1054
+ }
1055
+ const merged = {
1056
+ symbols: mergedSymbols,
1057
+ responsibilities: {
1058
+ ...existing.responsibilities,
1059
+ ...incremental.responsibilities
1060
+ },
1061
+ lastIndexed: ""
1062
+ };
1063
+ return this.applyIndexMetadata(merged, newHashes, newTreeHash);
1064
+ }
950
1065
  }
951
1066
 
952
1067
  // src/agent/codebase-indexer-service.ts
@@ -957,9 +1072,8 @@ class CodebaseIndexerService {
957
1072
  this.deps = deps;
958
1073
  this.indexer = new CodebaseIndexer(deps.projectPath);
959
1074
  }
960
- async reindex() {
1075
+ async reindex(force = false) {
961
1076
  try {
962
- this.deps.log("Reindexing codebase...", "info");
963
1077
  const index = await this.indexer.index((msg) => this.deps.log(msg, "info"), async (tree) => {
964
1078
  const prompt = `You are a codebase analysis expert. Analyze the file tree and extract:
965
1079
  1. Key symbols (classes, functions, types) and their locations
@@ -980,7 +1094,11 @@ Return ONLY valid JSON, no markdown formatting.`;
980
1094
  return JSON.parse(jsonMatch[0]);
981
1095
  }
982
1096
  return { symbols: {}, responsibilities: {}, lastIndexed: "" };
983
- });
1097
+ }, force);
1098
+ if (index === null) {
1099
+ this.deps.log("No changes detected, skipping reindex", "info");
1100
+ return;
1101
+ }
984
1102
  this.indexer.saveIndex(index);
985
1103
  this.deps.log("Codebase reindexed successfully", "success");
986
1104
  } catch (error) {
@@ -2,17 +2,30 @@ export interface CodebaseIndex {
2
2
  symbols: Record<string, string[]>;
3
3
  responsibilities: Record<string, string>;
4
4
  lastIndexed: string;
5
+ treeHash?: string;
6
+ fileHashes?: Record<string, string>;
5
7
  }
6
8
  export declare class CodebaseIndexer {
7
9
  private projectPath;
8
10
  private indexPath;
11
+ private fullReindexRatioThreshold;
9
12
  constructor(projectPath: string);
10
13
  /**
11
14
  * Generates a codebase index by providing the entire file tree to an AI summarizer.
12
- * This is much more efficient than per-file indexing for large projects.
15
+ * Supports incremental updates - only analyzes changed files when possible.
16
+ * Returns null if no changes detected (unless force=true).
13
17
  */
14
- index(onProgress?: (message: string) => void, treeSummarizer?: (tree: string) => Promise<CodebaseIndex>): Promise<CodebaseIndex>;
18
+ index(onProgress?: (message: string) => void, treeSummarizer?: (tree: string) => Promise<CodebaseIndex>, force?: boolean): Promise<CodebaseIndex | null>;
19
+ private getFileTree;
15
20
  loadIndex(): CodebaseIndex | null;
16
21
  saveIndex(index: CodebaseIndex): void;
22
+ private cloneIndex;
23
+ private applyIndexMetadata;
24
+ private hashTree;
25
+ private hashFile;
26
+ private computeFileHashes;
27
+ private diffFiles;
28
+ private removeFilesFromIndex;
29
+ private mergeIndex;
17
30
  }
18
31
  //# sourceMappingURL=indexer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"indexer.d.ts","sourceRoot":"","sources":["../../src/core/indexer.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAClC,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,SAAS,CAAS;gBAEd,WAAW,EAAE,MAAM;IAK/B;;;OAGG;IACG,KAAK,CACT,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,EACtC,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,aAAa,CAAC,GACxD,OAAO,CAAC,aAAa,CAAC;IAsFzB,SAAS,IAAI,aAAa,GAAG,IAAI;IAWjC,SAAS,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI;CAOtC"}
1
+ {"version":3,"file":"indexer.d.ts","sourceRoot":"","sources":["../../src/core/indexer.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAClC,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACrC;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,yBAAyB,CAAe;gBAEpC,WAAW,EAAE,MAAM;IAK/B;;;;OAIG;IACG,KAAK,CACT,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,EACtC,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,aAAa,CAAC,EACzD,KAAK,UAAQ,GACZ,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;YAyGlB,WAAW;IAgEzB,SAAS,IAAI,aAAa,GAAG,IAAI;IAWjC,SAAS,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI;IAQrC,OAAO,CAAC,UAAU;IAKlB,OAAO,CAAC,kBAAkB;IAY1B,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,QAAQ;IAShB,OAAO,CAAC,iBAAiB;IAWzB,OAAO,CAAC,SAAS;IAkBjB,OAAO,CAAC,oBAAoB;IAiB5B,OAAO,CAAC,UAAU;CA8BnB"}
@@ -847,6 +847,7 @@ class ArtifactSyncer {
847
847
  }
848
848
 
849
849
  // src/core/indexer.ts
850
+ var import_node_crypto2 = require("node:crypto");
850
851
  var import_node_fs3 = require("node:fs");
851
852
  var import_node_path5 = require("node:path");
852
853
  var import_globby = require("globby");
@@ -854,16 +855,64 @@ var import_globby = require("globby");
854
855
  class CodebaseIndexer {
855
856
  projectPath;
856
857
  indexPath;
858
+ fullReindexRatioThreshold = 0.2;
857
859
  constructor(projectPath) {
858
860
  this.projectPath = projectPath;
859
861
  this.indexPath = import_node_path5.join(projectPath, ".locus", "codebase-index.json");
860
862
  }
861
- async index(onProgress, treeSummarizer) {
863
+ async index(onProgress, treeSummarizer, force = false) {
862
864
  if (!treeSummarizer) {
863
865
  throw new Error("A treeSummarizer is required for this indexing method.");
864
866
  }
865
- if (onProgress)
866
- onProgress("Generating file tree...");
867
+ onProgress?.("Generating file tree...");
868
+ const currentFiles = await this.getFileTree();
869
+ const treeString = currentFiles.join(`
870
+ `);
871
+ const newTreeHash = this.hashTree(treeString);
872
+ const existingIndex = this.loadIndex();
873
+ if (!force && existingIndex?.treeHash === newTreeHash) {
874
+ onProgress?.("No file changes detected, skipping reindex");
875
+ return null;
876
+ }
877
+ const currentHashes = this.computeFileHashes(currentFiles);
878
+ const existingHashes = existingIndex?.fileHashes;
879
+ const canIncremental = !force && existingIndex && existingHashes;
880
+ if (canIncremental) {
881
+ onProgress?.("Performing incremental update");
882
+ const { added, deleted, modified } = this.diffFiles(currentHashes, existingHashes);
883
+ const changedFiles = [...added, ...modified];
884
+ const totalChanges = changedFiles.length + deleted.length;
885
+ const existingFileCount = Object.keys(existingHashes).length;
886
+ onProgress?.(`File changes detected: ${changedFiles.length} changed, ${added.length} added, ${deleted.length} deleted`);
887
+ if (existingFileCount > 0) {
888
+ const changeRatio = totalChanges / existingFileCount;
889
+ if (changeRatio <= this.fullReindexRatioThreshold && changedFiles.length > 0) {
890
+ onProgress?.(`Reindexing ${changedFiles.length} changed files and merging with existing index`);
891
+ const incrementalIndex = await treeSummarizer(changedFiles.join(`
892
+ `));
893
+ const updatedIndex = this.cloneIndex(existingIndex);
894
+ this.removeFilesFromIndex(updatedIndex, [...deleted, ...modified]);
895
+ return this.mergeIndex(updatedIndex, incrementalIndex, currentHashes, newTreeHash);
896
+ }
897
+ if (changedFiles.length === 0 && deleted.length > 0) {
898
+ onProgress?.(`Removing ${deleted.length} deleted files from index`);
899
+ const updatedIndex = this.cloneIndex(existingIndex);
900
+ this.removeFilesFromIndex(updatedIndex, deleted);
901
+ return this.applyIndexMetadata(updatedIndex, currentHashes, newTreeHash);
902
+ }
903
+ if (changedFiles.length === 0 && deleted.length === 0) {
904
+ onProgress?.("No actual file changes, updating hashes only");
905
+ const updatedIndex = this.cloneIndex(existingIndex);
906
+ return this.applyIndexMetadata(updatedIndex, currentHashes, newTreeHash);
907
+ }
908
+ onProgress?.(`Too many changes (${(changeRatio * 100).toFixed(1)}%), performing full reindex`);
909
+ }
910
+ }
911
+ onProgress?.("AI is analyzing codebase structure...");
912
+ const index = await treeSummarizer(treeString);
913
+ return this.applyIndexMetadata(index, currentHashes, newTreeHash);
914
+ }
915
+ async getFileTree() {
867
916
  const gitmodulesPath = import_node_path5.join(this.projectPath, ".gitmodules");
868
917
  const submoduleIgnores = [];
869
918
  if (import_node_fs3.existsSync(gitmodulesPath)) {
@@ -881,7 +930,7 @@ class CodebaseIndexer {
881
930
  }
882
931
  } catch {}
883
932
  }
884
- const files = await import_globby.globby(["**/*"], {
933
+ return import_globby.globby(["**/*"], {
885
934
  cwd: this.projectPath,
886
935
  gitignore: true,
887
936
  ignore: [
@@ -922,13 +971,6 @@ class CodebaseIndexer {
922
971
  "**/*.{png,jpg,jpeg,gif,svg,ico,mp4,webm,wav,mp3,woff,woff2,eot,ttf,otf,pdf,zip,tar.gz,rar}"
923
972
  ]
924
973
  });
925
- const treeString = files.join(`
926
- `);
927
- if (onProgress)
928
- onProgress("AI is analyzing codebase structure...");
929
- const index = await treeSummarizer(treeString);
930
- index.lastIndexed = new Date().toISOString();
931
- return index;
932
974
  }
933
975
  loadIndex() {
934
976
  if (import_node_fs3.existsSync(this.indexPath)) {
@@ -947,6 +989,79 @@ class CodebaseIndexer {
947
989
  }
948
990
  import_node_fs3.writeFileSync(this.indexPath, JSON.stringify(index, null, 2));
949
991
  }
992
+ cloneIndex(index) {
993
+ return JSON.parse(JSON.stringify(index));
994
+ }
995
+ applyIndexMetadata(index, fileHashes, treeHash) {
996
+ index.lastIndexed = new Date().toISOString();
997
+ index.treeHash = treeHash;
998
+ index.fileHashes = fileHashes;
999
+ return index;
1000
+ }
1001
+ hashTree(tree) {
1002
+ return import_node_crypto2.createHash("sha256").update(tree).digest("hex");
1003
+ }
1004
+ hashFile(filePath) {
1005
+ try {
1006
+ const content = import_node_fs3.readFileSync(import_node_path5.join(this.projectPath, filePath), "utf-8");
1007
+ return import_node_crypto2.createHash("sha256").update(content).digest("hex").slice(0, 16);
1008
+ } catch {
1009
+ return null;
1010
+ }
1011
+ }
1012
+ computeFileHashes(files) {
1013
+ const hashes = {};
1014
+ for (const file of files) {
1015
+ const hash = this.hashFile(file);
1016
+ if (hash !== null) {
1017
+ hashes[file] = hash;
1018
+ }
1019
+ }
1020
+ return hashes;
1021
+ }
1022
+ diffFiles(currentHashes, existingHashes) {
1023
+ const currentFiles = Object.keys(currentHashes);
1024
+ const existingFiles = Object.keys(existingHashes);
1025
+ const existingSet = new Set(existingFiles);
1026
+ const currentSet = new Set(currentFiles);
1027
+ const added = currentFiles.filter((f) => !existingSet.has(f));
1028
+ const deleted = existingFiles.filter((f) => !currentSet.has(f));
1029
+ const modified = currentFiles.filter((f) => existingSet.has(f) && currentHashes[f] !== existingHashes[f]);
1030
+ return { added, deleted, modified };
1031
+ }
1032
+ removeFilesFromIndex(index, files) {
1033
+ const fileSet = new Set(files);
1034
+ for (const file of files) {
1035
+ delete index.responsibilities[file];
1036
+ }
1037
+ for (const [symbol, paths] of Object.entries(index.symbols)) {
1038
+ index.symbols[symbol] = paths.filter((p) => !fileSet.has(p));
1039
+ if (index.symbols[symbol].length === 0) {
1040
+ delete index.symbols[symbol];
1041
+ }
1042
+ }
1043
+ }
1044
+ mergeIndex(existing, incremental, newHashes, newTreeHash) {
1045
+ const mergedSymbols = { ...existing.symbols };
1046
+ for (const [symbol, paths] of Object.entries(incremental.symbols)) {
1047
+ if (mergedSymbols[symbol]) {
1048
+ mergedSymbols[symbol] = [
1049
+ ...new Set([...mergedSymbols[symbol], ...paths])
1050
+ ];
1051
+ } else {
1052
+ mergedSymbols[symbol] = paths;
1053
+ }
1054
+ }
1055
+ const merged = {
1056
+ symbols: mergedSymbols,
1057
+ responsibilities: {
1058
+ ...existing.responsibilities,
1059
+ ...incremental.responsibilities
1060
+ },
1061
+ lastIndexed: ""
1062
+ };
1063
+ return this.applyIndexMetadata(merged, newHashes, newTreeHash);
1064
+ }
950
1065
  }
951
1066
 
952
1067
  // src/agent/codebase-indexer-service.ts
@@ -957,9 +1072,8 @@ class CodebaseIndexerService {
957
1072
  this.deps = deps;
958
1073
  this.indexer = new CodebaseIndexer(deps.projectPath);
959
1074
  }
960
- async reindex() {
1075
+ async reindex(force = false) {
961
1076
  try {
962
- this.deps.log("Reindexing codebase...", "info");
963
1077
  const index = await this.indexer.index((msg) => this.deps.log(msg, "info"), async (tree) => {
964
1078
  const prompt = `You are a codebase analysis expert. Analyze the file tree and extract:
965
1079
  1. Key symbols (classes, functions, types) and their locations
@@ -980,7 +1094,11 @@ Return ONLY valid JSON, no markdown formatting.`;
980
1094
  return JSON.parse(jsonMatch[0]);
981
1095
  }
982
1096
  return { symbols: {}, responsibilities: {}, lastIndexed: "" };
983
- });
1097
+ }, force);
1098
+ if (index === null) {
1099
+ this.deps.log("No changes detected, skipping reindex", "info");
1100
+ return;
1101
+ }
984
1102
  this.indexer.saveIndex(index);
985
1103
  this.deps.log("Codebase reindexed successfully", "success");
986
1104
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@locusai/sdk",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -30,7 +30,7 @@
30
30
  "clean": "rm -rf node_modules"
31
31
  },
32
32
  "dependencies": {
33
- "@locusai/shared": "^0.5.0",
33
+ "@locusai/shared": "^0.5.1",
34
34
  "axios": "^1.13.2",
35
35
  "events": "^3.3.0",
36
36
  "globby": "^14.0.2"