@locusai/cli 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.
@@ -35423,6 +35423,7 @@ class ArtifactSyncer {
35423
35423
  }
35424
35424
 
35425
35425
  // ../sdk/src/core/indexer.ts
35426
+ import { createHash } from "node:crypto";
35426
35427
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync } from "node:fs";
35427
35428
  import { dirname, join as join4 } from "node:path";
35428
35429
 
@@ -35934,16 +35935,64 @@ var generateGlobTasksSync = normalizeArgumentsSync(generateTasksSync);
35934
35935
  class CodebaseIndexer {
35935
35936
  projectPath;
35936
35937
  indexPath;
35938
+ fullReindexRatioThreshold = 0.2;
35937
35939
  constructor(projectPath) {
35938
35940
  this.projectPath = projectPath;
35939
35941
  this.indexPath = join4(projectPath, ".locus", "codebase-index.json");
35940
35942
  }
35941
- async index(onProgress, treeSummarizer) {
35943
+ async index(onProgress, treeSummarizer, force = false) {
35942
35944
  if (!treeSummarizer) {
35943
35945
  throw new Error("A treeSummarizer is required for this indexing method.");
35944
35946
  }
35945
- if (onProgress)
35946
- onProgress("Generating file tree...");
35947
+ onProgress?.("Generating file tree...");
35948
+ const currentFiles = await this.getFileTree();
35949
+ const treeString = currentFiles.join(`
35950
+ `);
35951
+ const newTreeHash = this.hashTree(treeString);
35952
+ const existingIndex = this.loadIndex();
35953
+ if (!force && existingIndex?.treeHash === newTreeHash) {
35954
+ onProgress?.("No file changes detected, skipping reindex");
35955
+ return null;
35956
+ }
35957
+ const currentHashes = this.computeFileHashes(currentFiles);
35958
+ const existingHashes = existingIndex?.fileHashes;
35959
+ const canIncremental = !force && existingIndex && existingHashes;
35960
+ if (canIncremental) {
35961
+ onProgress?.("Performing incremental update");
35962
+ const { added, deleted, modified } = this.diffFiles(currentHashes, existingHashes);
35963
+ const changedFiles = [...added, ...modified];
35964
+ const totalChanges = changedFiles.length + deleted.length;
35965
+ const existingFileCount = Object.keys(existingHashes).length;
35966
+ onProgress?.(`File changes detected: ${changedFiles.length} changed, ${added.length} added, ${deleted.length} deleted`);
35967
+ if (existingFileCount > 0) {
35968
+ const changeRatio = totalChanges / existingFileCount;
35969
+ if (changeRatio <= this.fullReindexRatioThreshold && changedFiles.length > 0) {
35970
+ onProgress?.(`Reindexing ${changedFiles.length} changed files and merging with existing index`);
35971
+ const incrementalIndex = await treeSummarizer(changedFiles.join(`
35972
+ `));
35973
+ const updatedIndex = this.cloneIndex(existingIndex);
35974
+ this.removeFilesFromIndex(updatedIndex, [...deleted, ...modified]);
35975
+ return this.mergeIndex(updatedIndex, incrementalIndex, currentHashes, newTreeHash);
35976
+ }
35977
+ if (changedFiles.length === 0 && deleted.length > 0) {
35978
+ onProgress?.(`Removing ${deleted.length} deleted files from index`);
35979
+ const updatedIndex = this.cloneIndex(existingIndex);
35980
+ this.removeFilesFromIndex(updatedIndex, deleted);
35981
+ return this.applyIndexMetadata(updatedIndex, currentHashes, newTreeHash);
35982
+ }
35983
+ if (changedFiles.length === 0 && deleted.length === 0) {
35984
+ onProgress?.("No actual file changes, updating hashes only");
35985
+ const updatedIndex = this.cloneIndex(existingIndex);
35986
+ return this.applyIndexMetadata(updatedIndex, currentHashes, newTreeHash);
35987
+ }
35988
+ onProgress?.(`Too many changes (${(changeRatio * 100).toFixed(1)}%), performing full reindex`);
35989
+ }
35990
+ }
35991
+ onProgress?.("AI is analyzing codebase structure...");
35992
+ const index = await treeSummarizer(treeString);
35993
+ return this.applyIndexMetadata(index, currentHashes, newTreeHash);
35994
+ }
35995
+ async getFileTree() {
35947
35996
  const gitmodulesPath = join4(this.projectPath, ".gitmodules");
35948
35997
  const submoduleIgnores = [];
35949
35998
  if (existsSync3(gitmodulesPath)) {
@@ -35961,7 +36010,7 @@ class CodebaseIndexer {
35961
36010
  }
35962
36011
  } catch {}
35963
36012
  }
35964
- const files = await globby(["**/*"], {
36013
+ return globby(["**/*"], {
35965
36014
  cwd: this.projectPath,
35966
36015
  gitignore: true,
35967
36016
  ignore: [
@@ -36002,13 +36051,6 @@ class CodebaseIndexer {
36002
36051
  "**/*.{png,jpg,jpeg,gif,svg,ico,mp4,webm,wav,mp3,woff,woff2,eot,ttf,otf,pdf,zip,tar.gz,rar}"
36003
36052
  ]
36004
36053
  });
36005
- const treeString = files.join(`
36006
- `);
36007
- if (onProgress)
36008
- onProgress("AI is analyzing codebase structure...");
36009
- const index = await treeSummarizer(treeString);
36010
- index.lastIndexed = new Date().toISOString();
36011
- return index;
36012
36054
  }
36013
36055
  loadIndex() {
36014
36056
  if (existsSync3(this.indexPath)) {
@@ -36027,6 +36069,79 @@ class CodebaseIndexer {
36027
36069
  }
36028
36070
  writeFileSync(this.indexPath, JSON.stringify(index, null, 2));
36029
36071
  }
36072
+ cloneIndex(index) {
36073
+ return JSON.parse(JSON.stringify(index));
36074
+ }
36075
+ applyIndexMetadata(index, fileHashes, treeHash) {
36076
+ index.lastIndexed = new Date().toISOString();
36077
+ index.treeHash = treeHash;
36078
+ index.fileHashes = fileHashes;
36079
+ return index;
36080
+ }
36081
+ hashTree(tree) {
36082
+ return createHash("sha256").update(tree).digest("hex");
36083
+ }
36084
+ hashFile(filePath) {
36085
+ try {
36086
+ const content = readFileSync3(join4(this.projectPath, filePath), "utf-8");
36087
+ return createHash("sha256").update(content).digest("hex").slice(0, 16);
36088
+ } catch {
36089
+ return null;
36090
+ }
36091
+ }
36092
+ computeFileHashes(files) {
36093
+ const hashes = {};
36094
+ for (const file2 of files) {
36095
+ const hash2 = this.hashFile(file2);
36096
+ if (hash2 !== null) {
36097
+ hashes[file2] = hash2;
36098
+ }
36099
+ }
36100
+ return hashes;
36101
+ }
36102
+ diffFiles(currentHashes, existingHashes) {
36103
+ const currentFiles = Object.keys(currentHashes);
36104
+ const existingFiles = Object.keys(existingHashes);
36105
+ const existingSet = new Set(existingFiles);
36106
+ const currentSet = new Set(currentFiles);
36107
+ const added = currentFiles.filter((f) => !existingSet.has(f));
36108
+ const deleted = existingFiles.filter((f) => !currentSet.has(f));
36109
+ const modified = currentFiles.filter((f) => existingSet.has(f) && currentHashes[f] !== existingHashes[f]);
36110
+ return { added, deleted, modified };
36111
+ }
36112
+ removeFilesFromIndex(index, files) {
36113
+ const fileSet = new Set(files);
36114
+ for (const file2 of files) {
36115
+ delete index.responsibilities[file2];
36116
+ }
36117
+ for (const [symbol2, paths] of Object.entries(index.symbols)) {
36118
+ index.symbols[symbol2] = paths.filter((p) => !fileSet.has(p));
36119
+ if (index.symbols[symbol2].length === 0) {
36120
+ delete index.symbols[symbol2];
36121
+ }
36122
+ }
36123
+ }
36124
+ mergeIndex(existing, incremental, newHashes, newTreeHash) {
36125
+ const mergedSymbols = { ...existing.symbols };
36126
+ for (const [symbol2, paths] of Object.entries(incremental.symbols)) {
36127
+ if (mergedSymbols[symbol2]) {
36128
+ mergedSymbols[symbol2] = [
36129
+ ...new Set([...mergedSymbols[symbol2], ...paths])
36130
+ ];
36131
+ } else {
36132
+ mergedSymbols[symbol2] = paths;
36133
+ }
36134
+ }
36135
+ const merged = {
36136
+ symbols: mergedSymbols,
36137
+ responsibilities: {
36138
+ ...existing.responsibilities,
36139
+ ...incremental.responsibilities
36140
+ },
36141
+ lastIndexed: ""
36142
+ };
36143
+ return this.applyIndexMetadata(merged, newHashes, newTreeHash);
36144
+ }
36030
36145
  }
36031
36146
 
36032
36147
  // ../sdk/src/agent/codebase-indexer-service.ts
@@ -36037,9 +36152,8 @@ class CodebaseIndexerService {
36037
36152
  this.deps = deps;
36038
36153
  this.indexer = new CodebaseIndexer(deps.projectPath);
36039
36154
  }
36040
- async reindex() {
36155
+ async reindex(force = false) {
36041
36156
  try {
36042
- this.deps.log("Reindexing codebase...", "info");
36043
36157
  const index = await this.indexer.index((msg) => this.deps.log(msg, "info"), async (tree) => {
36044
36158
  const prompt = `You are a codebase analysis expert. Analyze the file tree and extract:
36045
36159
  1. Key symbols (classes, functions, types) and their locations
@@ -36060,7 +36174,11 @@ Return ONLY valid JSON, no markdown formatting.`;
36060
36174
  return JSON.parse(jsonMatch[0]);
36061
36175
  }
36062
36176
  return { symbols: {}, responsibilities: {}, lastIndexed: "" };
36063
- });
36177
+ }, force);
36178
+ if (index === null) {
36179
+ this.deps.log("No changes detected, skipping reindex", "info");
36180
+ return;
36181
+ }
36064
36182
  this.indexer.saveIndex(index);
36065
36183
  this.deps.log("Codebase reindexed successfully", "success");
36066
36184
  } catch (error48) {
package/bin/locus.js CHANGED
@@ -17129,6 +17129,7 @@ class ArtifactSyncer {
17129
17129
  }
17130
17130
  }
17131
17131
  // ../sdk/src/core/indexer.ts
17132
+ import { createHash } from "node:crypto";
17132
17133
  import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "node:fs";
17133
17134
  import { dirname, join as join3 } from "node:path";
17134
17135
 
@@ -17640,16 +17641,64 @@ var generateGlobTasksSync = normalizeArgumentsSync(generateTasksSync);
17640
17641
  class CodebaseIndexer {
17641
17642
  projectPath;
17642
17643
  indexPath;
17644
+ fullReindexRatioThreshold = 0.2;
17643
17645
  constructor(projectPath) {
17644
17646
  this.projectPath = projectPath;
17645
17647
  this.indexPath = join3(projectPath, ".locus", "codebase-index.json");
17646
17648
  }
17647
- async index(onProgress, treeSummarizer) {
17649
+ async index(onProgress, treeSummarizer, force = false) {
17648
17650
  if (!treeSummarizer) {
17649
17651
  throw new Error("A treeSummarizer is required for this indexing method.");
17650
17652
  }
17651
- if (onProgress)
17652
- onProgress("Generating file tree...");
17653
+ onProgress?.("Generating file tree...");
17654
+ const currentFiles = await this.getFileTree();
17655
+ const treeString = currentFiles.join(`
17656
+ `);
17657
+ const newTreeHash = this.hashTree(treeString);
17658
+ const existingIndex = this.loadIndex();
17659
+ if (!force && existingIndex?.treeHash === newTreeHash) {
17660
+ onProgress?.("No file changes detected, skipping reindex");
17661
+ return null;
17662
+ }
17663
+ const currentHashes = this.computeFileHashes(currentFiles);
17664
+ const existingHashes = existingIndex?.fileHashes;
17665
+ const canIncremental = !force && existingIndex && existingHashes;
17666
+ if (canIncremental) {
17667
+ onProgress?.("Performing incremental update");
17668
+ const { added, deleted, modified } = this.diffFiles(currentHashes, existingHashes);
17669
+ const changedFiles = [...added, ...modified];
17670
+ const totalChanges = changedFiles.length + deleted.length;
17671
+ const existingFileCount = Object.keys(existingHashes).length;
17672
+ onProgress?.(`File changes detected: ${changedFiles.length} changed, ${added.length} added, ${deleted.length} deleted`);
17673
+ if (existingFileCount > 0) {
17674
+ const changeRatio = totalChanges / existingFileCount;
17675
+ if (changeRatio <= this.fullReindexRatioThreshold && changedFiles.length > 0) {
17676
+ onProgress?.(`Reindexing ${changedFiles.length} changed files and merging with existing index`);
17677
+ const incrementalIndex = await treeSummarizer(changedFiles.join(`
17678
+ `));
17679
+ const updatedIndex = this.cloneIndex(existingIndex);
17680
+ this.removeFilesFromIndex(updatedIndex, [...deleted, ...modified]);
17681
+ return this.mergeIndex(updatedIndex, incrementalIndex, currentHashes, newTreeHash);
17682
+ }
17683
+ if (changedFiles.length === 0 && deleted.length > 0) {
17684
+ onProgress?.(`Removing ${deleted.length} deleted files from index`);
17685
+ const updatedIndex = this.cloneIndex(existingIndex);
17686
+ this.removeFilesFromIndex(updatedIndex, deleted);
17687
+ return this.applyIndexMetadata(updatedIndex, currentHashes, newTreeHash);
17688
+ }
17689
+ if (changedFiles.length === 0 && deleted.length === 0) {
17690
+ onProgress?.("No actual file changes, updating hashes only");
17691
+ const updatedIndex = this.cloneIndex(existingIndex);
17692
+ return this.applyIndexMetadata(updatedIndex, currentHashes, newTreeHash);
17693
+ }
17694
+ onProgress?.(`Too many changes (${(changeRatio * 100).toFixed(1)}%), performing full reindex`);
17695
+ }
17696
+ }
17697
+ onProgress?.("AI is analyzing codebase structure...");
17698
+ const index = await treeSummarizer(treeString);
17699
+ return this.applyIndexMetadata(index, currentHashes, newTreeHash);
17700
+ }
17701
+ async getFileTree() {
17653
17702
  const gitmodulesPath = join3(this.projectPath, ".gitmodules");
17654
17703
  const submoduleIgnores = [];
17655
17704
  if (existsSync2(gitmodulesPath)) {
@@ -17667,7 +17716,7 @@ class CodebaseIndexer {
17667
17716
  }
17668
17717
  } catch {}
17669
17718
  }
17670
- const files = await globby(["**/*"], {
17719
+ return globby(["**/*"], {
17671
17720
  cwd: this.projectPath,
17672
17721
  gitignore: true,
17673
17722
  ignore: [
@@ -17708,13 +17757,6 @@ class CodebaseIndexer {
17708
17757
  "**/*.{png,jpg,jpeg,gif,svg,ico,mp4,webm,wav,mp3,woff,woff2,eot,ttf,otf,pdf,zip,tar.gz,rar}"
17709
17758
  ]
17710
17759
  });
17711
- const treeString = files.join(`
17712
- `);
17713
- if (onProgress)
17714
- onProgress("AI is analyzing codebase structure...");
17715
- const index = await treeSummarizer(treeString);
17716
- index.lastIndexed = new Date().toISOString();
17717
- return index;
17718
17760
  }
17719
17761
  loadIndex() {
17720
17762
  if (existsSync2(this.indexPath)) {
@@ -17733,6 +17775,79 @@ class CodebaseIndexer {
17733
17775
  }
17734
17776
  writeFileSync(this.indexPath, JSON.stringify(index, null, 2));
17735
17777
  }
17778
+ cloneIndex(index) {
17779
+ return JSON.parse(JSON.stringify(index));
17780
+ }
17781
+ applyIndexMetadata(index, fileHashes, treeHash) {
17782
+ index.lastIndexed = new Date().toISOString();
17783
+ index.treeHash = treeHash;
17784
+ index.fileHashes = fileHashes;
17785
+ return index;
17786
+ }
17787
+ hashTree(tree) {
17788
+ return createHash("sha256").update(tree).digest("hex");
17789
+ }
17790
+ hashFile(filePath) {
17791
+ try {
17792
+ const content = readFileSync2(join3(this.projectPath, filePath), "utf-8");
17793
+ return createHash("sha256").update(content).digest("hex").slice(0, 16);
17794
+ } catch {
17795
+ return null;
17796
+ }
17797
+ }
17798
+ computeFileHashes(files) {
17799
+ const hashes = {};
17800
+ for (const file of files) {
17801
+ const hash = this.hashFile(file);
17802
+ if (hash !== null) {
17803
+ hashes[file] = hash;
17804
+ }
17805
+ }
17806
+ return hashes;
17807
+ }
17808
+ diffFiles(currentHashes, existingHashes) {
17809
+ const currentFiles = Object.keys(currentHashes);
17810
+ const existingFiles = Object.keys(existingHashes);
17811
+ const existingSet = new Set(existingFiles);
17812
+ const currentSet = new Set(currentFiles);
17813
+ const added = currentFiles.filter((f) => !existingSet.has(f));
17814
+ const deleted = existingFiles.filter((f) => !currentSet.has(f));
17815
+ const modified = currentFiles.filter((f) => existingSet.has(f) && currentHashes[f] !== existingHashes[f]);
17816
+ return { added, deleted, modified };
17817
+ }
17818
+ removeFilesFromIndex(index, files) {
17819
+ const fileSet = new Set(files);
17820
+ for (const file of files) {
17821
+ delete index.responsibilities[file];
17822
+ }
17823
+ for (const [symbol, paths] of Object.entries(index.symbols)) {
17824
+ index.symbols[symbol] = paths.filter((p) => !fileSet.has(p));
17825
+ if (index.symbols[symbol].length === 0) {
17826
+ delete index.symbols[symbol];
17827
+ }
17828
+ }
17829
+ }
17830
+ mergeIndex(existing, incremental, newHashes, newTreeHash) {
17831
+ const mergedSymbols = { ...existing.symbols };
17832
+ for (const [symbol, paths] of Object.entries(incremental.symbols)) {
17833
+ if (mergedSymbols[symbol]) {
17834
+ mergedSymbols[symbol] = [
17835
+ ...new Set([...mergedSymbols[symbol], ...paths])
17836
+ ];
17837
+ } else {
17838
+ mergedSymbols[symbol] = paths;
17839
+ }
17840
+ }
17841
+ const merged = {
17842
+ symbols: mergedSymbols,
17843
+ responsibilities: {
17844
+ ...existing.responsibilities,
17845
+ ...incremental.responsibilities
17846
+ },
17847
+ lastIndexed: ""
17848
+ };
17849
+ return this.applyIndexMetadata(merged, newHashes, newTreeHash);
17850
+ }
17736
17851
  }
17737
17852
 
17738
17853
  // ../sdk/src/agent/codebase-indexer-service.ts
@@ -17743,9 +17858,8 @@ class CodebaseIndexerService {
17743
17858
  this.deps = deps;
17744
17859
  this.indexer = new CodebaseIndexer(deps.projectPath);
17745
17860
  }
17746
- async reindex() {
17861
+ async reindex(force = false) {
17747
17862
  try {
17748
- this.deps.log("Reindexing codebase...", "info");
17749
17863
  const index = await this.indexer.index((msg) => this.deps.log(msg, "info"), async (tree) => {
17750
17864
  const prompt = `You are a codebase analysis expert. Analyze the file tree and extract:
17751
17865
  1. Key symbols (classes, functions, types) and their locations
@@ -17766,7 +17880,11 @@ Return ONLY valid JSON, no markdown formatting.`;
17766
17880
  return JSON.parse(jsonMatch[0]);
17767
17881
  }
17768
17882
  return { symbols: {}, responsibilities: {}, lastIndexed: "" };
17769
- });
17883
+ }, force);
17884
+ if (index === null) {
17885
+ this.deps.log("No changes detected, skipping reindex", "info");
17886
+ return;
17887
+ }
17770
17888
  this.indexer.saveIndex(index);
17771
17889
  this.deps.log("Codebase reindexed successfully", "success");
17772
17890
  } catch (error) {
@@ -36923,9 +37041,9 @@ async function runCommand(args) {
36923
37041
  const projectPath = values.dir || process.cwd();
36924
37042
  requireInitialization(projectPath, "run");
36925
37043
  new ConfigManager(projectPath).updateVersion(VERSION2);
36926
- const apiKey = values["api-key"] || process.env.LOCUS_API_KEY;
36927
- const workspaceId = values.workspace || process.env.LOCUS_WORKSPACE_ID;
36928
- const provider = resolveProvider2(values.provider || process.env.LOCUS_AI_PROVIDER);
37044
+ const apiKey = values["api-key"];
37045
+ const workspaceId = values.workspace;
37046
+ const provider = resolveProvider2(values.provider);
36929
37047
  const model = values.model || DEFAULT_MODEL[provider];
36930
37048
  if (!apiKey || !workspaceId) {
36931
37049
  console.error(c.error("Error: --api-key and --workspace are required"));
@@ -36979,7 +37097,9 @@ async function indexCommand(args) {
36979
37097
  const indexer = new CodebaseIndexer(projectPath);
36980
37098
  console.log(`${c.primary("\uD83D\uDD0D Indexing codebase in")} ${c.bold(projectPath)}...`);
36981
37099
  const index = await indexer.index((msg) => console.log(` ${c.dim(msg)}`), (tree) => summarizer.summarize(tree));
36982
- indexer.saveIndex(index);
37100
+ if (index) {
37101
+ indexer.saveIndex(index);
37102
+ }
36983
37103
  console.log(c.success("✅ Indexing complete!"));
36984
37104
  }
36985
37105
  async function initCommand() {
@@ -37043,11 +37163,6 @@ Examples:
37043
37163
  locus index
37044
37164
  locus run --api-key YOUR_KEY --workspace WORKSPACE_ID
37045
37165
 
37046
- Environment Variables:
37047
- LOCUS_API_KEY API key for authentication
37048
- LOCUS_WORKSPACE_ID Workspace ID
37049
- LOCUS_AI_PROVIDER AI provider: claude or codex
37050
-
37051
37166
  For more information, visit: https://locusai.dev/docs
37052
37167
  `);
37053
37168
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@locusai/cli",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "CLI for Locus - AI-native project management platform",
5
5
  "type": "module",
6
6
  "bin": {
@@ -32,7 +32,7 @@
32
32
  "author": "",
33
33
  "license": "MIT",
34
34
  "dependencies": {
35
- "@locusai/sdk": "^0.5.0"
35
+ "@locusai/sdk": "^0.5.1"
36
36
  },
37
37
  "devDependencies": {}
38
38
  }