context-mcp-server 1.0.7 → 1.1.0

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.
@@ -25,33 +25,34 @@ function autoDigest(entries, project) {
25
25
  export const definition = {
26
26
  name: 'context',
27
27
  description:
28
- `Factual memory — record what happened, what was decided, what broke, what was built.\n` +
29
- `• "resume" — START HERE every conversation. Loads recent context, active discussions, and graph status for a project.\n` +
30
- `• "save" — Store a note, decision, bug, or code snippet. Auto-deduplicates.\n` +
31
- `• "get" — Load entries. Pass id/ids to fetch specific ones, or project/tags/limit for recent.\n` +
32
- `• "update" — Edit an existing entry by id (any field).\n` +
33
- `• "delete" — Remove one entry (id) or multiple at once (ids: [...]).\n` +
34
- `• "list_projects"— Show all projects and entry counts.`,
28
+ `Factual memory — decisions, bugs, notes, discoveries.\n` +
29
+ `• "resume" — call first every session. Returns recent entries, active plans, graph status.\n` +
30
+ `• "save" — store an entry. Auto-deduplicates by content similarity.\n` +
31
+ `• "get" — fetch by id/ids, or filter by project/tags/limit.\n` +
32
+ `• "update" — edit an entry by id.\n` +
33
+ `• "delete" — remove one entry (id) or several (ids: [...]).\n` +
34
+ `• "list_projects"— list all projects and entry counts.`,
35
35
  inputSchema: {
36
36
  type: 'object',
37
37
  properties: {
38
38
  action: { type: 'string', enum: ['resume', 'save', 'get', 'update', 'delete', 'list_projects'] },
39
39
  content: { type: 'string' },
40
- title: { type: 'string' },
40
+ title: { type: 'string', description: 'Up to 120 chars' },
41
+ why: { type: 'string', description: 'Why it mattered' },
42
+ outcome: { type: 'string', description: 'What the result was' },
41
43
  project: { type: 'string' },
42
44
  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'] },
45
+ type: { type: 'string', enum: ['note', 'compaction'] },
44
46
  status: { type: 'string', enum: ['active', 'archived'] },
45
47
  tags: { type: 'array', items: { type: 'string' } },
46
48
  source: { type: 'string', enum: ['user', 'ai-summary', 'file', 'web', 'cli', 'auto'] },
47
49
  files: { type: 'array', items: { type: 'object' } },
48
50
  codeRefs: { type: 'array', items: { type: 'object' } },
49
- relations: { type: 'array', items: { type: 'object' } },
50
51
  expiresAt: { type: 'string' },
51
52
  limit: { type: 'number' },
52
53
  includeArchived: { type: 'boolean' },
53
- id: { type: 'string', description: 'Single entry ID (get/update/delete)' },
54
- ids: { type: 'array', items: { type: 'string' }, description: 'Multiple entry IDs — fetch or delete several at once' },
54
+ id: { type: 'string', description: 'Single entry ID' },
55
+ ids: { type: 'array', items: { type: 'string' }, description: 'Multiple entry IDs' },
55
56
  },
56
57
  required: ['action'],
57
58
  },
@@ -79,18 +80,27 @@ export async function handle(args, state) {
79
80
  const proj = args.project || null;
80
81
  archiveExpired(proj);
81
82
 
82
- // Set project on state so autoLink works for subsequent saves
83
83
  if (proj) state.sessionProject = proj;
84
84
 
85
- // Store rootPath with project (first time only) and load it onto session state.
86
- // Auto-detect from git if neither provided nor previously stored.
87
85
  const storedRoot = proj ? getProjectRoot(proj) : null;
88
86
  const resolvedRoot = args.rootPath || storedRoot || detectGitRoot() || null;
89
87
  if (proj) ensureProject(proj, resolvedRoot || undefined);
90
88
  state.projectRootPath = resolvedRoot;
91
89
 
92
- const entries = getContext({ project: proj, limit: 15, compact: true })
90
+ const rawEntries = getContext({ project: proj, limit: 15, compact: false })
93
91
  .filter(e => e.status !== 'archived');
92
+ // Newest 5 get full content; older entries get a lightweight preview
93
+ const entries = rawEntries.map((e, i) => {
94
+ if (i < 5) return e;
95
+ return {
96
+ id: e.id, project: e.project, title: e.title, type: e.type,
97
+ status: e.status, tags: e.tags, source: e.source,
98
+ createdAt: e.createdAt, updatedAt: e.updatedAt,
99
+ ...(e.why ? { why: e.why } : {}),
100
+ ...(e.outcome ? { outcome: e.outcome } : {}),
101
+ preview: (e.content || '').slice(0, 200),
102
+ };
103
+ });
94
104
  const discussions = listDiscussions({ project: proj, status: 'active' });
95
105
  const allGraphs = listGraphs();
96
106
  const np = p => (p || '').toLowerCase().replace(/\\/g, '/');
@@ -104,7 +114,6 @@ export async function handle(args, state) {
104
114
  : null;
105
115
  const totalEntries = countContext(proj);
106
116
 
107
- // Auto-restore single active discussion
108
117
  if (discussions.length === 1) state.discussionId = discussions[0].id;
109
118
 
110
119
  const digest = totalEntries > 10
@@ -117,7 +126,7 @@ export async function handle(args, state) {
117
126
 
118
127
  return {
119
128
  recentEntries: entries,
120
- activeDiscussions: discussions,
129
+ activePlans: discussions,
121
130
  restoredDiscussion: discussions.length === 1 ? { id: discussions[0].id, name: discussions[0].name } : null,
122
131
  codegraph: graphStatus,
123
132
  digest: digest || undefined,
@@ -128,7 +137,7 @@ export async function handle(args, state) {
128
137
  ? `All file and git operations are sandboxed to: ${state.projectRootPath} — do not use paths outside this root.`
129
138
  : 'No project root configured — pass rootPath to restrict file/git access to a directory.',
130
139
  hint: graphStatus.built
131
- ? `Graph ready (${graphStatus.nodes} nodes). Use codegraph_query for structural questions.`
140
+ ? `Graph ready (${graphStatus.nodes} nodes). Use codegraph_arch for module map, codegraph_query for specific symbol lookups.`
132
141
  : 'No graph built yet. Call codegraph_build on the project root to enable graph queries.',
133
142
  };
134
143
  }
@@ -136,7 +145,6 @@ export async function handle(args, state) {
136
145
  case 'save': {
137
146
  if (!args.content) throw new Error('content is required for save');
138
147
  if (!args.project && state.sessionProject) args = { ...args, project: state.sessionProject };
139
- // Auto-detect and store project root if not yet configured
140
148
  if (args.project) {
141
149
  const existing = getProjectRoot(args.project);
142
150
  if (!existing) {
@@ -147,7 +155,6 @@ export async function handle(args, state) {
147
155
  }
148
156
  }
149
157
  }
150
- // Validate file paths in files[] and codeRefs[] stay within project root
151
158
  if (state.projectRootPath) {
152
159
  if (Array.isArray(args.files)) {
153
160
  args.files.forEach(f => { if (f.path) guardPath(f.path, state.projectRootPath); });
@@ -163,24 +170,33 @@ export async function handle(args, state) {
163
170
  id: dupe.id, content: args.content,
164
171
  title: args.title || dupe.title, tags: args.tags || dupe.tags,
165
172
  type: args.type || dupe.type, status: args.status || dupe.status,
173
+ why: args.why !== undefined ? args.why : dupe.why,
174
+ outcome: args.outcome !== undefined ? args.outcome : dupe.outcome,
166
175
  expiresAt: args.expiresAt !== undefined ? args.expiresAt : dupe.expiresAt,
167
176
  files: args.files || dupe.files, codeRefs: args.codeRefs || dupe.codeRefs,
168
- relations: args.relations || dupe.relations,
169
177
  });
170
178
  fireAutoLink(updated.id, state);
171
179
  return { success: true, id: updated.id, deduplicated: true,
172
180
  message: `Updated existing entry "${updated.title || updated.id}" (auto-dedup).` };
173
181
  }
174
- const entry = saveContext({ ...args });
182
+ const entry = saveContext({ ...args, rootPath: state.projectRootPath || undefined });
175
183
  fireAutoLink(entry.id, state);
176
184
 
177
- // Auto-compact when too many entries accumulate
185
+ // Auto-compact when too many entries accumulate.
186
+ // If the AI just saved a compaction entry, use that content as the summary
187
+ // instead of running TF-IDF on top of it.
178
188
  let compaction = null;
179
189
  if (shouldCompact(entry.project)) {
180
- const old = getContext({ project: entry.project, limit: 500 });
181
- const { summarizeEntries: summarize } = await import('../summarizer.js');
182
- const cs = summarize(old.slice(old.length - 30), { project: entry.project || 'global', sessionLabel: 'auto-compaction', topN: 5 });
183
- compaction = compactProject(entry.project, cs);
190
+ if (entry.type === 'compaction') {
191
+ // AI wrote a proper summary compact old entries without creating a duplicate summary
192
+ compaction = compactProject(entry.project, entry.content, { skipSummaryEntry: true });
193
+ } else {
194
+ // AI didn't write a summary — fall back to TF-IDF extractive summarization
195
+ const old = getContext({ project: entry.project, limit: 500 });
196
+ const { summarizeEntries: summarize } = await import('../summarizer.js');
197
+ const summaryContent = summarize(old.slice(old.length - 30), { project: entry.project || 'global', sessionLabel: 'auto-compaction', topN: 5 });
198
+ compaction = compactProject(entry.project, summaryContent);
199
+ }
184
200
  }
185
201
 
186
202
  return { success: true, id: entry.id, deduplicated: false,
@@ -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,133 @@
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
+ `Plan storage — saves to project store, optionally writes a .md file to planDir.\n` +
23
+ `• "save" — store a new plan or overwrite by name.\n` +
24
+ `• "update" — patch title/content/status.\n` +
25
+ `• "get" — retrieve a plan by name or id.\n` +
26
+ `• "list" — list all plans for the project.\n` +
27
+ `• "delete" — remove a plan by name or id.`,
28
+ inputSchema: {
29
+ type: 'object',
30
+ properties: {
31
+ action: { type: 'string', enum: ['save', 'get', 'list', 'update', 'delete'] },
32
+ name: { type: 'string', description: 'Short slug-style identifier for the plan, e.g. "auth-refactor"' },
33
+ id: { type: 'string' },
34
+ project: { type: 'string' },
35
+ title: { type: 'string', description: 'Plan title' },
36
+ content: { type: 'string', description: 'Full plan summary in markdown' },
37
+ status: { type: 'string', enum: ['active', 'done'] },
38
+ tags: { type: 'array', items: { type: 'string' } },
39
+ 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).' },
40
+ },
41
+ required: ['action'],
42
+ },
43
+ outputSchema: {
44
+ type: 'object',
45
+ properties: {
46
+ success: { type: 'boolean' },
47
+ id: { type: 'string' },
48
+ name: { type: 'string' },
49
+ filePath: { type: 'string' },
50
+ plan: { type: 'object' },
51
+ plans: { type: 'array' },
52
+ message: { type: 'string' },
53
+ },
54
+ },
55
+ };
56
+
57
+ export async function handle(args, state) {
58
+ if (!args.project && state.sessionProject) args = { ...args, project: state.sessionProject };
59
+
60
+ switch (args.action) {
61
+ case 'save': {
62
+ if (!args.name) throw new Error('name is required for save');
63
+ if (!args.content) throw new Error('content is required for save');
64
+ const plan = saveDiscussion({
65
+ name: args.name,
66
+ title: args.title || args.name,
67
+ content: args.content,
68
+ project: args.project,
69
+ tags: args.tags,
70
+ type: 'plan',
71
+ status: args.status || 'active',
72
+ sessionId: state.sessionId || null,
73
+ });
74
+ if (plan.status === 'active') state.discussionId = plan.id;
75
+ const filePath = writePlanFile(args.planDir, args.name, args.content, args.title);
76
+ return {
77
+ success: true, id: plan.id, name: plan.name,
78
+ filePath: filePath || undefined,
79
+ message: `Plan "${plan.name}" saved.${filePath ? ` Written to ${filePath}` : ''}`,
80
+ };
81
+ }
82
+
83
+ case 'update': {
84
+ if (!args.name && !args.id) throw new Error('name or id is required for update');
85
+ if (args.status === 'done') {
86
+ const result = deleteDiscussion({ name: args.name, id: args.id });
87
+ state.discussionId = null;
88
+ return { success: true, message: `Plan "${args.name || args.id}" completed and removed.` };
89
+ }
90
+ const updated = updateDiscussion({
91
+ name: args.name,
92
+ id: args.id,
93
+ title: args.title,
94
+ content: args.content,
95
+ status: args.status,
96
+ tags: args.tags,
97
+ });
98
+ if (!updated) throw new Error(`No plan found for "${args.name || args.id}".`);
99
+ if (updated.status === 'active') state.discussionId = updated.id;
100
+ const filePath = writePlanFile(args.planDir, updated.name, updated.content, updated.title);
101
+ return {
102
+ success: true, id: updated.id, name: updated.name,
103
+ filePath: filePath || undefined,
104
+ message: `Plan "${updated.name}" updated.`,
105
+ };
106
+ }
107
+
108
+ case 'get': {
109
+ if (!args.name && !args.id) throw new Error('name or id is required for get');
110
+ const plan = getDiscussion({ name: args.name, id: args.id, project: args.project });
111
+ return plan
112
+ ? { plan }
113
+ : { plan: null, message: `No plan found for "${args.name || args.id}".` };
114
+ }
115
+
116
+ case 'list': {
117
+ const plans = listDiscussions({ project: args.project, status: args.status });
118
+ return { plans };
119
+ }
120
+
121
+ case 'delete': {
122
+ if (!args.name && !args.id) throw new Error('name or id is required for delete');
123
+ const result = deleteDiscussion({ name: args.name, id: args.id });
124
+ if (state.discussionId && (args.id === state.discussionId || result.deleted > 0)) {
125
+ state.discussionId = null;
126
+ }
127
+ return { ...result, message: `Deleted ${result.deleted} plan(s).` };
128
+ }
129
+
130
+ default:
131
+ throw new Error(`Unknown plan action: ${args.action}`);
132
+ }
133
+ }
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.1.0"
172
172
  source = { editable = "." }
173
173
  dependencies = [
174
174
  { name = "mcp" },
@@ -1,123 +0,0 @@
1
- import {
2
- saveDiscussion, getDiscussion, listDiscussions, deleteDiscussion,
3
- updateDiscussion, updateDiscussionStep, clearDiscussionLink,
4
- } from '../db.js';
5
-
6
- export const definition = {
7
- name: 'discussion',
8
- description:
9
- `Forward-looking thinking space — plans, research, ideas, design.\n` +
10
- `• "save" — Create or update a discussion.\n` +
11
- `• "update" — Patch specific fields without touching the rest.\n` +
12
- `• "get" — Retrieve by name or id (full content + steps).\n` +
13
- `• "list" — List discussions (filterable). Header + stepsSummary only.\n` +
14
- `• "delete" — Remove by name or id.\n` +
15
- `• "update_step" — Mark a step done/in-progress. Auto-closes when all done.`,
16
- inputSchema: {
17
- type: 'object',
18
- properties: {
19
- action: { type: 'string', enum: ['save', 'get', 'list', 'delete', 'update', 'update_step'] },
20
- name: { type: 'string' },
21
- id: { type: 'string' },
22
- project: { type: 'string' },
23
- title: { type: 'string' },
24
- description: { type: 'string' },
25
- content: { type: 'string' },
26
- type: { type: 'string', enum: ['plan', 'research', 'idea', 'design', 'implementation', 'review', 'thread'] },
27
- status: { type: 'string', enum: ['active', 'done'] },
28
- tags: { type: 'array', items: { type: 'string' } },
29
- parentId: { type: 'string' },
30
- linkedContextIds: { type: 'array', items: { type: 'string' } },
31
- steps: { type: 'array', items: { type: 'object' } },
32
- stepId: { type: 'string' },
33
- stepStatus: { type: 'string', enum: ['pending', 'in-progress', 'done', 'skipped'] },
34
- linkedContextId: { type: 'string' },
35
- filterStatus: { type: 'string' },
36
- filterType: { type: 'string' },
37
- },
38
- required: ['action'],
39
- },
40
- outputSchema: {
41
- type: 'object',
42
- properties: {
43
- success: { type: 'boolean' },
44
- id: { type: 'string' },
45
- name: { type: 'string' },
46
- discussion: { type: 'object' },
47
- discussions: { type: 'array' },
48
- step: { type: 'object' },
49
- message: { type: 'string' },
50
- },
51
- },
52
- };
53
-
54
- export async function handle(args, state) {
55
- switch (args.action) {
56
- case 'save': {
57
- if (!args.name) throw new Error('name is required for save');
58
- const disc = saveDiscussion({ ...args, sessionId: args.sessionId || state.sessionId });
59
- if (disc.status === 'active') state.discussionId = disc.id;
60
- else if (state.discussionId === disc.id) state.discussionId = null;
61
- return { success: true, id: disc.id, name: disc.name,
62
- message: `Discussion "${disc.name}" saved (${disc.type}, ${disc.status}).` };
63
- }
64
-
65
- case 'get': {
66
- if (!args.name && !args.id) throw new Error('name or id is required for get');
67
- const disc = getDiscussion({ name: args.name, id: args.id, project: args.project });
68
- return disc
69
- ? { discussion: disc }
70
- : { discussion: null, message: `No discussion found for "${args.name || args.id}".` };
71
- }
72
-
73
- case 'update': {
74
- if (!args.name && !args.id) throw new Error('name or id is required for update');
75
- const updated = updateDiscussion({ ...args });
76
- if (!updated) throw new Error(`No discussion found for "${args.name || args.id}".`);
77
- if (updated.status !== 'active' && state.discussionId === updated.id) state.discussionId = null;
78
- if (updated.status === 'active') state.discussionId = updated.id;
79
- return { success: true, id: updated.id, name: updated.name, status: updated.status,
80
- message: `Discussion "${updated.name}" updated (${updated.status}).` };
81
- }
82
-
83
- case 'list': {
84
- return { discussions: listDiscussions({ project: args.project, status: args.filterStatus, type: args.filterType }) };
85
- }
86
-
87
- case 'delete': {
88
- if (!args.name && !args.id) throw new Error('name or id is required for delete');
89
- const toDelete = getDiscussion({ name: args.name, id: args.id });
90
- if (toDelete) {
91
- for (const ctxId of (toDelete.linkedContextIds || [])) clearDiscussionLink(ctxId);
92
- }
93
- const del = deleteDiscussion({ name: args.name, id: args.id });
94
- if (state.discussionId) {
95
- const matchById = args.id && args.id === state.discussionId;
96
- const matchByName = args.name && del.deleted > 0;
97
- if (matchById || matchByName) state.discussionId = null;
98
- }
99
- return { ...del, message: `Deleted ${del.deleted} discussion(s).` };
100
- }
101
-
102
- case 'update_step': {
103
- if (!args.name && !args.id) throw new Error('name or id is required for update_step');
104
- if (!args.stepId) throw new Error('stepId is required for update_step');
105
- const updated = updateDiscussionStep({
106
- discussionName: args.name,
107
- discussionId: args.id,
108
- stepId: args.stepId,
109
- status: args.stepStatus,
110
- linkedContextId: args.linkedContextId,
111
- });
112
- if (!updated) throw new Error('Discussion or step not found.');
113
- if (updated.discussion.status === 'done' && state.discussionId === updated.discussion.id) {
114
- state.discussionId = null;
115
- }
116
- return { success: true, step: updated.step, discussionStatus: updated.discussion.status,
117
- message: `Step updated. Discussion "${updated.discussion.name}" is now "${updated.discussion.status}".` };
118
- }
119
-
120
- default:
121
- throw new Error(`Unknown discussion action: ${args.action}`);
122
- }
123
- }