context-mcp-server 1.0.7 → 1.0.8
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/README.md +10 -11
- package/package.json +2 -2
- package/pyproject.toml +1 -1
- package/src/cli.js +64 -52
- package/src/db.js +946 -805
- package/src/guard.js +9 -3
- package/src/migrator.js +124 -0
- package/src/server.js +7 -6
- package/src/templates/AGENTS.md +6 -13
- package/src/templates/CLAUDE.md +6 -12
- package/src/templates/GEMINI.md +6 -13
- package/src/templates/commands/context-resume.md +1 -1
- package/src/templates/commands/save-context.md +1 -1
- package/src/templates/cursor-rules.mdc +3 -3
- package/src/templates/skills/SKILL.md +7 -13
- package/src/templates/windsurf-rules.md +3 -3
- package/src/tools/context.js +3 -5
- package/src/tools/gitTools.js +1 -3
- package/src/tools/plan.js +130 -0
- package/uv.lock +1 -1
- package/src/tools/discussion.js +0 -123
package/src/guard.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { resolve, sep } from 'node:path';
|
|
2
|
+
import { realpathSync } from 'node:fs';
|
|
3
|
+
|
|
4
|
+
// Follow symlinks to get the true path; fall back to resolve() if path doesn't exist yet.
|
|
5
|
+
function safeStat(p) {
|
|
6
|
+
try { return realpathSync(p); } catch { return resolve(p); }
|
|
7
|
+
}
|
|
2
8
|
|
|
3
9
|
/**
|
|
4
|
-
* Resolves `inputPath` and asserts it falls within `allowedRoot`.
|
|
10
|
+
* Resolves `inputPath` (following symlinks) and asserts it falls within `allowedRoot`.
|
|
5
11
|
* Throws if no root is configured or if the path escapes the root.
|
|
6
12
|
* Returns the resolved absolute path on success.
|
|
7
13
|
*/
|
|
@@ -11,8 +17,8 @@ export function guardPath(inputPath, allowedRoot) {
|
|
|
11
17
|
'Project root not configured. Call context with action:"resume" and include rootPath to enable file/git access.'
|
|
12
18
|
);
|
|
13
19
|
}
|
|
14
|
-
const resolved =
|
|
15
|
-
const root =
|
|
20
|
+
const resolved = safeStat(inputPath);
|
|
21
|
+
const root = safeStat(allowedRoot);
|
|
16
22
|
if (resolved !== root && !resolved.startsWith(root + sep)) {
|
|
17
23
|
throw new Error(`Access denied: "${resolved}" is outside the project root "${root}"`);
|
|
18
24
|
}
|
package/src/migrator.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* migrator.js — one-time migration from flat JSON files to per-project directory structure.
|
|
3
|
+
*
|
|
4
|
+
* Old layout:
|
|
5
|
+
* ~/.context-mcp/contexts.json all entries flat
|
|
6
|
+
* ~/.context-mcp/discussions.json all discussions flat
|
|
7
|
+
* ~/.context-mcp/graphs.json all graph build records flat
|
|
8
|
+
*
|
|
9
|
+
* New layout:
|
|
10
|
+
* ~/.context-mcp/projects/<slug>/context.json
|
|
11
|
+
* ~/.context-mcp/projects/<slug>/graph.json { build, entries[] }
|
|
12
|
+
* ~/.context-mcp/projects/<slug>/summary.json
|
|
13
|
+
* ~/.context-mcp/projects/<slug>/discussions.json
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { readFileSync, existsSync, mkdirSync, unlinkSync } from 'node:fs';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
|
|
19
|
+
function normPath(p) {
|
|
20
|
+
return p ? p.toLowerCase().replace(/\\/g, '/').replace(/\/$/, '') : '';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function treeFor(entry) {
|
|
24
|
+
if (entry.type === 'compaction') return 'summary';
|
|
25
|
+
return 'context';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readArr(filePath, key) {
|
|
29
|
+
if (!existsSync(filePath)) return [];
|
|
30
|
+
try {
|
|
31
|
+
const d = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
32
|
+
return Array.isArray(d[key]) ? d[key] : (Array.isArray(d) ? d : []);
|
|
33
|
+
} catch { return []; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Run migration if legacy flat files are present.
|
|
38
|
+
* @param {object} opts
|
|
39
|
+
* @param {string} opts.dataDir - base data dir (~/.context-mcp)
|
|
40
|
+
* @param {string} opts.projectsDir - projects sub-dir
|
|
41
|
+
* @param {string} opts.projectsPath - path to projects.json
|
|
42
|
+
* @param {Function} opts.slugify - name → filesystem slug
|
|
43
|
+
* @param {Function} opts.flushFile - (filePath, content) atomic write
|
|
44
|
+
* @param {Array} opts.projectsIndex - current projects.json array (mutated in place)
|
|
45
|
+
*/
|
|
46
|
+
export function runMigration({ dataDir, projectsDir, projectsPath, slugify, flushFile, projectsIndex }) {
|
|
47
|
+
const legacyContexts = join(dataDir, 'contexts.json');
|
|
48
|
+
const legacyDiscussions = join(dataDir, 'discussions.json');
|
|
49
|
+
const legacyGraphs = join(dataDir, 'graphs.json');
|
|
50
|
+
|
|
51
|
+
const hasLegacy = existsSync(legacyContexts)
|
|
52
|
+
|| existsSync(legacyDiscussions)
|
|
53
|
+
|| existsSync(legacyGraphs);
|
|
54
|
+
|
|
55
|
+
if (!hasLegacy) return false;
|
|
56
|
+
|
|
57
|
+
const oldContexts = readArr(legacyContexts, 'contexts');
|
|
58
|
+
const oldDiscussions = readArr(legacyDiscussions, 'discussions');
|
|
59
|
+
const oldGraphs = readArr(legacyGraphs, 'graphs');
|
|
60
|
+
|
|
61
|
+
// Remap legacy type names to current 4-type schema
|
|
62
|
+
const TYPE_MAP = { architecture: 'note', code: 'note', error: 'bug', summary: 'compaction' };
|
|
63
|
+
const remapType = entry => {
|
|
64
|
+
if (TYPE_MAP[entry.type]) entry.type = TYPE_MAP[entry.type];
|
|
65
|
+
return entry;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Group everything by project name
|
|
69
|
+
const byProject = {};
|
|
70
|
+
const ensure = name => {
|
|
71
|
+
if (!byProject[name]) byProject[name] = {
|
|
72
|
+
context: [], graph: { build: null }, summary: [], discussions: [],
|
|
73
|
+
};
|
|
74
|
+
return byProject[name];
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
for (const entry of oldContexts) {
|
|
78
|
+
remapType(entry);
|
|
79
|
+
const p = entry.project || 'global';
|
|
80
|
+
const d = ensure(p);
|
|
81
|
+
if (treeFor(entry) === 'summary') d.summary.push(entry);
|
|
82
|
+
else d.context.push(entry);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const disc of oldDiscussions) {
|
|
86
|
+
ensure(disc.project || 'global').discussions.push(disc);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Match graph build records to projects via rootPath
|
|
90
|
+
for (const graph of oldGraphs) {
|
|
91
|
+
const proj = projectsIndex.find(p => normPath(p.rootPath) === normPath(graph.path));
|
|
92
|
+
const name = proj ? proj.name : 'global';
|
|
93
|
+
const d = ensure(name);
|
|
94
|
+
d.graph.build = {
|
|
95
|
+
path: graph.path, nodes: graph.nodes, edges: graph.edges,
|
|
96
|
+
communities: graph.communities, cached: graph.cached || 0,
|
|
97
|
+
changed: graph.changed || 0, time_ms: graph.time_ms || 0,
|
|
98
|
+
summary: graph.summary || '', builtAt: graph.builtAt,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Write per-project files
|
|
103
|
+
for (const [name, data] of Object.entries(byProject)) {
|
|
104
|
+
const dir = join(projectsDir, slugify(name));
|
|
105
|
+
mkdirSync(dir, { recursive: true });
|
|
106
|
+
flushFile(join(dir, 'context.json'), { entries: data.context });
|
|
107
|
+
flushFile(join(dir, 'graph.json'), data.graph);
|
|
108
|
+
flushFile(join(dir, 'summary.json'), { entries: data.summary });
|
|
109
|
+
flushFile(join(dir, 'discussions.json'), { discussions: data.discussions });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Stamp dataDir onto each project record
|
|
113
|
+
for (const proj of projectsIndex) {
|
|
114
|
+
if (!proj.dataDir) proj.dataDir = `projects/${slugify(proj.name)}`;
|
|
115
|
+
}
|
|
116
|
+
flushFile(projectsPath, { projects: projectsIndex });
|
|
117
|
+
|
|
118
|
+
// Remove legacy flat files
|
|
119
|
+
try { unlinkSync(legacyContexts); } catch {}
|
|
120
|
+
try { unlinkSync(legacyDiscussions); } catch {}
|
|
121
|
+
try { unlinkSync(legacyGraphs); } catch {}
|
|
122
|
+
|
|
123
|
+
return true;
|
|
124
|
+
}
|
package/src/server.js
CHANGED
|
@@ -5,7 +5,7 @@ import { getConfig } from './config.js';
|
|
|
5
5
|
|
|
6
6
|
import * as contextTool from './tools/context.js';
|
|
7
7
|
import * as searchTool from './tools/search.js';
|
|
8
|
-
import * as
|
|
8
|
+
import * as planTool from './tools/plan.js';
|
|
9
9
|
import * as errorCheckTool from './tools/errorCheck.js';
|
|
10
10
|
import * as fileTool from './tools/fileTools.js';
|
|
11
11
|
import * as gitTool from './tools/gitTools.js';
|
|
@@ -17,8 +17,9 @@ const CODEGRAPH_TOOL_NAMES = codegraphTool.TOOL_NAMES;
|
|
|
17
17
|
|
|
18
18
|
export function createServer({ enableFileTools = false, enableGitTools = getConfig().access_git === true } = {}) {
|
|
19
19
|
const state = {
|
|
20
|
-
sessionProject:
|
|
21
|
-
discussionId:
|
|
20
|
+
sessionProject: null,
|
|
21
|
+
discussionId: null,
|
|
22
|
+
projectRootPath: null,
|
|
22
23
|
};
|
|
23
24
|
|
|
24
25
|
const server = new Server(
|
|
@@ -30,7 +31,7 @@ export function createServer({ enableFileTools = false, enableGitTools = getConf
|
|
|
30
31
|
const tools = [
|
|
31
32
|
contextTool.definition,
|
|
32
33
|
searchTool.definition,
|
|
33
|
-
|
|
34
|
+
planTool.definition,
|
|
34
35
|
errorCheckTool.definition,
|
|
35
36
|
];
|
|
36
37
|
if (enableFileTools) tools.push(...fileTool.definitions);
|
|
@@ -56,8 +57,8 @@ export function createServer({ enableFileTools = false, enableGitTools = getConf
|
|
|
56
57
|
result = await contextTool.handle(args, state);
|
|
57
58
|
} else if (name === searchTool.definition.name) {
|
|
58
59
|
result = await searchTool.handle(args, state);
|
|
59
|
-
} else if (name ===
|
|
60
|
-
result = await
|
|
60
|
+
} else if (name === planTool.definition.name) {
|
|
61
|
+
result = await planTool.handle(args, state);
|
|
61
62
|
} else if (name === errorCheckTool.definition.name) {
|
|
62
63
|
result = await errorCheckTool.handle(args, state);
|
|
63
64
|
} else if (FILE_TOOL_NAMES.has(name)) {
|
package/src/templates/AGENTS.md
CHANGED
|
@@ -11,7 +11,7 @@ Call the `context` MCP tool with `action: "resume"`, `project: "<project-name>"`
|
|
|
11
11
|
|
|
12
12
|
Returns:
|
|
13
13
|
- `recentEntries` — decisions, bugs, notes from previous conversations
|
|
14
|
-
- `
|
|
14
|
+
- `activePlans` — active AI-created plans for this project
|
|
15
15
|
- `codegraph` — `{ built: true/false, nodes, edges, communities }`
|
|
16
16
|
|
|
17
17
|
Then:
|
|
@@ -24,7 +24,7 @@ Then:
|
|
|
24
24
|
|
|
25
25
|
**After graph build or rebuild** — every time `codegraph_build` completes:
|
|
26
26
|
```
|
|
27
|
-
context.save type: "
|
|
27
|
+
context.save type: "note" title: "ContextGraph built — <project>"
|
|
28
28
|
content: "nodes: X | edges: Y | communities: Z"
|
|
29
29
|
```
|
|
30
30
|
|
|
@@ -35,18 +35,12 @@ content: "nodes: X | edges: Y | communities: Z"
|
|
|
35
35
|
| What happened | Type |
|
|
36
36
|
|--------------|------|
|
|
37
37
|
| Approach / library / pattern decided | `decision` |
|
|
38
|
-
| Bug found
|
|
39
|
-
|
|
|
40
|
-
|
|
|
41
|
-
| Config / env var / secret key discovered | `config` |
|
|
42
|
-
| External API or service integration learned | `note` |
|
|
43
|
-
| Performance insight (why something is slow/fast) | `note` |
|
|
44
|
-
| How to run tests / test pattern discovered | `note` |
|
|
45
|
-
| Deploy / release step discovered | `note` |
|
|
46
|
-
| Milestone / feature / task completed | `note` |
|
|
38
|
+
| Bug found, root cause known, or fixed | `bug` |
|
|
39
|
+
| Gotcha, constraint, discovery, structure understood | `note` |
|
|
40
|
+
| Config / env var / secret / deploy step | `config` |
|
|
47
41
|
|
|
48
42
|
Do NOT save: routine reads, search results, temporary debugging dead-ends.
|
|
49
|
-
|
|
43
|
+
**Making any kind of plan** → call `plan.save` immediately with the plan summary and `planDir` pointing to your platform's plans folder.
|
|
50
44
|
Need past info → `search` before asking. Always pass `project`.
|
|
51
45
|
|
|
52
46
|
---
|
|
@@ -63,7 +57,6 @@ codegraph_build(path) → AST graph: functions, classes, imports, edges
|
|
|
63
57
|
### Step 2 — Query (free, instant)
|
|
64
58
|
```
|
|
65
59
|
codegraph_query(path, question) → fetch any details about the codebase
|
|
66
|
-
codegraph_explain(path, node) → single node: type, file, connections
|
|
67
60
|
codegraph_path(path, from, to) → shortest path
|
|
68
61
|
codegraph_nodes(path, type) → list nodes by type
|
|
69
62
|
codegraph_report(path) → full graph analysis
|
package/src/templates/CLAUDE.md
CHANGED
|
@@ -24,7 +24,7 @@ Both fields are required: `project` names the memory bucket, `rootPath` enables
|
|
|
24
24
|
|
|
25
25
|
Returns:
|
|
26
26
|
- `recentEntries` — decisions, bugs, notes from previous conversations
|
|
27
|
-
- `
|
|
27
|
+
- `activePlans` — active AI-created plans for this project
|
|
28
28
|
- `codegraph` — `{ built: true/false, nodes, edges, communities }`
|
|
29
29
|
|
|
30
30
|
Then:
|
|
@@ -39,7 +39,7 @@ Then:
|
|
|
39
39
|
|
|
40
40
|
**After graph build or rebuild** — every time `codegraph_build` completes:
|
|
41
41
|
```
|
|
42
|
-
context.save project: "<project>" type: "
|
|
42
|
+
context.save project: "<project>" type: "note" title: "ContextGraph built — <project>"
|
|
43
43
|
content: "nodes: X | edges: Y | communities: Z"
|
|
44
44
|
```
|
|
45
45
|
|
|
@@ -50,19 +50,13 @@ content: "nodes: X | edges: Y | communities: Z"
|
|
|
50
50
|
| What happened | Type |
|
|
51
51
|
|--------------|------|
|
|
52
52
|
| Approach / library / pattern decided | `decision` |
|
|
53
|
-
| Bug found
|
|
54
|
-
|
|
|
55
|
-
|
|
|
56
|
-
| Config / env var / secret key discovered | `config` |
|
|
57
|
-
| External API or service integration learned | `note` |
|
|
58
|
-
| Performance insight (why something is slow/fast) | `note` |
|
|
59
|
-
| How to run tests / test pattern discovered | `note` |
|
|
60
|
-
| Deploy / release step discovered | `note` |
|
|
61
|
-
| Milestone / feature / task completed | `note` |
|
|
53
|
+
| Bug found, root cause known, or fixed | `bug` |
|
|
54
|
+
| Gotcha, constraint, discovery, structure understood | `note` |
|
|
55
|
+
| Config / env var / secret / deploy step | `config` |
|
|
62
56
|
|
|
63
57
|
Do NOT save: routine reads, search results, temporary debugging dead-ends.
|
|
64
58
|
|
|
65
|
-
|
|
59
|
+
**Making any kind of plan** → call `plan.save` immediately with the plan summary and `planDir` pointing to your platform's plans folder.
|
|
66
60
|
Need past info → `search` before asking user.
|
|
67
61
|
Always pass `project`. Auto-compact fires at >20 entries.
|
|
68
62
|
|
package/src/templates/GEMINI.md
CHANGED
|
@@ -11,7 +11,7 @@ Call the `context` MCP tool with `action: "resume"`, `project: "<project-name>"`
|
|
|
11
11
|
|
|
12
12
|
Returns:
|
|
13
13
|
- `recentEntries` — decisions, bugs, notes from previous conversations
|
|
14
|
-
- `
|
|
14
|
+
- `activePlans` — active AI-created plans for this project
|
|
15
15
|
- `codegraph` — `{ built: true/false, nodes, edges, communities }`
|
|
16
16
|
|
|
17
17
|
Then:
|
|
@@ -24,7 +24,7 @@ Then:
|
|
|
24
24
|
|
|
25
25
|
**After graph build or rebuild** — every time `codegraph_build` completes:
|
|
26
26
|
```
|
|
27
|
-
context.save type: "
|
|
27
|
+
context.save type: "note" title: "ContextGraph built — <project>"
|
|
28
28
|
content: "nodes: X | edges: Y | communities: Z"
|
|
29
29
|
```
|
|
30
30
|
|
|
@@ -35,18 +35,12 @@ content: "nodes: X | edges: Y | communities: Z"
|
|
|
35
35
|
| What happened | Type |
|
|
36
36
|
|--------------|------|
|
|
37
37
|
| Approach / library / pattern decided | `decision` |
|
|
38
|
-
| Bug found
|
|
39
|
-
|
|
|
40
|
-
|
|
|
41
|
-
| Config / env var / secret key discovered | `config` |
|
|
42
|
-
| External API or service integration learned | `note` |
|
|
43
|
-
| Performance insight (why something is slow/fast) | `note` |
|
|
44
|
-
| How to run tests / test pattern discovered | `note` |
|
|
45
|
-
| Deploy / release step discovered | `note` |
|
|
46
|
-
| Milestone / feature / task completed | `note` |
|
|
38
|
+
| Bug found, root cause known, or fixed | `bug` |
|
|
39
|
+
| Gotcha, constraint, discovery, structure understood | `note` |
|
|
40
|
+
| Config / env var / secret / deploy step | `config` |
|
|
47
41
|
|
|
48
42
|
Do NOT save: routine reads, search results, temporary debugging dead-ends.
|
|
49
|
-
|
|
43
|
+
**Making any kind of plan** → call `plan.save` immediately with the plan summary and `planDir` pointing to your platform's plans folder.
|
|
50
44
|
Need past info → `search` before asking. Always pass `project`.
|
|
51
45
|
|
|
52
46
|
---
|
|
@@ -63,7 +57,6 @@ codegraph_build(path) → AST graph: functions, classes, imports, edges
|
|
|
63
57
|
### Step 2 — Query (free, instant)
|
|
64
58
|
```
|
|
65
59
|
codegraph_query(path, question) → fetch any details about the codebase
|
|
66
|
-
codegraph_explain(path, node) → single node: type, file, connections
|
|
67
60
|
codegraph_path(path, from, to) → shortest path
|
|
68
61
|
codegraph_nodes(path, type) → list nodes by type
|
|
69
62
|
codegraph_report(path) → full graph analysis
|
|
@@ -4,7 +4,7 @@ Both `project` and `rootPath` are required: `project` names the memory bucket, `
|
|
|
4
4
|
|
|
5
5
|
This loads:
|
|
6
6
|
- Recent decisions, bugs, and notes from past sessions
|
|
7
|
-
- Active
|
|
7
|
+
- Active plans
|
|
8
8
|
- ContextGraph status (built or not)
|
|
9
9
|
|
|
10
10
|
If `codegraph.built` is false in the response, immediately call `codegraph_build` on the project path before proceeding.
|
|
@@ -4,6 +4,6 @@ Parse `$ARGUMENTS` to determine:
|
|
|
4
4
|
- `project`: infer from current working directory name if not specified
|
|
5
5
|
- `title`: first sentence or phrase from the argument
|
|
6
6
|
- `content`: full argument text
|
|
7
|
-
- `type`: auto-detect — if mentions a bug/fix → `"bug"`, decision/chose/decided → `"decision"`,
|
|
7
|
+
- `type`: auto-detect — if mentions a bug/fix → `"bug"`, decision/chose/decided → `"decision"`, config/env/secret/deploy → `"config"`, otherwise `"note"`
|
|
8
8
|
|
|
9
9
|
Confirm to the user what was saved (title, type, project).
|
|
@@ -13,7 +13,7 @@ Every conversation starts with `context.resume`. Every structural question uses
|
|
|
13
13
|
|
|
14
14
|
Call `context` tool, `action: "resume"`, `project: "<project-name>"` before anything else.
|
|
15
15
|
|
|
16
|
-
Returns: `recentEntries`, `
|
|
16
|
+
Returns: `recentEntries`, `activePlans`, `codegraph { built, nodes, edges }`.
|
|
17
17
|
|
|
18
18
|
- `codegraph.built: true` → use `codegraph_query` before reading files
|
|
19
19
|
- `codegraph.built: false` → run `codegraph_build(path)` first
|
|
@@ -24,8 +24,8 @@ Returns: `recentEntries`, `activeDiscussions`, `codegraph { built, nodes, edges
|
|
|
24
24
|
|-----------|--------|
|
|
25
25
|
| Decision made | `context.save` type: `"decision"` |
|
|
26
26
|
| Bug found/fixed | `context.save` type: `"bug"` |
|
|
27
|
-
|
|
|
28
|
-
|
|
|
27
|
+
| Discovery / structure understood | `context.save` type: `"note"` |
|
|
28
|
+
| Making any plan | `plan.save` + `planDir` |
|
|
29
29
|
|
|
30
30
|
## 3. CodeGraph
|
|
31
31
|
|
|
@@ -27,7 +27,7 @@ Both fields are required: `project` names the memory bucket, `rootPath` enables
|
|
|
27
27
|
|
|
28
28
|
Returns:
|
|
29
29
|
- `recentEntries` — decisions, bugs, notes from past sessions
|
|
30
|
-
- `
|
|
30
|
+
- `activePlans` — active AI-created plans for this project
|
|
31
31
|
- `codegraph` — `{ built: true/false, nodes, edges, communities }`
|
|
32
32
|
|
|
33
33
|
Then:
|
|
@@ -43,7 +43,7 @@ Then:
|
|
|
43
43
|
**1. After graph build or rebuild**
|
|
44
44
|
Every time `codegraph_build` completes successfully, immediately call:
|
|
45
45
|
```
|
|
46
|
-
context.save project: "<project>" type: "
|
|
46
|
+
context.save project: "<project>" type: "note" title: "ContextGraph built — <project>"
|
|
47
47
|
content: "nodes: X | edges: Y | communities: Z | built: <timestamp>"
|
|
48
48
|
```
|
|
49
49
|
|
|
@@ -63,17 +63,11 @@ Do NOT save for: routine file reads, search results, explanations of existing co
|
|
|
63
63
|
| What happened | Type |
|
|
64
64
|
|--------------|------|
|
|
65
65
|
| Approach / library / pattern decided | `decision` |
|
|
66
|
-
| Bug found
|
|
67
|
-
|
|
|
68
|
-
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
| Performance insight (why something is slow/fast) | `note` |
|
|
72
|
-
| How to run tests / test pattern discovered | `note` |
|
|
73
|
-
| Deploy / release step discovered | `note` |
|
|
74
|
-
| Milestone / feature / task completed | `note` |
|
|
75
|
-
|
|
76
|
-
Always pass `project`. Feature spans multiple sessions → `discussion.create` or `discussion.update`. Need past info → `search` before asking user. Auto-compact fires at >20 entries.
|
|
66
|
+
| Bug found, root cause known, or fixed | `bug` |
|
|
67
|
+
| Gotcha, constraint, discovery, structure understood | `note` |
|
|
68
|
+
| Config / env var / secret / deploy step | `config` |
|
|
69
|
+
|
|
70
|
+
Always pass `project`. **Making any plan** → `plan.save` immediately (planDir = your platform's plans folder). Need past info → `search` before asking user. Auto-compact fires at >20 entries.
|
|
77
71
|
|
|
78
72
|
---
|
|
79
73
|
|
|
@@ -7,7 +7,7 @@ Every conversation starts with `context.resume`. Every structural question uses
|
|
|
7
7
|
|
|
8
8
|
Call `context` tool, `action: "resume"`, `project: "<project-name>"` before anything else.
|
|
9
9
|
|
|
10
|
-
Returns: `recentEntries`, `
|
|
10
|
+
Returns: `recentEntries`, `activePlans`, `codegraph { built, nodes, edges }`.
|
|
11
11
|
|
|
12
12
|
- `codegraph.built: true` → use `codegraph_query` before reading files
|
|
13
13
|
- `codegraph.built: false` → run `codegraph_build(path)` first
|
|
@@ -18,8 +18,8 @@ Returns: `recentEntries`, `activeDiscussions`, `codegraph { built, nodes, edges
|
|
|
18
18
|
|-----------|--------|
|
|
19
19
|
| Decision made | `context.save` type: `"decision"` |
|
|
20
20
|
| Bug found/fixed | `context.save` type: `"bug"` |
|
|
21
|
-
|
|
|
22
|
-
|
|
|
21
|
+
| Discovery / structure understood | `context.save` type: `"note"` |
|
|
22
|
+
| Making any plan | `plan.save` + `planDir` |
|
|
23
23
|
|
|
24
24
|
## 3. CodeGraph
|
|
25
25
|
|
package/src/tools/context.js
CHANGED
|
@@ -40,13 +40,12 @@ export const definition = {
|
|
|
40
40
|
title: { type: 'string' },
|
|
41
41
|
project: { type: 'string' },
|
|
42
42
|
rootPath: { type: 'string', description: 'Absolute path to the project root directory. Stored on first call and used to sandbox file/git tool access.' },
|
|
43
|
-
type: { type: 'string', enum: ['decision', '
|
|
43
|
+
type: { type: 'string', enum: ['decision', 'bug', 'note', 'config'] },
|
|
44
44
|
status: { type: 'string', enum: ['active', 'archived'] },
|
|
45
45
|
tags: { type: 'array', items: { type: 'string' } },
|
|
46
46
|
source: { type: 'string', enum: ['user', 'ai-summary', 'file', 'web', 'cli', 'auto'] },
|
|
47
47
|
files: { type: 'array', items: { type: 'object' } },
|
|
48
48
|
codeRefs: { type: 'array', items: { type: 'object' } },
|
|
49
|
-
relations: { type: 'array', items: { type: 'object' } },
|
|
50
49
|
expiresAt: { type: 'string' },
|
|
51
50
|
limit: { type: 'number' },
|
|
52
51
|
includeArchived: { type: 'boolean' },
|
|
@@ -117,7 +116,7 @@ export async function handle(args, state) {
|
|
|
117
116
|
|
|
118
117
|
return {
|
|
119
118
|
recentEntries: entries,
|
|
120
|
-
|
|
119
|
+
activePlans: discussions,
|
|
121
120
|
restoredDiscussion: discussions.length === 1 ? { id: discussions[0].id, name: discussions[0].name } : null,
|
|
122
121
|
codegraph: graphStatus,
|
|
123
122
|
digest: digest || undefined,
|
|
@@ -165,13 +164,12 @@ export async function handle(args, state) {
|
|
|
165
164
|
type: args.type || dupe.type, status: args.status || dupe.status,
|
|
166
165
|
expiresAt: args.expiresAt !== undefined ? args.expiresAt : dupe.expiresAt,
|
|
167
166
|
files: args.files || dupe.files, codeRefs: args.codeRefs || dupe.codeRefs,
|
|
168
|
-
relations: args.relations || dupe.relations,
|
|
169
167
|
});
|
|
170
168
|
fireAutoLink(updated.id, state);
|
|
171
169
|
return { success: true, id: updated.id, deduplicated: true,
|
|
172
170
|
message: `Updated existing entry "${updated.title || updated.id}" (auto-dedup).` };
|
|
173
171
|
}
|
|
174
|
-
const entry = saveContext({ ...args });
|
|
172
|
+
const entry = saveContext({ ...args, rootPath: state.projectRootPath || undefined });
|
|
175
173
|
fireAutoLink(entry.id, state);
|
|
176
174
|
|
|
177
175
|
// Auto-compact when too many entries accumulate
|
package/src/tools/gitTools.js
CHANGED
|
@@ -26,10 +26,8 @@ function autoDetectRoot(fromDir) {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
function resolveCwd(args, state) {
|
|
29
|
-
// Auto-detect project root on first use if not already configured.
|
|
30
29
|
if (!state.projectRootPath) {
|
|
31
|
-
|
|
32
|
-
state.projectRootPath = detected || process.cwd();
|
|
30
|
+
throw new Error('No project root configured. Call context.resume with rootPath before using git tools.');
|
|
33
31
|
}
|
|
34
32
|
const raw = args.cwd ? pathResolve(args.cwd) : state.projectRootPath;
|
|
35
33
|
return guardPath(raw, state.projectRootPath);
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
saveDiscussion, getDiscussion, listDiscussions,
|
|
5
|
+
deleteDiscussion, updateDiscussion,
|
|
6
|
+
} from '../db.js';
|
|
7
|
+
|
|
8
|
+
function writePlanFile(planDir, name, content, title) {
|
|
9
|
+
if (!planDir) return null;
|
|
10
|
+
const dir = resolve(planDir);
|
|
11
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
12
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
13
|
+
const filePath = join(dir, `${slug}.md`);
|
|
14
|
+
const md = `# ${title || name}\n\n${content || ''}\n`;
|
|
15
|
+
writeFileSync(filePath, md, 'utf8');
|
|
16
|
+
return filePath;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const definition = {
|
|
20
|
+
name: 'plan',
|
|
21
|
+
description:
|
|
22
|
+
`AI plan storage — auto-call this tool whenever you create any kind of plan.\n` +
|
|
23
|
+
`Saves a summary of the plan to the project store and writes a .md file to planDir.\n` +
|
|
24
|
+
`• "save" — Store a new plan or overwrite an existing one by name. Pass planDir to write a file.\n` +
|
|
25
|
+
`• "update" — Patch title/content/status on an existing plan.\n` +
|
|
26
|
+
`• "get" — Retrieve a plan by name or id (full content).\n` +
|
|
27
|
+
`• "list" — List all plans for the project.\n` +
|
|
28
|
+
`• "delete" — Remove a plan by name or id.`,
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: 'object',
|
|
31
|
+
properties: {
|
|
32
|
+
action: { type: 'string', enum: ['save', 'get', 'list', 'update', 'delete'] },
|
|
33
|
+
name: { type: 'string', description: 'Short slug-style identifier for the plan, e.g. "auth-refactor"' },
|
|
34
|
+
id: { type: 'string' },
|
|
35
|
+
project: { type: 'string' },
|
|
36
|
+
title: { type: 'string', description: 'Human-readable plan title' },
|
|
37
|
+
content: { type: 'string', description: 'Full plan summary in markdown' },
|
|
38
|
+
status: { type: 'string', enum: ['active', 'done'] },
|
|
39
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
40
|
+
planDir: { type: 'string', description: 'Absolute path to the folder where .md plan files are written. Pass the path for your AI platform (e.g. ~/.claude/plans/ for Claude Code).' },
|
|
41
|
+
},
|
|
42
|
+
required: ['action'],
|
|
43
|
+
},
|
|
44
|
+
outputSchema: {
|
|
45
|
+
type: 'object',
|
|
46
|
+
properties: {
|
|
47
|
+
success: { type: 'boolean' },
|
|
48
|
+
id: { type: 'string' },
|
|
49
|
+
name: { type: 'string' },
|
|
50
|
+
filePath: { type: 'string' },
|
|
51
|
+
plan: { type: 'object' },
|
|
52
|
+
plans: { type: 'array' },
|
|
53
|
+
message: { type: 'string' },
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export async function handle(args, state) {
|
|
59
|
+
if (!args.project && state.sessionProject) args = { ...args, project: state.sessionProject };
|
|
60
|
+
|
|
61
|
+
switch (args.action) {
|
|
62
|
+
case 'save': {
|
|
63
|
+
if (!args.name) throw new Error('name is required for save');
|
|
64
|
+
if (!args.content) throw new Error('content is required for save');
|
|
65
|
+
const plan = saveDiscussion({
|
|
66
|
+
name: args.name,
|
|
67
|
+
title: args.title || args.name,
|
|
68
|
+
content: args.content,
|
|
69
|
+
project: args.project,
|
|
70
|
+
tags: args.tags,
|
|
71
|
+
type: 'plan',
|
|
72
|
+
status: args.status || 'active',
|
|
73
|
+
sessionId: state.sessionId || null,
|
|
74
|
+
});
|
|
75
|
+
if (plan.status === 'active') state.discussionId = plan.id;
|
|
76
|
+
const filePath = writePlanFile(args.planDir, args.name, args.content, args.title);
|
|
77
|
+
return {
|
|
78
|
+
success: true, id: plan.id, name: plan.name,
|
|
79
|
+
filePath: filePath || undefined,
|
|
80
|
+
message: `Plan "${plan.name}" saved.${filePath ? ` Written to ${filePath}` : ''}`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
case 'update': {
|
|
85
|
+
if (!args.name && !args.id) throw new Error('name or id is required for update');
|
|
86
|
+
const updated = updateDiscussion({
|
|
87
|
+
name: args.name,
|
|
88
|
+
id: args.id,
|
|
89
|
+
title: args.title,
|
|
90
|
+
content: args.content,
|
|
91
|
+
status: args.status,
|
|
92
|
+
tags: args.tags,
|
|
93
|
+
});
|
|
94
|
+
if (!updated) throw new Error(`No plan found for "${args.name || args.id}".`);
|
|
95
|
+
if (updated.status !== 'active' && state.discussionId === updated.id) state.discussionId = null;
|
|
96
|
+
if (updated.status === 'active') state.discussionId = updated.id;
|
|
97
|
+
const filePath = writePlanFile(args.planDir, updated.name, updated.content, updated.title);
|
|
98
|
+
return {
|
|
99
|
+
success: true, id: updated.id, name: updated.name, status: updated.status,
|
|
100
|
+
filePath: filePath || undefined,
|
|
101
|
+
message: `Plan "${updated.name}" updated (${updated.status}).`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
case 'get': {
|
|
106
|
+
if (!args.name && !args.id) throw new Error('name or id is required for get');
|
|
107
|
+
const plan = getDiscussion({ name: args.name, id: args.id, project: args.project });
|
|
108
|
+
return plan
|
|
109
|
+
? { plan }
|
|
110
|
+
: { plan: null, message: `No plan found for "${args.name || args.id}".` };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
case 'list': {
|
|
114
|
+
const plans = listDiscussions({ project: args.project, status: args.status });
|
|
115
|
+
return { plans };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
case 'delete': {
|
|
119
|
+
if (!args.name && !args.id) throw new Error('name or id is required for delete');
|
|
120
|
+
const result = deleteDiscussion({ name: args.name, id: args.id });
|
|
121
|
+
if (state.discussionId && (args.id === state.discussionId || result.deleted > 0)) {
|
|
122
|
+
state.discussionId = null;
|
|
123
|
+
}
|
|
124
|
+
return { ...result, message: `Deleted ${result.deleted} plan(s).` };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
default:
|
|
128
|
+
throw new Error(`Unknown plan action: ${args.action}`);
|
|
129
|
+
}
|
|
130
|
+
}
|