@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 +89 -0
- package/dist/lib/file-deps.d.ts +27 -0
- package/dist/lib/file-deps.d.ts.map +1 -0
- package/dist/mcp/index.js +188 -0
- package/package.json +1 -1
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);
|