@hasna/mementos 0.10.2 → 0.10.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.
@@ -0,0 +1,27 @@
1
+ /**
2
+ * File dependency graph builder for open-mementos.
3
+ * Scans a codebase, creates 'file' entities for each source file, and
4
+ * creates 'depends_on' relations between files based on import/require statements.
5
+ *
6
+ * Supports: TypeScript, JavaScript, Python, Go (basic), Rust (basic).
7
+ */
8
+ import type { Database } from "bun:sqlite";
9
+ export interface FileDepsOptions {
10
+ root_dir: string;
11
+ project_id?: string;
12
+ extensions?: string[];
13
+ exclude_patterns?: string[];
14
+ incremental?: boolean;
15
+ }
16
+ export interface FileDepsResult {
17
+ files_scanned: number;
18
+ entities_created: number;
19
+ entities_updated: number;
20
+ relations_created: number;
21
+ errors: string[];
22
+ }
23
+ /**
24
+ * Scan a codebase, create file entities and import-based depends_on relations.
25
+ */
26
+ export declare function buildFileDependencyGraph(opts: FileDepsOptions, db?: Database): Promise<FileDepsResult>;
27
+ //# sourceMappingURL=file-deps.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-deps.d.ts","sourceRoot":"","sources":["../../src/lib/file-deps.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAK3C,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;IACzB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAgFD;;GAEG;AACH,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,eAAe,EACrB,EAAE,CAAC,EAAE,QAAQ,GACZ,OAAO,CAAC,cAAc,CAAC,CAwFzB"}
package/dist/mcp/index.js CHANGED
@@ -7651,6 +7651,163 @@ var FORMAT_UNITS = [
7651
7651
 
7652
7652
  // src/mcp/index.ts
7653
7653
  init_hooks();
7654
+
7655
+ // src/lib/file-deps.ts
7656
+ init_database();
7657
+ init_entities();
7658
+ init_relations();
7659
+ import { readdirSync, readFileSync, statSync, existsSync as existsSync3 } from "fs";
7660
+ import { join as join3, resolve as resolve3, relative, dirname as dirname3, extname, basename as basename2 } from "path";
7661
+ var DEFAULT_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".py", ".go", ".rs"];
7662
+ var DEFAULT_EXCLUDES = ["node_modules", ".git", "dist", "build", ".next", "__pycache__", "target", "vendor"];
7663
+ function parseImports(_filePath, content) {
7664
+ const imports = [];
7665
+ const tsImports = [
7666
+ /import\s+(?:.*?\s+from\s+)?['"]([^'"]+)['"]/g,
7667
+ /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
7668
+ /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g
7669
+ ];
7670
+ for (const re of tsImports) {
7671
+ let m2;
7672
+ re.lastIndex = 0;
7673
+ while ((m2 = re.exec(content)) !== null) {
7674
+ const imp = m2[1];
7675
+ if (imp.startsWith("."))
7676
+ imports.push(imp);
7677
+ }
7678
+ }
7679
+ const pyImports = /^(?:from\s+(\.+[\w.]*)|import\s+([\w.]+))/gm;
7680
+ let m;
7681
+ while ((m = pyImports.exec(content)) !== null) {
7682
+ const imp = m[1] || m[2];
7683
+ if (imp && imp.startsWith("."))
7684
+ imports.push(imp);
7685
+ }
7686
+ return [...new Set(imports)];
7687
+ }
7688
+ function resolveImport(fromFile, importPath, allFiles) {
7689
+ const dir = dirname3(fromFile);
7690
+ const base = resolve3(dir, importPath);
7691
+ if (allFiles.has(base))
7692
+ return base;
7693
+ for (const ext of [".ts", ".tsx", ".js", ".jsx", ".mjs"]) {
7694
+ const withExt = base + ext;
7695
+ if (allFiles.has(withExt))
7696
+ return withExt;
7697
+ const index = join3(base, `index${ext}`);
7698
+ if (allFiles.has(index))
7699
+ return index;
7700
+ }
7701
+ return null;
7702
+ }
7703
+ function collectFiles(dir, extensions, excludes) {
7704
+ const files = [];
7705
+ function walk(current) {
7706
+ let entries;
7707
+ try {
7708
+ entries = readdirSync(current);
7709
+ } catch {
7710
+ return;
7711
+ }
7712
+ for (const entry of entries) {
7713
+ if (excludes.some((e) => entry === e || current.includes(`/${e}/`)))
7714
+ continue;
7715
+ const full = join3(current, entry);
7716
+ let stat;
7717
+ try {
7718
+ stat = statSync(full);
7719
+ } catch {
7720
+ continue;
7721
+ }
7722
+ if (stat.isDirectory()) {
7723
+ walk(full);
7724
+ } else if (extensions.includes(extname(entry))) {
7725
+ files.push(full);
7726
+ }
7727
+ }
7728
+ }
7729
+ walk(resolve3(dir));
7730
+ return files;
7731
+ }
7732
+ async function buildFileDependencyGraph(opts, db) {
7733
+ const d = db || getDatabase();
7734
+ const result = { files_scanned: 0, entities_created: 0, entities_updated: 0, relations_created: 0, errors: [] };
7735
+ const extensions = opts.extensions ?? DEFAULT_EXTENSIONS;
7736
+ const excludes = opts.exclude_patterns ?? DEFAULT_EXCLUDES;
7737
+ const rootDir = resolve3(opts.root_dir);
7738
+ if (!existsSync3(rootDir)) {
7739
+ result.errors.push(`Directory not found: ${rootDir}`);
7740
+ return result;
7741
+ }
7742
+ const files = collectFiles(rootDir, extensions, excludes);
7743
+ const fileSet = new Set(files);
7744
+ result.files_scanned = files.length;
7745
+ const entityMap = new Map;
7746
+ const existingEntities = listEntities({ type: "file", project_id: opts.project_id, limit: 1e4 }, d);
7747
+ for (const e of existingEntities) {
7748
+ if (e.metadata?.["file_path"]) {
7749
+ entityMap.set(e.metadata["file_path"], e.id);
7750
+ }
7751
+ }
7752
+ for (const filePath of files) {
7753
+ const relPath = relative(rootDir, filePath);
7754
+ const name = basename2(filePath);
7755
+ if (entityMap.has(filePath)) {
7756
+ result.entities_updated++;
7757
+ continue;
7758
+ }
7759
+ try {
7760
+ const entity = createEntity({
7761
+ type: "file",
7762
+ name,
7763
+ description: relPath,
7764
+ project_id: opts.project_id,
7765
+ metadata: { file_path: filePath, rel_path: relPath, ext: extname(filePath), root_dir: rootDir }
7766
+ }, d);
7767
+ entityMap.set(filePath, entity.id);
7768
+ result.entities_created++;
7769
+ } catch (e) {
7770
+ result.errors.push(`Entity creation failed for ${relPath}: ${String(e)}`);
7771
+ }
7772
+ }
7773
+ for (const filePath of files) {
7774
+ const fromId = entityMap.get(filePath);
7775
+ if (!fromId)
7776
+ continue;
7777
+ let content;
7778
+ try {
7779
+ content = readFileSync(filePath, "utf-8");
7780
+ } catch {
7781
+ continue;
7782
+ }
7783
+ const imports = parseImports(filePath, content);
7784
+ for (const imp of imports) {
7785
+ const resolvedPath = resolveImport(filePath, imp, fileSet);
7786
+ if (!resolvedPath)
7787
+ continue;
7788
+ const toId = entityMap.get(resolvedPath);
7789
+ if (!toId || toId === fromId)
7790
+ continue;
7791
+ try {
7792
+ const existing = listRelations({ entity_id: fromId, relation_type: "depends_on", direction: "outgoing" }, d);
7793
+ if (existing.some((r) => r.target_entity_id === toId))
7794
+ continue;
7795
+ createRelation({
7796
+ source_entity_id: fromId,
7797
+ target_entity_id: toId,
7798
+ relation_type: "depends_on",
7799
+ metadata: { import_path: imp, project_id: opts.project_id }
7800
+ }, d);
7801
+ result.relations_created++;
7802
+ } catch (e) {
7803
+ result.errors.push(`Relation failed ${relative(rootDir, filePath)} \u2192 ${relative(rootDir, resolvedPath)}: ${String(e)}`);
7804
+ }
7805
+ }
7806
+ }
7807
+ return result;
7808
+ }
7809
+
7810
+ // src/mcp/index.ts
7654
7811
  init_built_in_hooks();
