@lovelybunch/api 1.0.70 → 1.0.71-alpha.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/dist/lib/storage/file-storage.d.ts +1 -0
- package/dist/lib/storage/file-storage.js +16 -1
- package/dist/routes/api/v1/ai/index.js +2 -0
- package/dist/routes/api/v1/ai/route.js +468 -170
- package/dist/routes/api/v1/ai/tools.d.ts +17 -0
- package/dist/routes/api/v1/ai/tools.js +926 -0
- package/dist/routes/api/v1/mcp/index.js +410 -184
- package/dist/routes/api/v1/resources/generate/route.d.ts +1 -1
- package/dist/routes/api/v1/resources/generate/route.js +66 -16
- package/package.json +4 -4
- package/static/assets/index-B2wygg2W.css +33 -0
- package/static/assets/index-CYlVRUG2.js +969 -0
- package/static/index.html +2 -2
- package/static/assets/index-BmLW21zG.js +0 -969
- package/static/assets/index-CfRmV6nM.css +0 -33
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
|
-
import { promises as fs } from 'fs';
|
|
2
|
+
import { promises as fs, createReadStream } from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
|
+
import readline from 'readline';
|
|
4
5
|
import matter from 'gray-matter';
|
|
5
|
-
import
|
|
6
|
+
import Fuse from 'fuse.js';
|
|
7
|
+
import { getLogsDir } from '@lovelybunch/core';
|
|
8
|
+
import { proposalsReadOnlyTool, knowledgeTool, normalizeKnowledgeMetadata, eventsTool, projectContextTool, architectureContextTool } from '@lovelybunch/mcp';
|
|
6
9
|
import { FileStorageAdapter } from '../../../../lib/storage/file-storage.js';
|
|
7
|
-
import { getAuthorInfo } from '../../../../lib/user-preferences.js';
|
|
8
10
|
const app = new Hono();
|
|
9
11
|
const storage = new FileStorageAdapter();
|
|
10
12
|
function resolveGaitPath() {
|
|
@@ -42,10 +44,13 @@ app.get('/', async (c) => {
|
|
|
42
44
|
}
|
|
43
45
|
const names = Object.keys(externalServers);
|
|
44
46
|
// Add built-in tools (include full JSON schema for parameters)
|
|
47
|
+
// Note: proposals is read-only, knowledge/project/architecture are read+write, events is read-only
|
|
45
48
|
const builtInTools = {
|
|
46
|
-
change_proposals:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
change_proposals: proposalsReadOnlyTool,
|
|
50
|
+
knowledge_documents: knowledgeTool,
|
|
51
|
+
activity_events: eventsTool,
|
|
52
|
+
project_context: projectContextTool,
|
|
53
|
+
architecture_context: architectureContextTool
|
|
49
54
|
};
|
|
50
55
|
return c.json({
|
|
51
56
|
success: true,
|
|
@@ -65,11 +70,14 @@ app.get('/', async (c) => {
|
|
|
65
70
|
*/
|
|
66
71
|
app.get('/schema', async (c) => {
|
|
67
72
|
try {
|
|
73
|
+
// Note: proposals is read-only, knowledge/project/architecture are read+write, events is read-only
|
|
68
74
|
const schema = {
|
|
69
75
|
tools: {
|
|
70
|
-
change_proposals:
|
|
71
|
-
|
|
72
|
-
|
|
76
|
+
change_proposals: proposalsReadOnlyTool,
|
|
77
|
+
knowledge_documents: knowledgeTool,
|
|
78
|
+
activity_events: eventsTool,
|
|
79
|
+
project_context: projectContextTool,
|
|
80
|
+
architecture_context: architectureContextTool
|
|
73
81
|
}
|
|
74
82
|
};
|
|
75
83
|
return c.json(schema);
|
|
@@ -92,12 +100,18 @@ app.post('/execute', async (c) => {
|
|
|
92
100
|
if (tool === 'change_proposals') {
|
|
93
101
|
return await executeProposalsTool(c, args);
|
|
94
102
|
}
|
|
95
|
-
if (tool === 'list_proposals') {
|
|
96
|
-
return await executeListProposalsTool(c, args);
|
|
97
|
-
}
|
|
98
103
|
if (tool === 'knowledge_documents') {
|
|
99
104
|
return await executeKnowledgeTool(c, args);
|
|
100
105
|
}
|
|
106
|
+
if (tool === 'activity_events') {
|
|
107
|
+
return await executeEventsTool(c, args);
|
|
108
|
+
}
|
|
109
|
+
if (tool === 'project_context') {
|
|
110
|
+
return await executeProjectContextTool(c, args);
|
|
111
|
+
}
|
|
112
|
+
if (tool === 'architecture_context') {
|
|
113
|
+
return await executeArchitectureContextTool(c, args);
|
|
114
|
+
}
|
|
101
115
|
return c.json({ success: false, error: 'Unknown tool' }, 400);
|
|
102
116
|
}
|
|
103
117
|
catch (error) {
|
|
@@ -105,8 +119,9 @@ app.post('/execute', async (c) => {
|
|
|
105
119
|
return c.json({ success: false, error: 'Tool execution failed' }, 500);
|
|
106
120
|
}
|
|
107
121
|
});
|
|
122
|
+
// Proposals tool is READ-ONLY - only list and get operations are supported
|
|
108
123
|
async function executeProposalsTool(c, args) {
|
|
109
|
-
const { operation, id, filters
|
|
124
|
+
const { operation, id, filters } = args;
|
|
110
125
|
try {
|
|
111
126
|
switch (operation) {
|
|
112
127
|
case 'list': {
|
|
@@ -131,138 +146,11 @@ async function executeProposalsTool(c, args) {
|
|
|
131
146
|
message: `Retrieved proposal ${id}`
|
|
132
147
|
});
|
|
133
148
|
}
|
|
134
|
-
case 'create': {
|
|
135
|
-
if (!proposal) {
|
|
136
|
-
return c.json({ success: false, error: 'Proposal data is required for create operation' }, 400);
|
|
137
|
-
}
|
|
138
|
-
const validatedProposal = validateProposalData(proposal);
|
|
139
|
-
const now = new Date();
|
|
140
|
-
const authorInfo = await getAuthorInfo();
|
|
141
|
-
// Ensure planSteps are properly structured
|
|
142
|
-
const planSteps = (validatedProposal.planSteps || []).map((step, index) => {
|
|
143
|
-
if (typeof step === 'string') {
|
|
144
|
-
return {
|
|
145
|
-
id: `step-${index + 1}`,
|
|
146
|
-
description: step,
|
|
147
|
-
status: 'pending'
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
return {
|
|
151
|
-
id: step.id || `step-${index + 1}`,
|
|
152
|
-
description: step.description || '',
|
|
153
|
-
status: step.status || 'pending',
|
|
154
|
-
command: step.command,
|
|
155
|
-
expectedOutcome: step.expectedOutcome,
|
|
156
|
-
output: step.output,
|
|
157
|
-
error: step.error,
|
|
158
|
-
executedAt: step.executedAt
|
|
159
|
-
};
|
|
160
|
-
});
|
|
161
|
-
const newProposal = {
|
|
162
|
-
id: `cp-${Date.now()}`,
|
|
163
|
-
intent: validatedProposal.intent || '',
|
|
164
|
-
content: validatedProposal.content || '',
|
|
165
|
-
author: {
|
|
166
|
-
id: validatedProposal.author?.id || 'current-user',
|
|
167
|
-
name: validatedProposal.author?.name || authorInfo.name || 'Unknown User',
|
|
168
|
-
email: validatedProposal.author?.email || authorInfo.email || '',
|
|
169
|
-
type: validatedProposal.author?.type || 'human'
|
|
170
|
-
},
|
|
171
|
-
planSteps,
|
|
172
|
-
evidence: validatedProposal.evidence || [],
|
|
173
|
-
policies: validatedProposal.policies || [],
|
|
174
|
-
featureFlags: validatedProposal.featureFlags || [],
|
|
175
|
-
experiments: validatedProposal.experiments || [],
|
|
176
|
-
telemetryContracts: validatedProposal.telemetryContracts || [],
|
|
177
|
-
releasePlan: validatedProposal.releasePlan || { strategy: 'immediate' },
|
|
178
|
-
status: validatedProposal.status || 'draft',
|
|
179
|
-
metadata: {
|
|
180
|
-
createdAt: now,
|
|
181
|
-
updatedAt: now,
|
|
182
|
-
reviewers: validatedProposal.metadata?.reviewers || [],
|
|
183
|
-
aiInteractions: validatedProposal.metadata?.aiInteractions || [],
|
|
184
|
-
tags: validatedProposal.metadata?.tags || [],
|
|
185
|
-
priority: validatedProposal.metadata?.priority || 'medium'
|
|
186
|
-
},
|
|
187
|
-
productSpecRef: validatedProposal.productSpecRef
|
|
188
|
-
};
|
|
189
|
-
// Debug logging to identify undefined values
|
|
190
|
-
console.log('Proposal data before storage:', JSON.stringify(newProposal, null, 2));
|
|
191
|
-
await storage.createCP(newProposal);
|
|
192
|
-
return c.json({
|
|
193
|
-
success: true,
|
|
194
|
-
data: newProposal,
|
|
195
|
-
message: `Created proposal ${newProposal.id}`
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
case 'update': {
|
|
199
|
-
if (!id) {
|
|
200
|
-
return c.json({ success: false, error: 'Proposal ID is required for update operation' }, 400);
|
|
201
|
-
}
|
|
202
|
-
if (!proposal) {
|
|
203
|
-
return c.json({ success: false, error: 'Proposal data is required for update operation' }, 400);
|
|
204
|
-
}
|
|
205
|
-
const existing = await storage.getCP(id);
|
|
206
|
-
if (!existing) {
|
|
207
|
-
return c.json({ success: false, error: 'Proposal not found' }, 404);
|
|
208
|
-
}
|
|
209
|
-
const validatedUpdates = validateProposalData(proposal);
|
|
210
|
-
// Ensure planSteps are properly structured if they're being updated
|
|
211
|
-
let planSteps = existing.planSteps;
|
|
212
|
-
if (validatedUpdates.planSteps) {
|
|
213
|
-
planSteps = validatedUpdates.planSteps.map((step, index) => {
|
|
214
|
-
if (typeof step === 'string') {
|
|
215
|
-
return {
|
|
216
|
-
id: `step-${index + 1}`,
|
|
217
|
-
description: step,
|
|
218
|
-
status: 'pending'
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
return {
|
|
222
|
-
id: step.id || `step-${index + 1}`,
|
|
223
|
-
description: step.description || '',
|
|
224
|
-
status: step.status || 'pending',
|
|
225
|
-
command: step.command,
|
|
226
|
-
expectedOutcome: step.expectedOutcome,
|
|
227
|
-
output: step.output,
|
|
228
|
-
error: step.error,
|
|
229
|
-
executedAt: step.executedAt
|
|
230
|
-
};
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
const updatedProposal = {
|
|
234
|
-
...existing,
|
|
235
|
-
...validatedUpdates,
|
|
236
|
-
planSteps,
|
|
237
|
-
metadata: {
|
|
238
|
-
...existing.metadata,
|
|
239
|
-
...validatedUpdates.metadata,
|
|
240
|
-
updatedAt: new Date()
|
|
241
|
-
}
|
|
242
|
-
};
|
|
243
|
-
await storage.updateCP(id, updatedProposal);
|
|
244
|
-
return c.json({
|
|
245
|
-
success: true,
|
|
246
|
-
data: updatedProposal,
|
|
247
|
-
message: `Updated proposal ${id}`
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
case 'delete': {
|
|
251
|
-
if (!id) {
|
|
252
|
-
return c.json({ success: false, error: 'Proposal ID is required for delete operation' }, 400);
|
|
253
|
-
}
|
|
254
|
-
const existing = await storage.getCP(id);
|
|
255
|
-
if (!existing) {
|
|
256
|
-
return c.json({ success: false, error: 'Proposal not found' }, 404);
|
|
257
|
-
}
|
|
258
|
-
await storage.deleteCP(id);
|
|
259
|
-
return c.json({
|
|
260
|
-
success: true,
|
|
261
|
-
message: `Deleted proposal ${id}`
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
149
|
default:
|
|
265
|
-
return c.json({
|
|
150
|
+
return c.json({
|
|
151
|
+
success: false,
|
|
152
|
+
error: `Proposals are read-only. Only 'list' and 'get' operations are supported. To create proposals, use a coding agent (Claude Code, Cursor, etc.) or the Proposals UI.`
|
|
153
|
+
}, 400);
|
|
266
154
|
}
|
|
267
155
|
}
|
|
268
156
|
catch (error) {
|
|
@@ -270,49 +158,21 @@ async function executeProposalsTool(c, args) {
|
|
|
270
158
|
return c.json({ success: false, error: error.message || 'Tool execution failed' }, 500);
|
|
271
159
|
}
|
|
272
160
|
}
|
|
273
|
-
async function executeListProposalsTool(c, args) {
|
|
274
|
-
const { filters } = args;
|
|
275
|
-
try {
|
|
276
|
-
const proposals = await storage.listCPs(filters || {});
|
|
277
|
-
// Return only metadata, not full content
|
|
278
|
-
const metadataOnly = proposals.map(proposal => ({
|
|
279
|
-
id: proposal.id,
|
|
280
|
-
intent: proposal.intent,
|
|
281
|
-
status: proposal.status,
|
|
282
|
-
priority: proposal.metadata?.priority || 'medium',
|
|
283
|
-
tags: proposal.metadata?.tags || [],
|
|
284
|
-
author: {
|
|
285
|
-
name: proposal.author.name,
|
|
286
|
-
email: proposal.author.email
|
|
287
|
-
},
|
|
288
|
-
createdAt: proposal.metadata.createdAt,
|
|
289
|
-
updatedAt: proposal.metadata.updatedAt,
|
|
290
|
-
reviewers: proposal.metadata.reviewers || [],
|
|
291
|
-
productSpecRef: proposal.productSpecRef
|
|
292
|
-
}));
|
|
293
|
-
return c.json({
|
|
294
|
-
success: true,
|
|
295
|
-
data: metadataOnly,
|
|
296
|
-
message: `Found ${metadataOnly.length} proposals`
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
catch (error) {
|
|
300
|
-
console.error('Error executing list proposals tool:', error);
|
|
301
|
-
return c.json({ success: false, error: error.message || 'Failed to list proposals' }, 500);
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
161
|
async function executeKnowledgeTool(c, args) {
|
|
305
|
-
const { operation, filename, title, content, metadata, summary } = args;
|
|
162
|
+
const { operation, filename, title, content, metadata, summary, query } = args;
|
|
306
163
|
try {
|
|
307
164
|
const knowledgePath = getKnowledgePath();
|
|
308
165
|
await fs.mkdir(knowledgePath, { recursive: true });
|
|
309
166
|
switch (operation) {
|
|
310
167
|
case 'list': {
|
|
311
|
-
const
|
|
168
|
+
const searchQuery = query && typeof query === 'string' ? query : undefined;
|
|
169
|
+
const documents = await listKnowledgeDocuments(knowledgePath, searchQuery);
|
|
312
170
|
return c.json({
|
|
313
171
|
success: true,
|
|
314
172
|
data: documents,
|
|
315
|
-
message:
|
|
173
|
+
message: searchQuery
|
|
174
|
+
? `Found ${documents.length} knowledge documents matching "${searchQuery}"`
|
|
175
|
+
: `Found ${documents.length} knowledge documents`
|
|
316
176
|
});
|
|
317
177
|
}
|
|
318
178
|
case 'get': {
|
|
@@ -449,6 +309,302 @@ async function executeKnowledgeTool(c, args) {
|
|
|
449
309
|
return c.json({ success: false, error: error.message || 'Knowledge tool execution failed' }, 500);
|
|
450
310
|
}
|
|
451
311
|
}
|
|
312
|
+
// Events tool is READ-ONLY - returns recent activity events
|
|
313
|
+
async function executeEventsTool(c, args) {
|
|
314
|
+
const { operation, limit = 20, kind } = args;
|
|
315
|
+
if (operation !== 'list') {
|
|
316
|
+
return c.json({
|
|
317
|
+
success: false,
|
|
318
|
+
error: `Events are read-only. Only 'list' operation is supported.`
|
|
319
|
+
}, 400);
|
|
320
|
+
}
|
|
321
|
+
try {
|
|
322
|
+
const eventsDir = getLogsDir();
|
|
323
|
+
const currentFile = path.join(eventsDir, 'events-current.jsonl');
|
|
324
|
+
// Check if file exists
|
|
325
|
+
try {
|
|
326
|
+
await fs.access(currentFile);
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
return c.json({
|
|
330
|
+
success: true,
|
|
331
|
+
data: [],
|
|
332
|
+
message: 'No events found'
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
// Read all events from file
|
|
336
|
+
const allEvents = [];
|
|
337
|
+
const fileStream = createReadStream(currentFile);
|
|
338
|
+
const rl = readline.createInterface({
|
|
339
|
+
input: fileStream,
|
|
340
|
+
crlfDelay: Infinity,
|
|
341
|
+
});
|
|
342
|
+
for await (const line of rl) {
|
|
343
|
+
if (!line.trim())
|
|
344
|
+
continue;
|
|
345
|
+
try {
|
|
346
|
+
const event = JSON.parse(line);
|
|
347
|
+
// Apply kind filter if specified
|
|
348
|
+
if (kind && typeof kind === 'string') {
|
|
349
|
+
if (!event.kind || !event.kind.startsWith(kind)) {
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
allEvents.push(event);
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
// Skip malformed lines
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
// Sort by seq descending (most recent first) and limit
|
|
361
|
+
const cappedLimit = Math.min(Math.max(1, limit), 100);
|
|
362
|
+
const sortedEvents = allEvents
|
|
363
|
+
.sort((a, b) => (b.seq || 0) - (a.seq || 0))
|
|
364
|
+
.slice(0, cappedLimit);
|
|
365
|
+
// Format events for readability
|
|
366
|
+
const formattedEvents = sortedEvents.map(event => ({
|
|
367
|
+
seq: event.seq,
|
|
368
|
+
kind: event.kind,
|
|
369
|
+
actor: event.actor,
|
|
370
|
+
subject: event.subject,
|
|
371
|
+
timestamp: event.ts,
|
|
372
|
+
level: event.level,
|
|
373
|
+
tags: event.tags,
|
|
374
|
+
summary: event.payload?.summary || event.payload?.message || event.payload?.title || undefined
|
|
375
|
+
}));
|
|
376
|
+
return c.json({
|
|
377
|
+
success: true,
|
|
378
|
+
data: formattedEvents,
|
|
379
|
+
message: kind
|
|
380
|
+
? `Found ${formattedEvents.length} recent '${kind}' events`
|
|
381
|
+
: `Found ${formattedEvents.length} recent events`
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
console.error('Error executing events tool:', error);
|
|
386
|
+
return c.json({ success: false, error: error.message || 'Events tool execution failed' }, 500);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
// Project context tool - read/write the project definition document
|
|
390
|
+
async function executeProjectContextTool(c, args) {
|
|
391
|
+
const { operation, content, old_text, new_text } = args;
|
|
392
|
+
try {
|
|
393
|
+
const contextPath = getContextPath();
|
|
394
|
+
const filePath = path.join(contextPath, 'project.md');
|
|
395
|
+
switch (operation) {
|
|
396
|
+
case 'get': {
|
|
397
|
+
try {
|
|
398
|
+
const fileContent = await fs.readFile(filePath, 'utf-8');
|
|
399
|
+
const parsed = matter(fileContent);
|
|
400
|
+
return c.json({
|
|
401
|
+
success: true,
|
|
402
|
+
data: {
|
|
403
|
+
content: parsed.content,
|
|
404
|
+
frontmatter: parsed.data,
|
|
405
|
+
raw: fileContent
|
|
406
|
+
},
|
|
407
|
+
message: 'Retrieved project context'
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
catch (err) {
|
|
411
|
+
if (err.code === 'ENOENT') {
|
|
412
|
+
return c.json({
|
|
413
|
+
success: true,
|
|
414
|
+
data: { content: '', frontmatter: {}, raw: '' },
|
|
415
|
+
message: 'Project context document does not exist yet. You can create it with an update operation.'
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
throw err;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
case 'append': {
|
|
422
|
+
if (!content) {
|
|
423
|
+
return c.json({ success: false, error: 'Content is required for append operation' }, 400);
|
|
424
|
+
}
|
|
425
|
+
await fs.mkdir(contextPath, { recursive: true });
|
|
426
|
+
// Read existing content if file exists
|
|
427
|
+
let existingContent = '';
|
|
428
|
+
try {
|
|
429
|
+
existingContent = await fs.readFile(filePath, 'utf-8');
|
|
430
|
+
}
|
|
431
|
+
catch (err) {
|
|
432
|
+
if (err.code !== 'ENOENT')
|
|
433
|
+
throw err;
|
|
434
|
+
}
|
|
435
|
+
// Append new content with a newline separator
|
|
436
|
+
const newContent = existingContent
|
|
437
|
+
? existingContent.trimEnd() + '\n\n' + content
|
|
438
|
+
: content;
|
|
439
|
+
await fs.writeFile(filePath, newContent, 'utf-8');
|
|
440
|
+
return c.json({
|
|
441
|
+
success: true,
|
|
442
|
+
message: 'Appended content to project context document'
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
case 'replace_section': {
|
|
446
|
+
if (!old_text || !new_text) {
|
|
447
|
+
return c.json({ success: false, error: 'Both old_text and new_text are required for replace_section operation' }, 400);
|
|
448
|
+
}
|
|
449
|
+
// Read existing content
|
|
450
|
+
let existingContent = '';
|
|
451
|
+
try {
|
|
452
|
+
existingContent = await fs.readFile(filePath, 'utf-8');
|
|
453
|
+
}
|
|
454
|
+
catch (err) {
|
|
455
|
+
if (err.code === 'ENOENT') {
|
|
456
|
+
return c.json({ success: false, error: 'Project context document does not exist. Use append or update to create it first.' }, 404);
|
|
457
|
+
}
|
|
458
|
+
throw err;
|
|
459
|
+
}
|
|
460
|
+
// Check if old_text exists in the document
|
|
461
|
+
if (!existingContent.includes(old_text)) {
|
|
462
|
+
return c.json({
|
|
463
|
+
success: false,
|
|
464
|
+
error: 'Could not find the specified text in the document. The text may have been paraphrased or changed.',
|
|
465
|
+
fallback_markdown: new_text,
|
|
466
|
+
suggestion: 'Here is the replacement text. You can copy it and manually edit the document at /context/project'
|
|
467
|
+
}, 400);
|
|
468
|
+
}
|
|
469
|
+
// Replace the text
|
|
470
|
+
const updatedContent = existingContent.replace(old_text, new_text);
|
|
471
|
+
await fs.writeFile(filePath, updatedContent, 'utf-8');
|
|
472
|
+
return c.json({
|
|
473
|
+
success: true,
|
|
474
|
+
message: 'Replaced section in project context document'
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
case 'update': {
|
|
478
|
+
if (!content) {
|
|
479
|
+
return c.json({ success: false, error: 'Content is required for update operation' }, 400);
|
|
480
|
+
}
|
|
481
|
+
await fs.mkdir(contextPath, { recursive: true });
|
|
482
|
+
await fs.writeFile(filePath, content, 'utf-8');
|
|
483
|
+
return c.json({
|
|
484
|
+
success: true,
|
|
485
|
+
message: 'Updated project context document'
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
default:
|
|
489
|
+
return c.json({ success: false, error: `Unknown operation: ${operation}. Use 'get', 'append', 'replace_section', or 'update'.` }, 400);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
catch (error) {
|
|
493
|
+
console.error('Error executing project context tool:', error);
|
|
494
|
+
return c.json({ success: false, error: error.message || 'Project context tool execution failed' }, 500);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
// Architecture context tool - read/write the architecture document
|
|
498
|
+
async function executeArchitectureContextTool(c, args) {
|
|
499
|
+
const { operation, content, old_text, new_text } = args;
|
|
500
|
+
try {
|
|
501
|
+
const contextPath = getContextPath();
|
|
502
|
+
const filePath = path.join(contextPath, 'architecture.md');
|
|
503
|
+
switch (operation) {
|
|
504
|
+
case 'get': {
|
|
505
|
+
try {
|
|
506
|
+
const fileContent = await fs.readFile(filePath, 'utf-8');
|
|
507
|
+
const parsed = matter(fileContent);
|
|
508
|
+
return c.json({
|
|
509
|
+
success: true,
|
|
510
|
+
data: {
|
|
511
|
+
content: parsed.content,
|
|
512
|
+
frontmatter: parsed.data,
|
|
513
|
+
raw: fileContent
|
|
514
|
+
},
|
|
515
|
+
message: 'Retrieved architecture context'
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
catch (err) {
|
|
519
|
+
if (err.code === 'ENOENT') {
|
|
520
|
+
return c.json({
|
|
521
|
+
success: true,
|
|
522
|
+
data: { content: '', frontmatter: {}, raw: '' },
|
|
523
|
+
message: 'Architecture context document does not exist yet. You can create it with an update operation.'
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
throw err;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
case 'append': {
|
|
530
|
+
if (!content) {
|
|
531
|
+
return c.json({ success: false, error: 'Content is required for append operation' }, 400);
|
|
532
|
+
}
|
|
533
|
+
await fs.mkdir(contextPath, { recursive: true });
|
|
534
|
+
// Read existing content if file exists
|
|
535
|
+
let existingContent = '';
|
|
536
|
+
try {
|
|
537
|
+
existingContent = await fs.readFile(filePath, 'utf-8');
|
|
538
|
+
}
|
|
539
|
+
catch (err) {
|
|
540
|
+
if (err.code !== 'ENOENT')
|
|
541
|
+
throw err;
|
|
542
|
+
}
|
|
543
|
+
// Append new content with a newline separator
|
|
544
|
+
const newContent = existingContent
|
|
545
|
+
? existingContent.trimEnd() + '\n\n' + content
|
|
546
|
+
: content;
|
|
547
|
+
await fs.writeFile(filePath, newContent, 'utf-8');
|
|
548
|
+
return c.json({
|
|
549
|
+
success: true,
|
|
550
|
+
message: 'Appended content to architecture context document'
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
case 'replace_section': {
|
|
554
|
+
if (!old_text || !new_text) {
|
|
555
|
+
return c.json({ success: false, error: 'Both old_text and new_text are required for replace_section operation' }, 400);
|
|
556
|
+
}
|
|
557
|
+
// Read existing content
|
|
558
|
+
let existingContent = '';
|
|
559
|
+
try {
|
|
560
|
+
existingContent = await fs.readFile(filePath, 'utf-8');
|
|
561
|
+
}
|
|
562
|
+
catch (err) {
|
|
563
|
+
if (err.code === 'ENOENT') {
|
|
564
|
+
return c.json({ success: false, error: 'Architecture context document does not exist. Use append or update to create it first.' }, 404);
|
|
565
|
+
}
|
|
566
|
+
throw err;
|
|
567
|
+
}
|
|
568
|
+
// Check if old_text exists in the document
|
|
569
|
+
if (!existingContent.includes(old_text)) {
|
|
570
|
+
return c.json({
|
|
571
|
+
success: false,
|
|
572
|
+
error: 'Could not find the specified text in the document. The text may have been paraphrased or changed.',
|
|
573
|
+
fallback_markdown: new_text,
|
|
574
|
+
suggestion: 'Here is the replacement text. You can copy it and manually edit the document at /context/architecture'
|
|
575
|
+
}, 400);
|
|
576
|
+
}
|
|
577
|
+
// Replace the text
|
|
578
|
+
const updatedContent = existingContent.replace(old_text, new_text);
|
|
579
|
+
await fs.writeFile(filePath, updatedContent, 'utf-8');
|
|
580
|
+
return c.json({
|
|
581
|
+
success: true,
|
|
582
|
+
message: 'Replaced section in architecture context document'
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
case 'update': {
|
|
586
|
+
if (!content) {
|
|
587
|
+
return c.json({ success: false, error: 'Content is required for update operation' }, 400);
|
|
588
|
+
}
|
|
589
|
+
await fs.mkdir(contextPath, { recursive: true });
|
|
590
|
+
await fs.writeFile(filePath, content, 'utf-8');
|
|
591
|
+
return c.json({
|
|
592
|
+
success: true,
|
|
593
|
+
message: 'Updated architecture context document'
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
default:
|
|
597
|
+
return c.json({ success: false, error: `Unknown operation: ${operation}. Use 'get', 'append', 'replace_section', or 'update'.` }, 400);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
catch (error) {
|
|
601
|
+
console.error('Error executing architecture context tool:', error);
|
|
602
|
+
return c.json({ success: false, error: error.message || 'Architecture context tool execution failed' }, 500);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
function getContextPath() {
|
|
606
|
+
return path.join(resolveGaitPath(), 'context');
|
|
607
|
+
}
|
|
452
608
|
function getKnowledgePath() {
|
|
453
609
|
return path.join(resolveGaitPath(), 'context', 'knowledge');
|
|
454
610
|
}
|
|
@@ -496,22 +652,92 @@ async function readKnowledgeDocument(basePath, filename) {
|
|
|
496
652
|
content
|
|
497
653
|
};
|
|
498
654
|
}
|
|
499
|
-
|
|
655
|
+
function extractSnippet(content, query, maxLength = 200) {
|
|
656
|
+
const lowerContent = content.toLowerCase();
|
|
657
|
+
const lowerQuery = query.toLowerCase();
|
|
658
|
+
const terms = lowerQuery.split(/\s+/);
|
|
659
|
+
// Find the first occurrence of any search term
|
|
660
|
+
let bestIndex = -1;
|
|
661
|
+
let matchedTerm = '';
|
|
662
|
+
for (const term of terms) {
|
|
663
|
+
const index = lowerContent.indexOf(term);
|
|
664
|
+
if (index !== -1 && (bestIndex === -1 || index < bestIndex)) {
|
|
665
|
+
bestIndex = index;
|
|
666
|
+
matchedTerm = term;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
if (bestIndex === -1) {
|
|
670
|
+
// No match found, return beginning of content
|
|
671
|
+
return content.slice(0, maxLength) + (content.length > maxLength ? '...' : '');
|
|
672
|
+
}
|
|
673
|
+
// Extract snippet around the match
|
|
674
|
+
const start = Math.max(0, bestIndex - 60);
|
|
675
|
+
const end = Math.min(content.length, bestIndex + matchedTerm.length + 140);
|
|
676
|
+
let snippet = content.slice(start, end);
|
|
677
|
+
// Add ellipsis if needed
|
|
678
|
+
if (start > 0)
|
|
679
|
+
snippet = '...' + snippet;
|
|
680
|
+
if (end < content.length)
|
|
681
|
+
snippet = snippet + '...';
|
|
682
|
+
return snippet;
|
|
683
|
+
}
|
|
684
|
+
function findMatchedFields(doc, query) {
|
|
685
|
+
const lowerQuery = query.toLowerCase();
|
|
686
|
+
const terms = lowerQuery.split(/\s+/);
|
|
687
|
+
const matched = [];
|
|
688
|
+
const checkField = (value, fieldName) => {
|
|
689
|
+
if (value && terms.some(term => value.toLowerCase().includes(term))) {
|
|
690
|
+
matched.push(fieldName);
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
checkField(doc.title, 'title');
|
|
694
|
+
checkField(doc.content, 'content');
|
|
695
|
+
checkField(doc.metadata?.category, 'category');
|
|
696
|
+
checkField(doc.metadata?.summary, 'summary');
|
|
697
|
+
if (doc.metadata?.tags?.some(tag => terms.some(term => tag.toLowerCase().includes(term)))) {
|
|
698
|
+
matched.push('tags');
|
|
699
|
+
}
|
|
700
|
+
return matched;
|
|
701
|
+
}
|
|
702
|
+
async function listKnowledgeDocuments(basePath, query) {
|
|
500
703
|
const files = await fs.readdir(basePath);
|
|
501
704
|
const markdownFiles = files.filter(file => file.endsWith('.md'));
|
|
502
705
|
const documents = await Promise.all(markdownFiles.map(async (file) => {
|
|
503
706
|
try {
|
|
504
|
-
|
|
505
|
-
return doc;
|
|
707
|
+
return await readKnowledgeDocument(basePath, file);
|
|
506
708
|
}
|
|
507
709
|
catch (error) {
|
|
508
710
|
console.error(`Failed to read knowledge document ${file}:`, error);
|
|
509
711
|
return null;
|
|
510
712
|
}
|
|
511
713
|
}));
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
714
|
+
const validDocs = documents.filter((doc) => doc !== null);
|
|
715
|
+
// If no query, return all documents sorted by filename
|
|
716
|
+
if (!query?.trim()) {
|
|
717
|
+
return validDocs.sort((a, b) => a.filename.localeCompare(b.filename));
|
|
718
|
+
}
|
|
719
|
+
// Use Fuse.js for fuzzy search
|
|
720
|
+
const fuse = new Fuse(validDocs, {
|
|
721
|
+
keys: [
|
|
722
|
+
{ name: 'title', weight: 2 },
|
|
723
|
+
{ name: 'content', weight: 1 },
|
|
724
|
+
{ name: 'metadata.summary', weight: 1.5 },
|
|
725
|
+
{ name: 'metadata.category', weight: 1 },
|
|
726
|
+
{ name: 'metadata.tags', weight: 1.5 }
|
|
727
|
+
],
|
|
728
|
+
threshold: 0.4,
|
|
729
|
+
includeScore: true,
|
|
730
|
+
ignoreLocation: true,
|
|
731
|
+
minMatchCharLength: 2
|
|
732
|
+
});
|
|
733
|
+
const results = fuse.search(query);
|
|
734
|
+
// Enhance results with snippets and matched fields
|
|
735
|
+
return results.map(result => ({
|
|
736
|
+
...result.item,
|
|
737
|
+
snippet: extractSnippet(result.item.content, query),
|
|
738
|
+
matchedIn: findMatchedFields(result.item, query),
|
|
739
|
+
score: result.score
|
|
740
|
+
}));
|
|
515
741
|
}
|
|
516
742
|
/**
|
|
517
743
|
* GET /api/v1/mcp/raw-config
|