@datafrog-io/n2n-memory 1.2.1 → 1.2.2
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/CHANGELOG.md +26 -1
- package/README.md +114 -9
- package/build/core/memory-manager.js +66 -51
- package/build/core/memory-manager.js.map +1 -1
- package/build/core/memory-service.js +36 -38
- package/build/core/memory-service.js.map +1 -1
- package/build/handlers/mcp-handlers.js +6 -5
- package/build/handlers/mcp-handlers.js.map +1 -1
- package/build/index.js +23 -7
- package/build/index.js.map +1 -1
- package/build/tools/definitions.js +17 -12
- package/build/tools/definitions.js.map +1 -1
- package/build/tools/schemas.js +1 -1
- package/build/tools/schemas.js.map +1 -1
- package/build/utils/logging.js +27 -0
- package/build/utils/logging.js.map +1 -0
- package/build/utils/path-utils.js +21 -9
- package/build/utils/path-utils.js.map +1 -1
- package/docs/API_REFERENCE.md +283 -0
- package/docs/API_REFERENCE_zh.md +283 -0
- package/docs/CHANGELOG_zh.md +108 -0
- package/docs/DESIGN.md +72 -0
- package/docs/DESIGN_zh.md +72 -0
- package/docs/DEVELOPMENT.md +98 -0
- package/docs/DEVELOPMENT_zh.md +98 -0
- package/docs/README_zh.md +195 -0
- package/llms.txt +83 -0
- package/package.json +55 -13
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,31 @@
|
|
|
6
6
|
|
|
7
7
|
All notable changes to this project will be documented in this file.
|
|
8
8
|
|
|
9
|
+
## [Unreleased]
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- Normal reads now refresh cached graph/context snapshots when the underlying `.mcp` files change.
|
|
13
|
+
- Existing but unreadable `memory.json` or `context.json` files now raise data integrity errors instead of being treated as empty state.
|
|
14
|
+
- `n2n_create_relations` now rejects relations that reference missing entities.
|
|
15
|
+
- `n2n_export_markdown` now rejects absolute output paths and relative paths that escape the project root.
|
|
16
|
+
- `projectPath` validation now rejects relative paths before resolving them.
|
|
17
|
+
- Production and development dependency audits now pass with zero vulnerabilities.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- Clarified that `projectPath` must point to the project root or workspace top-level directory.
|
|
21
|
+
- Refactored JSON storage writes through a shared atomic-write path and normalized graph copies before persisting.
|
|
22
|
+
- Switched the test runner from Mocha to Vitest to keep the development dependency tree clean.
|
|
23
|
+
- Tool discovery now exposes concrete JSON input schemas generated from the Zod schemas.
|
|
24
|
+
- README-only directories are treated as weak roots and are rejected during project recognition.
|
|
25
|
+
- Server logs hide full local paths by default unless `N2N_LOG_LEVEL=debug` is set.
|
|
26
|
+
- Updated English and Chinese documentation to reflect the current tool set, storage contract, path containment rules, and graph integrity behavior.
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
- GitHub CI workflow for pull requests and pushes to `main`.
|
|
30
|
+
- Hardened publish workflow that runs the full check suite before `npm publish`.
|
|
31
|
+
- npm package `files` whitelist and richer package metadata.
|
|
32
|
+
- Open-source governance files: contributing guide, security policy, code of conduct, issue templates, and PR template.
|
|
33
|
+
|
|
9
34
|
## [1.2.1] - 2026-01-12
|
|
10
35
|
|
|
11
36
|
### Added
|
|
@@ -28,7 +53,7 @@ All notable changes to this project will be documented in this file.
|
|
|
28
53
|
- Supports word-level partial matching and semantic similarity via Jaccard index.
|
|
29
54
|
- **New Unit Tests**:
|
|
30
55
|
- Comprehensive test suite for all similarity functions (`similarity.test.ts`).
|
|
31
|
-
-
|
|
56
|
+
- Expanded regression coverage for memory and search behavior.
|
|
32
57
|
|
|
33
58
|
## [1.1.0] - 2024-12-19
|
|
34
59
|
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# n2n-memory
|
|
2
2
|
|
|
3
|
+
Project-local memory MCP server for AI coding agents.
|
|
4
|
+
|
|
3
5
|
[](https://www.npmjs.com/package/@datafrog-io/n2n-memory)
|
|
4
6
|
[](https://www.npmjs.com/package/@datafrog-io/n2n-memory)
|
|
5
7
|
[](https://github.com/n2ns/n2n-memory/blob/main/LICENSE)
|
|
@@ -14,16 +16,56 @@
|
|
|
14
16
|
|
|
15
17
|
> **Context as code. Memory as asset.**
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
n2n-memory is an open-source, local-first Model Context Protocol (MCP) memory server for AI coding assistants. It prevents cross-project memory pollution by storing durable project knowledge in `.mcp/memory.json` and active task context in `.mcp/context.json` inside each repository.
|
|
20
|
+
|
|
21
|
+
### Search-friendly positioning
|
|
22
|
+
|
|
23
|
+
If you are searching for:
|
|
24
|
+
- AI coding memory
|
|
25
|
+
- project-local MCP memory
|
|
26
|
+
- local-first context memory for coding agents
|
|
27
|
+
- repository-isolated memory graph for Claude/Cursor/VS Code workflows
|
|
28
|
+
|
|
29
|
+
This server is designed for these goals: deterministic JSON output, project isolation, and Git-friendly reviewability.
|
|
30
|
+
|
|
31
|
+
## What Is n2n-memory?
|
|
32
|
+
|
|
33
|
+
n2n-memory gives AI coding tools a project-local knowledge graph they can read and update through MCP. It is designed for developers and teams who use AI assistants across multiple repositories and want memory that is local, auditable, Git-friendly, and not mixed with unrelated projects.
|
|
34
|
+
|
|
35
|
+
**TL;DR**
|
|
36
|
+
- **Install**: `npx -y @datafrog-io/n2n-memory`
|
|
37
|
+
- **Protocol**: Model Context Protocol (MCP), stdio transport
|
|
38
|
+
- **Storage**: `.mcp/memory.json` for durable memory, `.mcp/context.json` for active task state
|
|
39
|
+
- **Best for**: AI coding assistant memory, project architecture notes, shared team context
|
|
40
|
+
- **Not for**: cloud sync, global personal memory, source code indexing, or vector database replacement
|
|
18
41
|
|
|
19
42
|
### 🌟 Key Highlights
|
|
20
|
-
- **Project-Level Physical Isolation**: Memory
|
|
43
|
+
- **Project-Level Physical Isolation**: Memory and active context are stored under `[Project Root]/.mcp/`.
|
|
21
44
|
- **Git-Friendly**: JSON data is automatically sorted by key to generate clean and readable `git diff`.
|
|
45
|
+
- **Dual-Buffer State**: Durable graph knowledge lives in `memory.json`; high-frequency task state lives in `context.json`.
|
|
22
46
|
- **Tool Agnostic**: Uses the `.mcp` naming convention, not tied to any specific AI brand or IDE plugin.
|
|
23
47
|
- **Assets for Your Code**: Memory stays with your code; team members can share AI's understanding of the architecture by simply pulling the repository.
|
|
24
|
-
- **Universal Compatibility**: Works with
|
|
48
|
+
- **Universal Compatibility**: Works with MCP-enabled clients and AI coding tools that support stdio MCP servers.
|
|
25
49
|
- **Privacy-First**: Built with security by design, keeping your data local and isolated.
|
|
26
50
|
|
|
51
|
+
## Use Cases
|
|
52
|
+
|
|
53
|
+
- Restore project context at the start of an AI coding session.
|
|
54
|
+
- Preserve architecture decisions, implementation notes, known pitfalls, and active task state.
|
|
55
|
+
- Share durable project memory with teammates through Git.
|
|
56
|
+
- Keep memory isolated per repository when working across many client, product, or open-source projects.
|
|
57
|
+
- Query a local project knowledge graph without adding a database or cloud service.
|
|
58
|
+
|
|
59
|
+
## How It Compares
|
|
60
|
+
|
|
61
|
+
| Approach | Best for | Tradeoff |
|
|
62
|
+
| --- | --- | --- |
|
|
63
|
+
| n2n-memory | Project-local MCP memory for AI coding agents | Requires assistants to call MCP tools intentionally |
|
|
64
|
+
| Global MCP memory | Personal memory across many chats or projects | Can mix unrelated project context |
|
|
65
|
+
| Markdown memory bank | Human-readable project notes | Less structured for graph queries and tool updates |
|
|
66
|
+
| Vector database memory | Semantic retrieval over large corpora | Heavier infrastructure and less deterministic diffs |
|
|
67
|
+
| IDE-specific rules | Steering assistant behavior in one IDE | Less portable across MCP clients |
|
|
68
|
+
|
|
27
69
|
### 🚀 Quick Start
|
|
28
70
|
|
|
29
71
|
#### 1. Installation & Config (IDE / Claude Desktop)
|
|
@@ -50,29 +92,91 @@ Add in the MCP settings panel:
|
|
|
50
92
|
- **Type**: `command`
|
|
51
93
|
- **Command**: `npx -y @datafrog-io/n2n-memory`
|
|
52
94
|
|
|
95
|
+
##### Other MCP clients
|
|
96
|
+
- The server uses stdio MCP and can be wired to any MCP client that supports local command execution.
|
|
97
|
+
- If your client supports it, set `N2N_LOG_LEVEL=debug` only when troubleshooting local path resolution.
|
|
98
|
+
|
|
53
99
|
#### 2. Usage Guide
|
|
54
100
|
|
|
55
101
|
This service is path-driven. AI assistants should pay attention to:
|
|
56
102
|
|
|
57
|
-
1. **Absolute
|
|
58
|
-
2. **
|
|
59
|
-
3. **
|
|
103
|
+
1. **Absolute Project Root**: When calling any `n2n_*` tool, provide the absolute path of the current project root or workspace top-level directory (`projectPath`).
|
|
104
|
+
2. **Initialization Handshake**: If `.mcp` does not exist yet, call the tool again with `confirmNewProjectRoot` set to the detected root returned by the server.
|
|
105
|
+
3. **Auto Storage**: Durable memory is saved to `[ProjectPath]/.mcp/memory.json`; active task context is saved to `[ProjectPath]/.mcp/context.json`.
|
|
106
|
+
4. **Collaboration**: Commit `.mcp/memory.json` when the knowledge graph should be shared with the team. Commit `context.json` only if your workflow wants active task state shared.
|
|
107
|
+
|
|
108
|
+
Recommended `.gitignore` policy for teams that want to share durable memory:
|
|
109
|
+
|
|
110
|
+
```gitignore
|
|
111
|
+
.mcp/context.json
|
|
112
|
+
!.mcp/
|
|
113
|
+
!.mcp/memory.json
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
If your project memory may contain private implementation details, keep the whole `.mcp/` directory ignored.
|
|
60
117
|
|
|
61
118
|
##### Available Tools:
|
|
62
119
|
- `n2n_add_entities`: Create new entities.
|
|
63
120
|
- `n2n_add_observations`: Append observations or facts.
|
|
64
|
-
- `n2n_create_relations`: Establish connections between entities.
|
|
121
|
+
- `n2n_create_relations`: Establish connections between existing entities.
|
|
65
122
|
- `n2n_read_graph`: Read project memory and active context (Supports `summaryMode` and `pagination`).
|
|
66
123
|
- `n2n_get_graph_summary`: Quickly fetch a lightweight index of all entities (Supports `pagination`).
|
|
67
124
|
- `n2n_update_context`: Update current task status and next steps.
|
|
68
|
-
- `n2n_search`: Search the graph via
|
|
125
|
+
- `n2n_search`: Search the graph via keyword or fuzzy matching (Supports `pagination`).
|
|
126
|
+
- `n2n_delete_entities`: Remove entities and their attached relations.
|
|
127
|
+
- `n2n_delete_observations`: Remove specific observations from entities.
|
|
128
|
+
- `n2n_delete_relations`: Remove specific relations.
|
|
69
129
|
- `n2n_open_nodes`: Retrieve specific entities by name.
|
|
130
|
+
- `n2n_export_markdown`: Export the graph to a Markdown file inside the project root.
|
|
131
|
+
|
|
132
|
+
##### Safety Notes:
|
|
133
|
+
- Existing but unreadable JSON files are treated as data integrity errors, not as empty memory.
|
|
134
|
+
- Export paths must be relative and must stay inside the project root.
|
|
135
|
+
- Relations that point to missing entities are rejected.
|
|
136
|
+
- Generic README-only folders are not treated as project roots; use a real project marker such as `.git`, `package.json`, or language-specific build files.
|
|
137
|
+
- Full local paths are hidden from server logs by default. Set `N2N_LOG_LEVEL=debug` when diagnosing path issues.
|
|
70
138
|
|
|
71
139
|
### 🗺️ Future Roadmap
|
|
72
140
|
- **Semantic Search**: Integration of minimalist Vector Embeddings for fuzzy memory retrieval.
|
|
73
141
|
- **Ontology Enforcement**: Optional schema for relation type consistency.
|
|
74
142
|
- **Time Travel**: Versioned snapshots for memory rollback.
|
|
75
143
|
|
|
144
|
+
## Security and governance notes
|
|
145
|
+
|
|
146
|
+
- `n2n_delete_entities`, `n2n_delete_observations`, and `n2n_delete_relations` are destructive and should be governed by review workflows.
|
|
147
|
+
- Keep `context.json` uncommitted by default, and commit `.mcp/memory.json` only when team sharing is intentional.
|
|
148
|
+
- See [SECURITY.md](./SECURITY.md) for vulnerability reporting.
|
|
149
|
+
|
|
150
|
+
### Search disambiguation note
|
|
151
|
+
|
|
152
|
+
`n2n-memory` is not a network VPN or mesh overlay project. It is an MCP memory server for AI coding agents.
|
|
153
|
+
|
|
154
|
+
## FAQ
|
|
155
|
+
|
|
156
|
+
### Is n2n-memory a vector database?
|
|
157
|
+
|
|
158
|
+
No. n2n-memory stores a deterministic JSON knowledge graph. Semantic search may be added later, but the core design is structured, Git-friendly project memory.
|
|
159
|
+
|
|
160
|
+
### Should I commit `.mcp/memory.json`?
|
|
161
|
+
|
|
162
|
+
Commit `.mcp/memory.json` when the knowledge graph is useful to your team. Keep it ignored if it may contain private implementation details. `context.json` is active task state and is often better left uncommitted.
|
|
163
|
+
|
|
164
|
+
### Does n2n-memory send data to the cloud?
|
|
165
|
+
|
|
166
|
+
No. n2n-memory is a local MCP server. It reads and writes files under your project directory.
|
|
167
|
+
|
|
168
|
+
### How is this different from global memory?
|
|
169
|
+
|
|
170
|
+
Global memory follows the assistant or user across contexts. n2n-memory follows the repository, so unrelated projects do not contaminate each other's memory.
|
|
171
|
+
|
|
172
|
+
### Does it work with Claude Desktop, Cursor, and VS Code?
|
|
173
|
+
|
|
174
|
+
Yes, when the client supports stdio MCP servers. The README includes Claude Desktop and Cursor / VS Code MCP configuration examples.
|
|
175
|
+
|
|
176
|
+
### Is this the same as a global AI memory store?
|
|
177
|
+
|
|
178
|
+
No. This is a repository-scoped memory server. Each project gets its own `.mcp` workspace.
|
|
179
|
+
|
|
76
180
|
---
|
|
77
181
|
|
|
78
182
|
## 📖 Related Docs
|
|
@@ -81,10 +185,11 @@ This service is path-driven. AI assistants should pay attention to:
|
|
|
81
185
|
- **[API Reference](./docs/API_REFERENCE.md)**: Tool descriptions and schema.
|
|
82
186
|
- **[Development](./docs/DEVELOPMENT.md)**: How to build, test and extend.
|
|
83
187
|
- **[Changelog](./CHANGELOG.md)**: Version history and incident recovery.
|
|
188
|
+
- **[llms.txt](./llms.txt)**: Short AI-readable project summary for coding agents and answer engines.
|
|
84
189
|
|
|
85
190
|
## 📄 License
|
|
86
191
|
This project is licensed under the [MIT License](./LICENSE).
|
|
87
192
|
|
|
88
193
|
---
|
|
89
194
|
|
|
90
|
-
|
|
195
|
+
Built by N2NS Lab, the open-source lab from Datafrog, focused on practical AI developer tools.
|
|
@@ -6,16 +6,16 @@ export const CONTEXT_FILE_PATH = ".mcp/context.json";
|
|
|
6
6
|
export class MemoryManager {
|
|
7
7
|
static async readGraph(projectPath) {
|
|
8
8
|
const filePath = path.resolve(projectPath, MEMORY_FILE_PATH);
|
|
9
|
+
if (!await fs.pathExists(filePath)) {
|
|
10
|
+
return { entities: [], relations: [] };
|
|
11
|
+
}
|
|
9
12
|
try {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return KnowledgeGraphSchema.parse(data);
|
|
13
|
-
}
|
|
13
|
+
const data = await fs.readJson(filePath);
|
|
14
|
+
return KnowledgeGraphSchema.parse(data);
|
|
14
15
|
}
|
|
15
16
|
catch (error) {
|
|
16
|
-
|
|
17
|
+
throw new Error(`Failed to read memory file at ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
17
18
|
}
|
|
18
|
-
return { entities: [], relations: [] };
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
21
|
* Writes the graph to disk atomically using a temporary file.
|
|
@@ -23,69 +23,34 @@ export class MemoryManager {
|
|
|
23
23
|
*/
|
|
24
24
|
static async writeGraph(projectPath, graph) {
|
|
25
25
|
const filePath = path.resolve(projectPath, MEMORY_FILE_PATH);
|
|
26
|
-
const
|
|
27
|
-
const dirPath = path.dirname(filePath);
|
|
26
|
+
const normalizedGraph = this.normalizeGraphForStorage(graph);
|
|
28
27
|
try {
|
|
29
|
-
|
|
30
|
-
graph.entities.sort((a, b) => a.name.localeCompare(b.name));
|
|
31
|
-
graph.entities.forEach(entity => {
|
|
32
|
-
if (entity.observations) {
|
|
33
|
-
entity.observations.sort();
|
|
34
|
-
}
|
|
35
|
-
else {
|
|
36
|
-
entity.observations = [];
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
graph.relations.sort((a, b) => {
|
|
40
|
-
const fromComp = a.from.localeCompare(b.from);
|
|
41
|
-
if (fromComp !== 0)
|
|
42
|
-
return fromComp;
|
|
43
|
-
const toComp = a.to.localeCompare(b.to);
|
|
44
|
-
if (toComp !== 0)
|
|
45
|
-
return toComp;
|
|
46
|
-
return a.relationType.localeCompare(b.relationType);
|
|
47
|
-
});
|
|
48
|
-
await fs.ensureDir(dirPath);
|
|
49
|
-
// 1. Write to temporary file
|
|
50
|
-
await fs.writeJson(tempPath, graph, { spaces: 2 });
|
|
51
|
-
// 2. Atomic rename
|
|
52
|
-
await fs.move(tempPath, filePath, { overwrite: true });
|
|
28
|
+
await this.atomicWriteJson(filePath, normalizedGraph);
|
|
53
29
|
}
|
|
54
30
|
catch (error) {
|
|
55
|
-
// Cleanup temp file if it exists and write failed
|
|
56
|
-
if (await fs.pathExists(tempPath)) {
|
|
57
|
-
await fs.remove(tempPath);
|
|
58
|
-
}
|
|
59
31
|
throw new Error(`Failed to write memory file at ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
60
32
|
}
|
|
61
33
|
}
|
|
62
34
|
static async readContext(projectPath) {
|
|
63
35
|
const filePath = path.resolve(projectPath, CONTEXT_FILE_PATH);
|
|
36
|
+
if (!await fs.pathExists(filePath)) {
|
|
37
|
+
return { status: "PLANNING", nextSteps: [] };
|
|
38
|
+
}
|
|
64
39
|
try {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return ProjectContextSchema.parse(data);
|
|
68
|
-
}
|
|
40
|
+
const data = await fs.readJson(filePath);
|
|
41
|
+
return ProjectContextSchema.parse(data);
|
|
69
42
|
}
|
|
70
43
|
catch (error) {
|
|
71
|
-
|
|
44
|
+
throw new Error(`Failed to read context file at ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
72
45
|
}
|
|
73
|
-
return { status: "PLANNING", nextSteps: [] };
|
|
74
46
|
}
|
|
75
47
|
static async writeContext(projectPath, context) {
|
|
76
48
|
const filePath = path.resolve(projectPath, CONTEXT_FILE_PATH);
|
|
77
|
-
const tempPath = `${filePath}.${Date.now()}.tmp`;
|
|
78
|
-
const dirPath = path.dirname(filePath);
|
|
79
49
|
try {
|
|
80
50
|
context.updatedAt = new Date().toISOString();
|
|
81
|
-
await
|
|
82
|
-
await fs.writeJson(tempPath, context, { spaces: 2 });
|
|
83
|
-
await fs.move(tempPath, filePath, { overwrite: true });
|
|
51
|
+
await this.atomicWriteJson(filePath, context);
|
|
84
52
|
}
|
|
85
53
|
catch (error) {
|
|
86
|
-
if (await fs.pathExists(tempPath)) {
|
|
87
|
-
await fs.remove(tempPath);
|
|
88
|
-
}
|
|
89
54
|
throw new Error(`Failed to write context file at ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
90
55
|
}
|
|
91
56
|
}
|
|
@@ -102,7 +67,7 @@ export class MemoryManager {
|
|
|
102
67
|
}
|
|
103
68
|
static async exportToMarkdown(projectPath, outputPath) {
|
|
104
69
|
const graph = await this.readGraph(projectPath);
|
|
105
|
-
const filePath =
|
|
70
|
+
const filePath = this.resolveProjectOutputPath(projectPath, outputPath || "KNOWLEDGE_GRAPH.md");
|
|
106
71
|
let md = "# Knowledge Graph\n\n";
|
|
107
72
|
md += `> Generated from \`${MEMORY_FILE_PATH}\`\n\n`;
|
|
108
73
|
md += "## Entities\n\n";
|
|
@@ -137,5 +102,55 @@ export class MemoryManager {
|
|
|
137
102
|
await fs.writeFile(filePath, md, "utf-8");
|
|
138
103
|
return filePath;
|
|
139
104
|
}
|
|
105
|
+
static normalizeGraphForStorage(graph) {
|
|
106
|
+
return {
|
|
107
|
+
entities: graph.entities
|
|
108
|
+
.map(entity => ({
|
|
109
|
+
...entity,
|
|
110
|
+
observations: [...(entity.observations || [])].sort()
|
|
111
|
+
}))
|
|
112
|
+
.sort((a, b) => a.name.localeCompare(b.name)),
|
|
113
|
+
relations: [...graph.relations].sort((a, b) => {
|
|
114
|
+
const fromComp = a.from.localeCompare(b.from);
|
|
115
|
+
if (fromComp !== 0)
|
|
116
|
+
return fromComp;
|
|
117
|
+
const toComp = a.to.localeCompare(b.to);
|
|
118
|
+
if (toComp !== 0)
|
|
119
|
+
return toComp;
|
|
120
|
+
return a.relationType.localeCompare(b.relationType);
|
|
121
|
+
})
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
static async atomicWriteJson(filePath, data) {
|
|
125
|
+
const dirPath = path.dirname(filePath);
|
|
126
|
+
const tempPath = this.createTempPath(filePath);
|
|
127
|
+
try {
|
|
128
|
+
await fs.ensureDir(dirPath);
|
|
129
|
+
await fs.writeJson(tempPath, data, { spaces: 2 });
|
|
130
|
+
await fs.move(tempPath, filePath, { overwrite: true });
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
if (await fs.pathExists(tempPath)) {
|
|
134
|
+
await fs.remove(tempPath);
|
|
135
|
+
}
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
static createTempPath(filePath) {
|
|
140
|
+
const suffix = `${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}`;
|
|
141
|
+
return `${filePath}.${suffix}.tmp`;
|
|
142
|
+
}
|
|
143
|
+
static resolveProjectOutputPath(projectPath, outputPath) {
|
|
144
|
+
if (path.isAbsolute(outputPath)) {
|
|
145
|
+
throw new Error("Export outputPath must be relative to the project root.");
|
|
146
|
+
}
|
|
147
|
+
const projectRoot = path.resolve(projectPath);
|
|
148
|
+
const filePath = path.resolve(projectRoot, outputPath);
|
|
149
|
+
const relative = path.relative(projectRoot, filePath);
|
|
150
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
151
|
+
throw new Error("Export outputPath must stay inside the project root.");
|
|
152
|
+
}
|
|
153
|
+
return filePath;
|
|
154
|
+
}
|
|
140
155
|
}
|
|
141
156
|
//# sourceMappingURL=memory-manager.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory-manager.js","sourceRoot":"","sources":["../../src/core/memory-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,UAAU,CAAC;AAC1B,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAEH,oBAAoB,EAEpB,oBAAoB,EACvB,MAAM,aAAa,CAAC;AAErB,MAAM,CAAC,MAAM,gBAAgB,GAAG,kBAAkB,CAAC;AACnD,MAAM,CAAC,MAAM,iBAAiB,GAAG,mBAAmB,CAAC;AAErD,MAAM,OAAO,aAAa;IACtB,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,WAAmB;QACtC,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,gBAAgB,CAAC,CAAC;QAC7D,IAAI,CAAC
|
|
1
|
+
{"version":3,"file":"memory-manager.js","sourceRoot":"","sources":["../../src/core/memory-manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,6 +4,7 @@ import fs from "fs-extra";
|
|
|
4
4
|
import path from "path";
|
|
5
5
|
import { deduplicateObservations, fuzzyMatchScore } from "../utils/similarity.js";
|
|
6
6
|
import { MemoryManager, MEMORY_FILE_PATH, CONTEXT_FILE_PATH } from "./memory-manager.js";
|
|
7
|
+
import { formatProjectPath, logError, logInfo } from "../utils/logging.js";
|
|
7
8
|
/**
|
|
8
9
|
* MemoryService - High reliability dual-buffer memory manager.
|
|
9
10
|
* - Snapshot Buffer: Instant in-memory reads for both Graph and Context.
|
|
@@ -38,7 +39,7 @@ export class MemoryService {
|
|
|
38
39
|
* Gracefully shuts down the service, releasing all active file locks.
|
|
39
40
|
*/
|
|
40
41
|
async shutdown() {
|
|
41
|
-
|
|
42
|
+
logInfo(`[MemoryService] Shutting down, releasing ${this.activeLocks.size} locks...`);
|
|
42
43
|
const lockReleases = Array.from(this.activeLocks.values());
|
|
43
44
|
await Promise.all(lockReleases.map(release => release().catch(() => { })));
|
|
44
45
|
this.activeLocks.clear();
|
|
@@ -77,61 +78,47 @@ export class MemoryService {
|
|
|
77
78
|
async loadSnapshot(projectPath) {
|
|
78
79
|
this.touchProject(projectPath);
|
|
79
80
|
const filePath = path.resolve(projectPath, MEMORY_FILE_PATH);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return this.snapshots.get(projectPath);
|
|
86
|
-
}
|
|
87
|
-
const graph = await MemoryManager.readGraph(projectPath);
|
|
88
|
-
this.snapshots.set(projectPath, graph);
|
|
89
|
-
this.lastMtimes.set(projectPath, currentMtime);
|
|
90
|
-
return graph;
|
|
81
|
+
if (await fs.pathExists(filePath)) {
|
|
82
|
+
const stats = await fs.stat(filePath);
|
|
83
|
+
const currentMtime = stats.mtimeMs;
|
|
84
|
+
if (this.snapshots.has(projectPath) && this.lastMtimes.get(projectPath) === currentMtime) {
|
|
85
|
+
return this.snapshots.get(projectPath);
|
|
91
86
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
87
|
+
const graph = await MemoryManager.readGraph(projectPath);
|
|
88
|
+
this.snapshots.set(projectPath, graph);
|
|
89
|
+
this.lastMtimes.set(projectPath, currentMtime);
|
|
90
|
+
return graph;
|
|
95
91
|
}
|
|
96
92
|
const emptyGraph = { entities: [], relations: [] };
|
|
97
93
|
this.snapshots.set(projectPath, emptyGraph);
|
|
94
|
+
this.lastMtimes.delete(projectPath);
|
|
98
95
|
return emptyGraph;
|
|
99
96
|
}
|
|
100
97
|
async getGraph(projectPath) {
|
|
101
|
-
|
|
102
|
-
return await this.loadSnapshot(projectPath);
|
|
103
|
-
}
|
|
104
|
-
return this.snapshots.get(projectPath);
|
|
98
|
+
return await this.loadSnapshot(projectPath);
|
|
105
99
|
}
|
|
106
100
|
// --- Context Operations ---
|
|
107
101
|
async loadContextSnapshot(projectPath) {
|
|
108
102
|
this.touchProject(projectPath);
|
|
109
103
|
const filePath = path.resolve(projectPath, CONTEXT_FILE_PATH);
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return this.contextSnapshots.get(projectPath);
|
|
116
|
-
}
|
|
117
|
-
const context = await MemoryManager.readContext(projectPath);
|
|
118
|
-
this.contextSnapshots.set(projectPath, context);
|
|
119
|
-
this.contextMtimes.set(projectPath, currentMtime);
|
|
120
|
-
return context;
|
|
104
|
+
if (await fs.pathExists(filePath)) {
|
|
105
|
+
const stats = await fs.stat(filePath);
|
|
106
|
+
const currentMtime = stats.mtimeMs;
|
|
107
|
+
if (this.contextSnapshots.has(projectPath) && this.contextMtimes.get(projectPath) === currentMtime) {
|
|
108
|
+
return this.contextSnapshots.get(projectPath);
|
|
121
109
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
110
|
+
const context = await MemoryManager.readContext(projectPath);
|
|
111
|
+
this.contextSnapshots.set(projectPath, context);
|
|
112
|
+
this.contextMtimes.set(projectPath, currentMtime);
|
|
113
|
+
return context;
|
|
125
114
|
}
|
|
126
115
|
const defaultContext = { status: "PLANNING", nextSteps: [] };
|
|
127
116
|
this.contextSnapshots.set(projectPath, defaultContext);
|
|
117
|
+
this.contextMtimes.delete(projectPath);
|
|
128
118
|
return defaultContext;
|
|
129
119
|
}
|
|
130
120
|
async getContext(projectPath) {
|
|
131
|
-
|
|
132
|
-
return await this.loadContextSnapshot(projectPath);
|
|
133
|
-
}
|
|
134
|
-
return this.contextSnapshots.get(projectPath);
|
|
121
|
+
return await this.loadContextSnapshot(projectPath);
|
|
135
122
|
}
|
|
136
123
|
/**
|
|
137
124
|
* Updates project context with independent locking.
|
|
@@ -254,7 +241,7 @@ export class MemoryService {
|
|
|
254
241
|
this.snapshots.set(projectPath, currentGraph);
|
|
255
242
|
}
|
|
256
243
|
catch (error) {
|
|
257
|
-
|
|
244
|
+
logError(`[MemoryService] Write operation failed for ${formatProjectPath(projectPath)}.`, error);
|
|
258
245
|
throw error;
|
|
259
246
|
}
|
|
260
247
|
finally {
|
|
@@ -304,6 +291,17 @@ export class MemoryService {
|
|
|
304
291
|
}
|
|
305
292
|
async createRelations(projectPath, relations) {
|
|
306
293
|
await this.executeWrite(projectPath, (graph) => {
|
|
294
|
+
const entityNames = new Set(graph.entities.map(entity => entity.name));
|
|
295
|
+
const missingNames = new Set();
|
|
296
|
+
for (const relation of relations) {
|
|
297
|
+
if (!entityNames.has(relation.from))
|
|
298
|
+
missingNames.add(relation.from);
|
|
299
|
+
if (!entityNames.has(relation.to))
|
|
300
|
+
missingNames.add(relation.to);
|
|
301
|
+
}
|
|
302
|
+
if (missingNames.size > 0) {
|
|
303
|
+
throw new Error(`Cannot create relations with missing entities: ${Array.from(missingNames).sort().join(", ")}`);
|
|
304
|
+
}
|
|
307
305
|
relations.forEach(newRel => {
|
|
308
306
|
const exists = graph.relations.some(r => r.from === newRel.from && r.to === newRel.to && r.relationType === newRel.relationType);
|
|
309
307
|
if (!exists) {
|