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.
- package/README.md +10 -11
- package/codegraph/__pycache__/server.cpython-313.pyc +0 -0
- package/codegraph/graph/__pycache__/query.cpython-313.pyc +0 -0
- package/codegraph/graph/query.py +43 -0
- package/codegraph/server.py +20 -17
- package/package.json +2 -2
- package/pyproject.toml +1 -1
- package/src/cli.js +152 -229
- package/src/db.js +923 -805
- package/src/guard.js +9 -3
- package/src/search.js +73 -9
- package/src/server.js +7 -6
- package/src/templates/AGENTS.md +56 -53
- package/src/templates/CLAUDE.md +89 -61
- package/src/templates/GEMINI.md +56 -53
- package/src/templates/commands/context-resume.md +1 -1
- package/src/templates/commands/save-context.md +6 -3
- package/src/templates/cursor-rules.mdc +3 -3
- package/src/templates/skills/SKILL.md +87 -60
- package/src/templates/windsurf-rules.md +69 -20
- package/src/tools/codegraph.js +46 -43
- package/src/tools/context.js +44 -28
- package/src/tools/gitTools.js +1 -3
- package/src/tools/plan.js +133 -0
- package/uv.lock +1 -1
- package/src/tools/discussion.js +0 -123
package/src/tools/context.js
CHANGED
|
@@ -25,33 +25,34 @@ function autoDigest(entries, project) {
|
|
|
25
25
|
export const definition = {
|
|
26
26
|
name: 'context',
|
|
27
27
|
description:
|
|
28
|
-
`Factual memory —
|
|
29
|
-
`• "resume" —
|
|
30
|
-
`• "save" —
|
|
31
|
-
`• "get" —
|
|
32
|
-
`• "update" —
|
|
33
|
-
`• "delete" —
|
|
34
|
-
`• "list_projects"—
|
|
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: ['
|
|
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
|
|
54
|
-
ids: { type: 'array', items: { type: 'string' }, description: 'Multiple entry IDs
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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,
|
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,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
package/src/tools/discussion.js
DELETED
|
@@ -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
|
-
}
|