decisionnode 0.2.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/LICENSE +21 -0
- package/README.md +80 -0
- package/dist/ai/gemini.d.ts +15 -0
- package/dist/ai/gemini.js +56 -0
- package/dist/ai/rag.d.ts +79 -0
- package/dist/ai/rag.js +268 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1724 -0
- package/dist/cloud.d.ts +177 -0
- package/dist/cloud.js +631 -0
- package/dist/env.d.ts +47 -0
- package/dist/env.js +139 -0
- package/dist/history.d.ts +34 -0
- package/dist/history.js +159 -0
- package/dist/maintenance.d.ts +7 -0
- package/dist/maintenance.js +49 -0
- package/dist/marketplace.d.ts +46 -0
- package/dist/marketplace.js +300 -0
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.js +621 -0
- package/dist/setup.d.ts +6 -0
- package/dist/setup.js +132 -0
- package/dist/store.d.ts +126 -0
- package/dist/store.js +555 -0
- package/dist/types.d.ts +60 -0
- package/dist/types.js +9 -0
- package/package.json +57 -0
|
@@ -0,0 +1,621 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// MUST interact with environment before other imports - this import does side-effects (loading .env)
|
|
3
|
+
import { getSearchSensitivity } from '../env.js';
|
|
4
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
5
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
6
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
7
|
+
import { findRelevantDecisions, findPotentialConflicts } from '../ai/rag.js';
|
|
8
|
+
import { isEmbeddingAvailable } from '../ai/gemini.js';
|
|
9
|
+
import { getHistory } from '../history.js';
|
|
10
|
+
import { addDecision, updateDecision, deleteDecision, listDecisions, getDecisionById, getNextDecisionId, listProjects, listGlobalDecisions, addGlobalDecision, getNextGlobalDecisionId, getGlobalDecisionById, updateGlobalDecision, deleteGlobalDecision } from '../store.js';
|
|
11
|
+
import { getProjectRoot, getCurrentProject, setCurrentProject, isGlobalId } from '../env.js';
|
|
12
|
+
// Create MCP server
|
|
13
|
+
const server = new Server({
|
|
14
|
+
name: 'decisionnode',
|
|
15
|
+
version: '0.3.0',
|
|
16
|
+
}, {
|
|
17
|
+
capabilities: {
|
|
18
|
+
tools: {},
|
|
19
|
+
resources: {},
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
/**
|
|
23
|
+
* Helper to ensure project is set before operations
|
|
24
|
+
* Validates that project name is a simple folder basename (no slashes, colons, etc.)
|
|
25
|
+
*/
|
|
26
|
+
function ensureProject(args) {
|
|
27
|
+
if (args && typeof args === 'object' && 'project' in args && args.project) {
|
|
28
|
+
const projectName = args.project;
|
|
29
|
+
// Validate: reject project names that look like paths or corpus names
|
|
30
|
+
// Valid: "my-project", "decisionnode-marketplace"
|
|
31
|
+
// Invalid: "user/repo", "C:\\path\\to\\folder", "/absolute/path"
|
|
32
|
+
if (projectName.includes('/') || projectName.includes('\\') || projectName.includes(':')) {
|
|
33
|
+
throw new Error(`Invalid project name "${projectName}". ` +
|
|
34
|
+
`Project name must be a simple folder basename (no slashes, colons, or path separators). ` +
|
|
35
|
+
`Example: "my-project" not "user/repo" or "C:\\path\\to\\folder".`);
|
|
36
|
+
}
|
|
37
|
+
// Also reject if it looks too long (likely a full path)
|
|
38
|
+
if (projectName.length > 100) {
|
|
39
|
+
throw new Error(`Invalid project name "${projectName.substring(0, 50)}...". ` +
|
|
40
|
+
`Project name is too long. Use the folder basename only.`);
|
|
41
|
+
}
|
|
42
|
+
setCurrentProject(projectName);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Tool descriptions based on search sensitivity level
|
|
46
|
+
const SEARCH_DESCRIPTIONS = {
|
|
47
|
+
high: `**MANDATORY: Call this FIRST before ANY code changes.** When user asks you to: add a feature, modify code, fix a bug, implement something, refactor, style UI, or make ANY technical choice — you MUST call this tool FIRST to check for existing conventions. Skipping this causes inconsistency and wasted rework. Query with what you're about to work on: "button styling", "error handling", "API design", "authentication", "database schema", "component structure". If no decisions exist, proceed freely; if decisions exist, FOLLOW them.`,
|
|
48
|
+
medium: `Check for existing decisions when making significant changes or when unsure about project conventions. Use this tool to understand established patterns before implementing major features, architectural changes, or when working on new areas of the codebase. Query with what you're working on: "styling", "API design", "database". If no decisions exist, proceed with your best judgment.`
|
|
49
|
+
};
|
|
50
|
+
// Define available tools with enhanced descriptions for AI auto-discovery
|
|
51
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
52
|
+
// Get current sensitivity setting
|
|
53
|
+
const sensitivity = getSearchSensitivity();
|
|
54
|
+
const searchDescription = SEARCH_DESCRIPTIONS[sensitivity];
|
|
55
|
+
return {
|
|
56
|
+
tools: [
|
|
57
|
+
{
|
|
58
|
+
name: 'search_decisions',
|
|
59
|
+
description: searchDescription,
|
|
60
|
+
inputSchema: {
|
|
61
|
+
type: 'object',
|
|
62
|
+
properties: {
|
|
63
|
+
query: {
|
|
64
|
+
type: 'string',
|
|
65
|
+
description: 'Natural language query describing what you are about to work on (e.g., "button styling", "API error handling", "database connection")',
|
|
66
|
+
},
|
|
67
|
+
limit: {
|
|
68
|
+
type: 'number',
|
|
69
|
+
description: 'Maximum number of results (default: 3)',
|
|
70
|
+
},
|
|
71
|
+
project: {
|
|
72
|
+
type: 'string',
|
|
73
|
+
description: 'REQUIRED: The project folder name. Extract this from the user\'s active file path (e.g., if path is ".../decisionnode-marketplace/src/...", use "decisionnode-marketplace"). Call list_projects first if unsure.',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
required: ['query', 'project'],
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: 'list_decisions',
|
|
81
|
+
description: 'List all recorded decisions for the project. Use this when you need a complete overview of project conventions, or when starting work on a new feature area to understand existing patterns.',
|
|
82
|
+
inputSchema: {
|
|
83
|
+
type: 'object',
|
|
84
|
+
properties: {
|
|
85
|
+
scope: {
|
|
86
|
+
type: 'string',
|
|
87
|
+
description: 'Filter by scope (e.g., UI, Backend, API, Architecture)',
|
|
88
|
+
},
|
|
89
|
+
project: {
|
|
90
|
+
type: 'string',
|
|
91
|
+
description: 'The workspace folder name',
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
required: ['project'],
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: 'get_decision',
|
|
99
|
+
description: 'Get full details of a specific decision by ID. Use this after search_decisions returns relevant results to get complete context including rationale and constraints.',
|
|
100
|
+
inputSchema: {
|
|
101
|
+
type: 'object',
|
|
102
|
+
properties: {
|
|
103
|
+
id: {
|
|
104
|
+
type: 'string',
|
|
105
|
+
description: 'Decision ID (e.g., ui-001)',
|
|
106
|
+
},
|
|
107
|
+
project: {
|
|
108
|
+
type: 'string',
|
|
109
|
+
description: 'The workspace folder name',
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
required: ['id', 'project'],
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: 'add_decision',
|
|
117
|
+
description: `**Call this IMMEDIATELY** when user says phrases like: "Let's use...", "From now on...", "Always do...", "Never do...", "I prefer...", "The standard is...", "We should always...", or confirms ANY technical approach. Also call when: (1) A design pattern is established, (2) An architectural choice is made, (3) Coding standards are discussed, (4) UI/UX conventions are agreed, (5) Technology stack decisions happen. Capture decisions DURING the conversation, not after. Focus on WHY, not just WHAT.`,
|
|
118
|
+
inputSchema: {
|
|
119
|
+
type: 'object',
|
|
120
|
+
properties: {
|
|
121
|
+
scope: {
|
|
122
|
+
type: 'string',
|
|
123
|
+
description: 'Category: UI, Backend, API, Architecture, Database, Security, Testing, DevOps, Styling, Performance',
|
|
124
|
+
},
|
|
125
|
+
decision: {
|
|
126
|
+
type: 'string',
|
|
127
|
+
description: 'Clear statement of what was decided (be specific and actionable)',
|
|
128
|
+
},
|
|
129
|
+
rationale: {
|
|
130
|
+
type: 'string',
|
|
131
|
+
description: 'Why this decision was made - this is crucial for future context',
|
|
132
|
+
},
|
|
133
|
+
constraints: {
|
|
134
|
+
type: 'array',
|
|
135
|
+
items: { type: 'string' },
|
|
136
|
+
description: 'Specific rules or requirements to follow',
|
|
137
|
+
},
|
|
138
|
+
global: {
|
|
139
|
+
type: 'boolean',
|
|
140
|
+
description: 'Set to true to create a global decision that applies across ALL projects (e.g., "always use TypeScript strict mode", "never commit .env files")',
|
|
141
|
+
},
|
|
142
|
+
force: {
|
|
143
|
+
type: 'boolean',
|
|
144
|
+
description: 'Set to true to skip conflict detection and add the decision even if similar ones exist. Use after reviewing the conflicts returned by a previous add_decision call.',
|
|
145
|
+
},
|
|
146
|
+
project: {
|
|
147
|
+
type: 'string',
|
|
148
|
+
description: 'The workspace folder name',
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
required: ['scope', 'decision', 'rationale', 'constraints', 'project'],
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: 'update_decision',
|
|
156
|
+
description: 'Update an existing decision when requirements change or the approach evolves. Use this instead of creating duplicate decisions.',
|
|
157
|
+
inputSchema: {
|
|
158
|
+
type: 'object',
|
|
159
|
+
properties: {
|
|
160
|
+
id: {
|
|
161
|
+
type: 'string',
|
|
162
|
+
description: 'Decision ID to update',
|
|
163
|
+
},
|
|
164
|
+
decision: {
|
|
165
|
+
type: 'string',
|
|
166
|
+
description: 'Updated decision text',
|
|
167
|
+
},
|
|
168
|
+
rationale: {
|
|
169
|
+
type: 'string',
|
|
170
|
+
description: 'Updated rationale',
|
|
171
|
+
},
|
|
172
|
+
status: {
|
|
173
|
+
type: 'string',
|
|
174
|
+
enum: ['active', 'deprecated'],
|
|
175
|
+
description: 'Set to "deprecated" to hide from search (keeps for history), or "active" to re-enable. Only change when the user explicitly asks.',
|
|
176
|
+
},
|
|
177
|
+
constraints: {
|
|
178
|
+
type: 'array',
|
|
179
|
+
items: { type: 'string' },
|
|
180
|
+
description: 'Updated list of constraints',
|
|
181
|
+
},
|
|
182
|
+
project: {
|
|
183
|
+
type: 'string',
|
|
184
|
+
description: 'The workspace folder name',
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
required: ['id', 'decision', 'rationale', 'constraints', 'project'],
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: 'delete_decision',
|
|
192
|
+
description: 'Permanently delete a decision. Only use when a decision was created in error. For outdated decisions, prefer update_decision with status=deprecated to preserve history.',
|
|
193
|
+
inputSchema: {
|
|
194
|
+
type: 'object',
|
|
195
|
+
properties: {
|
|
196
|
+
id: {
|
|
197
|
+
type: 'string',
|
|
198
|
+
description: 'Decision ID to delete',
|
|
199
|
+
},
|
|
200
|
+
project: {
|
|
201
|
+
type: 'string',
|
|
202
|
+
description: 'The workspace folder name',
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
required: ['id', 'project'],
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
name: 'get_history',
|
|
210
|
+
description: 'View the activity log of recent decision changes. Use this to understand what decisions were recently added or modified.',
|
|
211
|
+
inputSchema: {
|
|
212
|
+
type: 'object',
|
|
213
|
+
properties: {
|
|
214
|
+
limit: {
|
|
215
|
+
type: 'number',
|
|
216
|
+
description: 'Number of entries (default: 10)',
|
|
217
|
+
},
|
|
218
|
+
project: {
|
|
219
|
+
type: 'string',
|
|
220
|
+
description: 'The workspace folder name',
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
required: ['limit', 'project'],
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
name: 'get_status',
|
|
228
|
+
description: 'Get project decision status overview including total count and last activity. Use this for a quick health check of the decision store.',
|
|
229
|
+
inputSchema: {
|
|
230
|
+
type: 'object',
|
|
231
|
+
properties: {
|
|
232
|
+
project: {
|
|
233
|
+
type: 'string',
|
|
234
|
+
description: 'The workspace folder name',
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
required: ['project'],
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
name: 'list_projects',
|
|
242
|
+
description: `**Call this FIRST if unsure which project to use.** In monorepos or multi-project workspaces, this lists all projects with decisions. Match the returned project name to the subfolder in the user's active file path. Example: if user is editing ".../my-app/src/component.tsx", look for project "my-app" in the results.`,
|
|
243
|
+
inputSchema: {
|
|
244
|
+
type: 'object',
|
|
245
|
+
properties: {
|
|
246
|
+
verbose: {
|
|
247
|
+
type: 'boolean',
|
|
248
|
+
description: 'Include detailed statistics (default: false)',
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
],
|
|
254
|
+
};
|
|
255
|
+
});
|
|
256
|
+
// MCP Resources - Provides AI instructions and context
|
|
257
|
+
const AI_INSTRUCTIONS = `
|
|
258
|
+
# DecisionNode AI Assistant Instructions
|
|
259
|
+
|
|
260
|
+
You have access to DecisionNode, a decision tracking system. Follow these rules:
|
|
261
|
+
|
|
262
|
+
## BEFORE Starting Any Work
|
|
263
|
+
**ALWAYS** call \`search_decisions\` FIRST before:
|
|
264
|
+
- Writing any new code
|
|
265
|
+
- Modifying existing code
|
|
266
|
+
- Making design choices
|
|
267
|
+
- Implementing features
|
|
268
|
+
- Refactoring code
|
|
269
|
+
- Choosing libraries or approaches
|
|
270
|
+
|
|
271
|
+
Query with what you're about to work on (e.g., "button styling", "authentication", "error handling").
|
|
272
|
+
|
|
273
|
+
## WHEN to Add Decisions
|
|
274
|
+
**IMMEDIATELY** call \`add_decision\` when:
|
|
275
|
+
- User confirms a technical approach
|
|
276
|
+
- A design pattern is established
|
|
277
|
+
- Architectural choices are made
|
|
278
|
+
- Coding standards are discussed
|
|
279
|
+
- Technology decisions happen
|
|
280
|
+
- UI/UX conventions are agreed upon
|
|
281
|
+
|
|
282
|
+
## Decision Quality Guidelines
|
|
283
|
+
- **Be specific**: "Use 8px spacing between form fields" not "Use consistent spacing"
|
|
284
|
+
- **Include rationale**: WHY matters more than WHAT
|
|
285
|
+
- **Scope appropriately**: UI, Backend, API, Architecture, Database, Security, etc.
|
|
286
|
+
- **Add constraints**: Specific rules that must be followed
|
|
287
|
+
|
|
288
|
+
## Common Triggers to Watch For
|
|
289
|
+
- "Let's use..." → Search for existing decisions, then possibly add new one
|
|
290
|
+
- "From now on..." → Add decision immediately
|
|
291
|
+
- "We should always..." → Add decision immediately
|
|
292
|
+
- "I prefer..." → Add decision immediately
|
|
293
|
+
- "The standard is..." → Add decision immediately
|
|
294
|
+
- "Never do..." → Add decision as a constraint
|
|
295
|
+
|
|
296
|
+
## Example Workflow
|
|
297
|
+
1. User asks: "Add a login button to the header"
|
|
298
|
+
2. You: Call \`search_decisions\` with query "button styling" and "header components"
|
|
299
|
+
3. You: Review any existing decisions about buttons, headers, UI patterns
|
|
300
|
+
4. You: Implement following those decisions
|
|
301
|
+
5. If user says "Make all buttons have rounded corners", call \`add_decision\` immediately
|
|
302
|
+
`;
|
|
303
|
+
// Define available resources
|
|
304
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
305
|
+
return {
|
|
306
|
+
resources: [
|
|
307
|
+
{
|
|
308
|
+
uri: 'decisionnode://instructions',
|
|
309
|
+
name: 'AI Assistant Instructions',
|
|
310
|
+
description: 'Guidelines for AI assistants on when and how to use DecisionNode tools. READ THIS FIRST before any coding task.',
|
|
311
|
+
mimeType: 'text/markdown',
|
|
312
|
+
},
|
|
313
|
+
],
|
|
314
|
+
};
|
|
315
|
+
});
|
|
316
|
+
// Handle resource reads
|
|
317
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
318
|
+
const { uri } = request.params;
|
|
319
|
+
if (uri === 'decisionnode://instructions') {
|
|
320
|
+
return {
|
|
321
|
+
contents: [
|
|
322
|
+
{
|
|
323
|
+
uri,
|
|
324
|
+
mimeType: 'text/markdown',
|
|
325
|
+
text: AI_INSTRUCTIONS,
|
|
326
|
+
},
|
|
327
|
+
],
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
throw new Error(`Unknown resource: ${uri}`);
|
|
331
|
+
});
|
|
332
|
+
// Handle tool calls
|
|
333
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
334
|
+
const { name, arguments: args } = request.params;
|
|
335
|
+
switch (name) {
|
|
336
|
+
case 'search_decisions': {
|
|
337
|
+
ensureProject(args);
|
|
338
|
+
const { query, limit = 3 } = args;
|
|
339
|
+
const embeddingReady = await isEmbeddingAvailable();
|
|
340
|
+
if (!embeddingReady) {
|
|
341
|
+
return {
|
|
342
|
+
content: [{
|
|
343
|
+
type: 'text',
|
|
344
|
+
text: JSON.stringify({
|
|
345
|
+
error: 'No Gemini API key configured. Ask the user to run: decide setup',
|
|
346
|
+
}),
|
|
347
|
+
}],
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
const results = await findRelevantDecisions(query, limit);
|
|
351
|
+
return {
|
|
352
|
+
content: [
|
|
353
|
+
{
|
|
354
|
+
type: 'text',
|
|
355
|
+
text: JSON.stringify(results.map((r) => ({
|
|
356
|
+
id: r.decision.id,
|
|
357
|
+
scope: r.decision.scope,
|
|
358
|
+
decision: r.decision.decision,
|
|
359
|
+
rationale: r.decision.rationale,
|
|
360
|
+
score: Math.round(r.score * 100) + '%',
|
|
361
|
+
})), null, 2),
|
|
362
|
+
},
|
|
363
|
+
],
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
case 'list_decisions': {
|
|
367
|
+
ensureProject(args);
|
|
368
|
+
const { scope } = args;
|
|
369
|
+
const projectDecisions = await listDecisions(scope);
|
|
370
|
+
const globalDecisions = await listGlobalDecisions();
|
|
371
|
+
const allDecisions = [...projectDecisions, ...globalDecisions];
|
|
372
|
+
return {
|
|
373
|
+
content: [
|
|
374
|
+
{
|
|
375
|
+
type: 'text',
|
|
376
|
+
text: JSON.stringify(allDecisions.map((d) => ({
|
|
377
|
+
id: d.id,
|
|
378
|
+
scope: d.scope,
|
|
379
|
+
decision: d.decision,
|
|
380
|
+
status: d.status,
|
|
381
|
+
})), null, 2),
|
|
382
|
+
},
|
|
383
|
+
],
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
case 'get_decision': {
|
|
387
|
+
ensureProject(args);
|
|
388
|
+
const { id } = args;
|
|
389
|
+
const decision = isGlobalId(id)
|
|
390
|
+
? await getGlobalDecisionById(id)
|
|
391
|
+
: await getDecisionById(id);
|
|
392
|
+
if (!decision) {
|
|
393
|
+
return {
|
|
394
|
+
content: [{ type: 'text', text: JSON.stringify({ error: `Decision ${id} not found` }) }],
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
return {
|
|
398
|
+
content: [{ type: 'text', text: JSON.stringify(decision, null, 2) }],
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
case 'add_decision': {
|
|
402
|
+
ensureProject(args);
|
|
403
|
+
const { scope, decision, rationale, constraints, global: isGlobal, force } = args;
|
|
404
|
+
const canEmbed = await isEmbeddingAvailable();
|
|
405
|
+
if (!canEmbed) {
|
|
406
|
+
return {
|
|
407
|
+
content: [{
|
|
408
|
+
type: 'text',
|
|
409
|
+
text: JSON.stringify({
|
|
410
|
+
error: 'No Gemini API key configured. The decision cannot be embedded for search. Ask the user to run: decide setup',
|
|
411
|
+
}),
|
|
412
|
+
}],
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
// Check for similar decisions unless force=true
|
|
416
|
+
if (!force) {
|
|
417
|
+
try {
|
|
418
|
+
const conflicts = await findPotentialConflicts(`${scope}: ${decision}`, 0.75);
|
|
419
|
+
if (conflicts.length > 0) {
|
|
420
|
+
return {
|
|
421
|
+
content: [
|
|
422
|
+
{
|
|
423
|
+
type: 'text',
|
|
424
|
+
text: JSON.stringify({
|
|
425
|
+
success: false,
|
|
426
|
+
reason: 'similar_decisions_found',
|
|
427
|
+
message: 'Similar decisions already exist. You can: (1) update an existing decision with update_decision, (2) deprecate the old one with update_decision(status="deprecated") then re-call add_decision with force=true, or (3) re-call add_decision with force=true to add anyway.',
|
|
428
|
+
similar: conflicts.map(c => ({
|
|
429
|
+
id: c.decision.id,
|
|
430
|
+
scope: c.decision.scope,
|
|
431
|
+
decision: c.decision.decision,
|
|
432
|
+
similarity: Math.round(c.score * 100) + '%',
|
|
433
|
+
})),
|
|
434
|
+
}, null, 2),
|
|
435
|
+
},
|
|
436
|
+
],
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
// API key not set or embedding failed — skip conflict check
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (isGlobal) {
|
|
445
|
+
const rawId = await getNextGlobalDecisionId(scope);
|
|
446
|
+
const newDecision = {
|
|
447
|
+
id: rawId,
|
|
448
|
+
scope,
|
|
449
|
+
decision,
|
|
450
|
+
rationale,
|
|
451
|
+
constraints,
|
|
452
|
+
status: 'active',
|
|
453
|
+
createdAt: new Date().toISOString(),
|
|
454
|
+
};
|
|
455
|
+
const { embedded } = await addGlobalDecision(newDecision, 'mcp');
|
|
456
|
+
if (!embedded) {
|
|
457
|
+
// Roll back — delete the decision that couldn't be embedded
|
|
458
|
+
await deleteGlobalDecision(`global:${rawId}`);
|
|
459
|
+
return {
|
|
460
|
+
content: [{
|
|
461
|
+
type: 'text',
|
|
462
|
+
text: JSON.stringify({
|
|
463
|
+
error: 'Embedding failed — the Gemini API returned an error. The decision was not saved. Check that the API key is valid by running: decide setup',
|
|
464
|
+
}),
|
|
465
|
+
}],
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
return {
|
|
469
|
+
content: [
|
|
470
|
+
{
|
|
471
|
+
type: 'text',
|
|
472
|
+
text: JSON.stringify({
|
|
473
|
+
success: true,
|
|
474
|
+
decision: { id: `global:${rawId}`, scope, decision },
|
|
475
|
+
message: 'Global decision added — applies to all projects',
|
|
476
|
+
}, null, 2),
|
|
477
|
+
},
|
|
478
|
+
],
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
const newId = await getNextDecisionId(scope);
|
|
482
|
+
const newDecision = {
|
|
483
|
+
id: newId,
|
|
484
|
+
scope,
|
|
485
|
+
decision,
|
|
486
|
+
rationale,
|
|
487
|
+
constraints,
|
|
488
|
+
status: 'active',
|
|
489
|
+
createdAt: new Date().toISOString(),
|
|
490
|
+
};
|
|
491
|
+
const { embedded } = await addDecision(newDecision, 'mcp');
|
|
492
|
+
if (!embedded) {
|
|
493
|
+
// Roll back — delete the decision that couldn't be embedded
|
|
494
|
+
await deleteDecision(newId);
|
|
495
|
+
return {
|
|
496
|
+
content: [{
|
|
497
|
+
type: 'text',
|
|
498
|
+
text: JSON.stringify({
|
|
499
|
+
error: 'Embedding failed — the Gemini API returned an error. The decision was not saved. Check that the API key is valid by running: decide setup',
|
|
500
|
+
}),
|
|
501
|
+
}],
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
return {
|
|
505
|
+
content: [
|
|
506
|
+
{
|
|
507
|
+
type: 'text',
|
|
508
|
+
text: JSON.stringify({
|
|
509
|
+
success: true,
|
|
510
|
+
decision: { id: newId, scope, decision },
|
|
511
|
+
message: 'Decision added and embedded for search',
|
|
512
|
+
}, null, 2),
|
|
513
|
+
},
|
|
514
|
+
],
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
case 'update_decision': {
|
|
518
|
+
ensureProject(args);
|
|
519
|
+
const { id, ...updates } = args;
|
|
520
|
+
const updated = isGlobalId(id)
|
|
521
|
+
? await updateGlobalDecision(id, updates)
|
|
522
|
+
: await updateDecision(id, updates);
|
|
523
|
+
if (!updated) {
|
|
524
|
+
return {
|
|
525
|
+
content: [{ type: 'text', text: JSON.stringify({ error: `Decision ${id} not found` }) }],
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
return {
|
|
529
|
+
content: [
|
|
530
|
+
{
|
|
531
|
+
type: 'text',
|
|
532
|
+
text: JSON.stringify({ success: true, decision: updated, message: 'Decision updated and re-embedded' }, null, 2),
|
|
533
|
+
},
|
|
534
|
+
],
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
case 'delete_decision': {
|
|
538
|
+
ensureProject(args);
|
|
539
|
+
const { id } = args;
|
|
540
|
+
const deleted = isGlobalId(id)
|
|
541
|
+
? await deleteGlobalDecision(id)
|
|
542
|
+
: await deleteDecision(id);
|
|
543
|
+
return {
|
|
544
|
+
content: [
|
|
545
|
+
{
|
|
546
|
+
type: 'text',
|
|
547
|
+
text: JSON.stringify({ success: deleted, message: deleted ? 'Deleted' : 'Not found' }),
|
|
548
|
+
},
|
|
549
|
+
],
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
case 'get_history': {
|
|
553
|
+
ensureProject(args);
|
|
554
|
+
const { limit = 10 } = args;
|
|
555
|
+
const history = await getHistory(limit);
|
|
556
|
+
return {
|
|
557
|
+
content: [
|
|
558
|
+
{
|
|
559
|
+
type: 'text',
|
|
560
|
+
text: JSON.stringify(history.map((e) => ({
|
|
561
|
+
id: e.id,
|
|
562
|
+
action: e.action,
|
|
563
|
+
description: e.description,
|
|
564
|
+
timestamp: e.timestamp,
|
|
565
|
+
})), null, 2),
|
|
566
|
+
},
|
|
567
|
+
],
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
case 'get_status': {
|
|
571
|
+
ensureProject(args);
|
|
572
|
+
const decisions = await listDecisions();
|
|
573
|
+
const history = await getHistory(1);
|
|
574
|
+
return {
|
|
575
|
+
content: [
|
|
576
|
+
{
|
|
577
|
+
type: 'text',
|
|
578
|
+
text: JSON.stringify({
|
|
579
|
+
project: getCurrentProject(),
|
|
580
|
+
storePath: getProjectRoot(),
|
|
581
|
+
totalDecisions: decisions.length,
|
|
582
|
+
activeDecisions: decisions.filter((d) => d.status === 'active').length,
|
|
583
|
+
lastActivity: history[0]?.description || 'No activity yet',
|
|
584
|
+
}, null, 2),
|
|
585
|
+
},
|
|
586
|
+
],
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
case 'list_projects': {
|
|
590
|
+
const projects = await listProjects();
|
|
591
|
+
const globalDecisions = await listGlobalDecisions();
|
|
592
|
+
return {
|
|
593
|
+
content: [
|
|
594
|
+
{
|
|
595
|
+
type: 'text',
|
|
596
|
+
text: JSON.stringify({
|
|
597
|
+
hint: 'Match project name to the subfolder in the user\'s active file path. Global decisions apply to ALL projects automatically.',
|
|
598
|
+
global: {
|
|
599
|
+
decisions: globalDecisions.length,
|
|
600
|
+
note: 'These decisions are included in all project searches',
|
|
601
|
+
},
|
|
602
|
+
projects: projects.map((p) => ({
|
|
603
|
+
name: p.name,
|
|
604
|
+
decisions: p.decisionCount,
|
|
605
|
+
scopes: p.scopes,
|
|
606
|
+
})),
|
|
607
|
+
}, null, 2),
|
|
608
|
+
},
|
|
609
|
+
],
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
default:
|
|
613
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
// Start the server
|
|
617
|
+
async function main() {
|
|
618
|
+
const transport = new StdioServerTransport();
|
|
619
|
+
await server.connect(transport);
|
|
620
|
+
}
|
|
621
|
+
main().catch(console.error);
|