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/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 = resolve(inputPath);
15
- const root = resolve(allowedRoot);
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
  }
@@ -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 discussionTool from './tools/discussion.js';
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: null,
21
- discussionId: null,
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
- discussionTool.definition,
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 === discussionTool.definition.name) {
60
- result = await discussionTool.handle(args, state);
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)) {
@@ -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
- - `activeDiscussions` — ongoing topics
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: "architecture" title: "ContextGraph built — <project>"
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 (root cause known) or fixed | `bug` |
39
- | System structure understood | `architecture` |
40
- | Gotcha, constraint, non-obvious behavior | `note` |
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
- Feature spans sessions → `discussion.save` status: `"active"`.
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
@@ -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
- - `activeDiscussions` — ongoing topics (auto-linked if exactly one active)
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: "architecture" title: "ContextGraph built — <project>"
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 (root cause known) or fixed | `bug` |
54
- | System structure understood | `architecture` |
55
- | Gotcha, constraint, non-obvious behavior | `note` |
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
- Feature spans multiple sessions → `discussion.create` or `discussion.update`.
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
 
@@ -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
- - `activeDiscussions` — ongoing topics (auto-linked if exactly one active)
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: "architecture" title: "ContextGraph built — <project>"
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 (root cause known) or fixed | `bug` |
39
- | System structure understood | `architecture` |
40
- | Gotcha, constraint, non-obvious behavior | `note` |
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
- Feature spans sessions → `discussion.save` then `discussion.update`.
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 discussions
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"`, structure/architecture → `"architecture"`, otherwise `"note"`
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`, `activeDiscussions`, `codegraph { built, nodes, edges }`.
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
- | Architecture understood | `context.save` type: `"architecture"` |
28
- | Multi-session feature | `discussion.create` |
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
- - `activeDiscussions` — ongoing topics (auto-linked if exactly one active)
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: "architecture" title: "ContextGraph built — <project>"
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 (root cause known) or fixed | `bug` |
67
- | System structure understood | `architecture` |
68
- | Gotcha, constraint, non-obvious behavior | `note` |
69
- | Config / env var / secret key discovered | `config` |
70
- | External API or service integration learned | `note` |
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`, `activeDiscussions`, `codegraph { built, nodes, edges }`.
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
- | Architecture understood | `context.save` type: `"architecture"` |
22
- | Multi-session feature | `discussion.create` |
21
+ | Discovery / structure understood | `context.save` type: `"note"` |
22
+ | Making any plan | `plan.save` + `planDir` |
23
23
 
24
24
  ## 3. CodeGraph
25
25
 
@@ -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', 'note', 'code', 'bug', 'architecture', 'config', 'summary', 'error'] },
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
- activeDiscussions: discussions,
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
@@ -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
- const detected = autoDetectRoot(args.cwd ? pathResolve(args.cwd) : process.cwd());
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
+ }
package/uv.lock CHANGED
@@ -168,7 +168,7 @@ wheels = [
168
168
 
169
169
  [[package]]
170
170
  name = "codegraph-mcp"
171
- version = "1.0.6"
171
+ version = "1.0.8"
172
172
  source = { editable = "." }
173
173
  dependencies = [
174
174
  { name = "mcp" },