@hasna/mementos 0.10.2 → 0.10.4

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/cli/index.js CHANGED
@@ -6920,6 +6920,13 @@ function listAgents(db) {
6920
6920
  const rows = d.query("SELECT * FROM agents ORDER BY last_seen_at DESC").all();
6921
6921
  return rows.map(parseAgentRow);
6922
6922
  }
6923
+ function touchAgent(idOrName, db) {
6924
+ const d = db || getDatabase();
6925
+ const agent = getAgent(idOrName, d);
6926
+ if (!agent)
6927
+ return;
6928
+ d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [now(), agent.id]);
6929
+ }
6923
6930
  function updateAgent(id, updates, db) {
6924
6931
  const d = db || getDatabase();
6925
6932
  const agent = getAgent(id, d);
@@ -6960,6 +6967,40 @@ function updateAgent(id, updates, db) {
6960
6967
  return getAgent(agent.id, d);
6961
6968
  }
6962
6969
 
6970
+ // src/lib/focus.ts
6971
+ init_hooks();
6972
+ var sessionFocus = new Map;
6973
+ function setFocus(agentId, projectId) {
6974
+ const previous = getFocusCached(agentId);
6975
+ sessionFocus.set(agentId, projectId);
6976
+ updateAgent(agentId, { active_project_id: projectId });
6977
+ if (projectId && projectId !== previous) {
6978
+ hookRegistry.runHooks("OnSessionStart", {
6979
+ agentId,
6980
+ projectId,
6981
+ timestamp: Date.now()
6982
+ });
6983
+ } else if (!projectId && previous) {
6984
+ hookRegistry.runHooks("OnSessionEnd", {
6985
+ agentId,
6986
+ projectId: previous,
6987
+ timestamp: Date.now()
6988
+ });
6989
+ }
6990
+ }
6991
+ function getFocusCached(agentId) {
6992
+ return sessionFocus.get(agentId) ?? null;
6993
+ }
6994
+ function getFocus(agentId) {
6995
+ if (sessionFocus.has(agentId)) {
6996
+ return sessionFocus.get(agentId) ?? null;
6997
+ }
6998
+ const agent = getAgent(agentId);
6999
+ const projectId = agent?.active_project_id ?? null;
7000
+ sessionFocus.set(agentId, projectId);
7001
+ return projectId;
7002
+ }
7003
+
6963
7004
  // src/db/projects.ts
6964
7005
  init_database();
6965
7006
  function parseProjectRow(row) {
@@ -10832,4 +10873,52 @@ session_end = "bun ${script}"`));
10832
10873
  console.log("Usage: mementos session setup-hook --claude | --codex");
10833
10874
  }
10834
10875
  });
10876
+ program2.command("heartbeat [agent-id]").description("Update last_seen_at to signal this agent is active").action((agentId) => {
10877
+ const globalOpts = program2.opts();
10878
+ const id = agentId || globalOpts.agent;
10879
+ if (!id) {
10880
+ process.stderr.write(`Agent ID required. Use --agent or pass as argument.
10881
+ `);
10882
+ process.exit(1);
10883
+ }
10884
+ const agent = getAgent(id);
10885
+ if (!agent) {
10886
+ process.stderr.write(`Agent not found: ${id}
10887
+ `);
10888
+ process.exit(1);
10889
+ }
10890
+ touchAgent(agent.id);
10891
+ if (globalOpts.json)
10892
+ console.log(JSON.stringify({ agent_id: agent.id, name: agent.name, last_seen_at: new Date().toISOString() }));
10893
+ else
10894
+ console.log(chalk.green(`\u2665 ${agent.name} (${agent.id}) \u2014 heartbeat sent`));
10895
+ });
10896
+ program2.command("set-focus [project]").description("Focus this agent on a project (or clear focus if no project given)").option("--agent <id>", "Agent ID").action((project, opts) => {
10897
+ const globalOpts = program2.opts();
10898
+ const agentId = opts?.agent || globalOpts.agent;
10899
+ if (!agentId) {
10900
+ process.stderr.write(`Agent ID required. Use --agent.
10901
+ `);
10902
+ process.exit(1);
10903
+ }
10904
+ setFocus(agentId, project ?? null);
10905
+ if (project)
10906
+ console.log(chalk.green(`Focused: ${agentId} \u2192 project ${project}`));
10907
+ else
10908
+ console.log(chalk.dim(`Focus cleared for ${agentId}`));
10909
+ });
10910
+ program2.command("get-focus").description("Show the current project focus for an agent").option("--agent <id>", "Agent ID").action((opts) => {
10911
+ const globalOpts = program2.opts();
10912
+ const agentId = opts?.agent || globalOpts.agent;
10913
+ if (!agentId) {
10914
+ process.stderr.write(`Agent ID required. Use --agent.
10915
+ `);
10916
+ process.exit(1);
10917
+ }
10918
+ const focus = getFocus(agentId);
10919
+ if (focus)
10920
+ console.log(chalk.cyan(`Focus: ${focus}`));
10921
+ else
10922
+ console.log(chalk.dim("No focus set."));
10923
+ });
10835
10924
  program2.parse(process.argv);
@@ -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.",
@@ -10988,6 +11165,17 @@ server.tool("set_focus", "Set focus for an agent on a project. Memory ops will a
10988
11165
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
10989
11166
  }
10990
11167
  });
11168
+ server.tool("heartbeat", "Update agent last_seen_at to signal active session. Call periodically during long tasks to prevent being marked stale.", { agent_id: exports_external.string().describe("Agent ID or name") }, async (args) => {
11169
+ try {
11170
+ const agent = getAgent(args.agent_id);
11171
+ if (!agent)
11172
+ return { content: [{ type: "text", text: `Agent not found: ${args.agent_id}` }], isError: true };
11173
+ touchAgent(agent.id);
11174
+ return { content: [{ type: "text", text: `\u2665 ${agent.name} (${agent.id}) \u2014 last_seen_at updated` }] };
11175
+ } catch (e) {
11176
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
11177
+ }
11178
+ });
10991
11179
  server.tool("get_focus", "Get the current focus project for an agent.", { agent_id: exports_external.string() }, async (args) => {
10992
11180
  try {
10993
11181
  const projectId = getFocus(args.agent_id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/mementos",
3
- "version": "0.10.2",
3
+ "version": "0.10.4",
4
4
  "description": "Universal memory system for AI agents - CLI + MCP server + library API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",