7655
7812
  init_webhook_hooks();
7656
7813
 
@@ -10323,6 +10480,26 @@ server.tool("graph_stats", "Get entity and relation counts by type.", {}, async
10323
10480
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
10324
10481
  }
10325
10482
  });
10483
+ server.tool("build_file_dep_graph", "Scan a codebase directory and build a file dependency graph: creates 'file' entities and 'depends_on' relations based on import/require statements. Use graph_query to find blast radius of a file change.", {
10484
+ root_dir: exports_external.string().describe("Root directory to scan"),
10485
+ project_id: exports_external.string().optional().describe("Project to associate file entities with"),
10486
+ extensions: exports_external.array(exports_external.string()).optional().describe("File extensions to scan (default: .ts .tsx .js .jsx .py .go .rs)"),
10487
+ exclude_patterns: exports_external.array(exports_external.string()).optional().describe("Directory/file patterns to skip (default: node_modules, dist, .git, etc.)"),
10488
+ incremental: exports_external.boolean().optional().describe("Skip files that already have entities (default: true)")
10489
+ }, async (args) => {
10490
+ try {
10491
+ const result = await buildFileDependencyGraph({
10492
+ root_dir: args.root_dir,
10493
+ project_id: args.project_id ? resolvePartialId(getDatabase(), "projects", args.project_id) ?? args.project_id : undefined,
10494
+ extensions: args.extensions,
10495
+ exclude_patterns: args.exclude_patterns,
10496
+ incremental: args.incremental ?? true
10497
+ });
10498
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
10499
+ } catch (e) {
10500
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
10501
+ }
10502
+ });
10326
10503
  var FULL_SCHEMAS = {
10327
10504
  memory_save: {
10328
10505
  description: "Save/upsert a memory. Creates new or merges with existing key.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/mementos",
3
- "version": "0.10.2",
3
+ "version": "0.10.3",
4
4
  "description": "Universal memory system for AI agents - CLI + MCP server + library API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",