@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,11 +1,13 @@
|
|
|
1
1
|
import { homedir } from 'os';
|
|
2
2
|
import { join, resolve as pathResolve, basename } from 'path';
|
|
3
|
-
import { existsSync, readFileSync, promises as fs } from 'fs';
|
|
3
|
+
import { existsSync, readFileSync, promises as fs, createReadStream } from 'fs';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
|
-
import
|
|
5
|
+
import readline from 'readline';
|
|
6
|
+
import { getLogsDir } from '@lovelybunch/core';
|
|
7
|
+
import { proposalsReadOnlyTool, knowledgeTool, normalizeKnowledgeMetadata, eventsTool, projectContextTool, architectureContextTool } from '@lovelybunch/mcp';
|
|
6
8
|
import matter from 'gray-matter';
|
|
9
|
+
import Fuse from 'fuse.js';
|
|
7
10
|
import { FileStorageAdapter } from '../../../../lib/storage/file-storage.js';
|
|
8
|
-
import { getAuthorInfo } from '../../../../lib/user-preferences.js';
|
|
9
11
|
// Function to get global config API key as fallback
|
|
10
12
|
function getGlobalApiKey(provider) {
|
|
11
13
|
try {
|
|
@@ -51,18 +53,27 @@ export async function POST(c) {
|
|
|
51
53
|
? `${baseSystem}\n\nThe following persona is authoritative and overrides general guidance above. You must strictly follow it.\n\n${agentPersona}`
|
|
52
54
|
: baseSystem;
|
|
53
55
|
// Prepare tools for function calling
|
|
56
|
+
// Note: proposals is read-only, knowledge/project/architecture are read+write, events is read-only
|
|
54
57
|
const tools = enableTools ? [
|
|
55
58
|
{
|
|
56
59
|
type: "function",
|
|
57
|
-
function:
|
|
60
|
+
function: proposalsReadOnlyTool
|
|
58
61
|
},
|
|
59
62
|
{
|
|
60
63
|
type: "function",
|
|
61
|
-
function:
|
|
64
|
+
function: knowledgeTool
|
|
62
65
|
},
|
|
63
66
|
{
|
|
64
67
|
type: "function",
|
|
65
|
-
function:
|
|
68
|
+
function: eventsTool
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
type: "function",
|
|
72
|
+
function: projectContextTool
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
type: "function",
|
|
76
|
+
function: architectureContextTool
|
|
66
77
|
}
|
|
67
78
|
] : null;
|
|
68
79
|
const contextMessages = Array.isArray(attachedContextFiles)
|
|
@@ -70,7 +81,7 @@ export async function POST(c) {
|
|
|
70
81
|
.filter((file) => file && typeof file.content === 'string' && file.content.trim().length > 0)
|
|
71
82
|
.map((file) => ({
|
|
72
83
|
role: 'system',
|
|
73
|
-
content: `
|
|
84
|
+
content: `[ATTACHED CONTEXT - FULL CONTENT INCLUDED BELOW - DO NOT USE TOOLS TO FETCH THIS FILE]\nDocument: ${file.name || file.path || 'reference'}\nType: ${file.type || 'context'}\n\n--- BEGIN CONTENT ---\n${file.content}\n--- END CONTENT ---`
|
|
74
85
|
}))
|
|
75
86
|
: [];
|
|
76
87
|
// Compose the message list: system + history (preferred) or single message
|
|
@@ -121,9 +132,26 @@ export async function POST(c) {
|
|
|
121
132
|
body: JSON.stringify(requestBody),
|
|
122
133
|
});
|
|
123
134
|
if (!response.ok) {
|
|
124
|
-
const
|
|
125
|
-
console.error("OpenRouter API error:",
|
|
126
|
-
|
|
135
|
+
const errorText = await response.text();
|
|
136
|
+
console.error("OpenRouter API error:", {
|
|
137
|
+
status: response.status,
|
|
138
|
+
statusText: response.statusText,
|
|
139
|
+
error: errorText,
|
|
140
|
+
model: requestBody.model
|
|
141
|
+
});
|
|
142
|
+
// Try to parse error for more detail
|
|
143
|
+
let errorDetail = "Failed to get AI response";
|
|
144
|
+
try {
|
|
145
|
+
const errorJson = JSON.parse(errorText);
|
|
146
|
+
errorDetail = errorJson.error?.message || errorJson.error || errorDetail;
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// Use raw text if not JSON
|
|
150
|
+
if (errorText.length < 200) {
|
|
151
|
+
errorDetail = errorText || errorDetail;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return c.json({ error: errorDetail, status: response.status }, response.status);
|
|
127
155
|
}
|
|
128
156
|
// Handle streaming response
|
|
129
157
|
if (stream === true && response.body) {
|
|
@@ -139,7 +167,13 @@ export async function POST(c) {
|
|
|
139
167
|
const data = await response.json();
|
|
140
168
|
const aiMessage = data.choices?.[0]?.message;
|
|
141
169
|
if (!aiMessage) {
|
|
142
|
-
|
|
170
|
+
console.error("No AI message in response:", {
|
|
171
|
+
model: requestBody.model,
|
|
172
|
+
choices: data.choices,
|
|
173
|
+
error: data.error
|
|
174
|
+
});
|
|
175
|
+
const errorDetail = data.error?.message || data.error || "No response from AI model";
|
|
176
|
+
return c.json({ error: errorDetail }, 500);
|
|
143
177
|
}
|
|
144
178
|
// Handle tool calls
|
|
145
179
|
if (aiMessage.tool_calls && aiMessage.tool_calls.length > 0) {
|
|
@@ -198,20 +232,44 @@ async function executeToolCalls(toolCalls) {
|
|
|
198
232
|
try {
|
|
199
233
|
// Handle both function calling formats
|
|
200
234
|
const functionName = toolCall.function?.name || toolCall.name;
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
235
|
+
let functionArgs;
|
|
236
|
+
if (toolCall.function?.arguments) {
|
|
237
|
+
try {
|
|
238
|
+
functionArgs = JSON.parse(toolCall.function.arguments);
|
|
239
|
+
}
|
|
240
|
+
catch (parseError) {
|
|
241
|
+
// LLM may generate invalid JSON with unescaped characters in long content
|
|
242
|
+
// Return a helpful error message
|
|
243
|
+
const errorMsg = parseError instanceof Error ? parseError.message : 'Unknown parse error';
|
|
244
|
+
return {
|
|
245
|
+
tool_call_id: toolCall.id,
|
|
246
|
+
content: JSON.stringify({
|
|
247
|
+
success: false,
|
|
248
|
+
error: `Invalid tool arguments: ${errorMsg}. Please retry with simpler content.`
|
|
249
|
+
})
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
functionArgs = toolCall.arguments;
|
|
255
|
+
}
|
|
204
256
|
let result = { success: false, error: 'Unknown tool' };
|
|
205
257
|
// Execute tools directly instead of making HTTP requests
|
|
206
258
|
if (functionName === 'change_proposals') {
|
|
207
259
|
result = await executeProposalsToolDirect(functionArgs, storage);
|
|
208
260
|
}
|
|
209
|
-
else if (functionName === 'list_proposals') {
|
|
210
|
-
result = await executeListProposalsToolDirect(functionArgs, storage);
|
|
211
|
-
}
|
|
212
261
|
else if (functionName === 'knowledge_documents') {
|
|
213
262
|
result = await executeKnowledgeToolDirect(functionArgs);
|
|
214
263
|
}
|
|
264
|
+
else if (functionName === 'activity_events') {
|
|
265
|
+
result = await executeEventsToolDirect(functionArgs);
|
|
266
|
+
}
|
|
267
|
+
else if (functionName === 'project_context') {
|
|
268
|
+
result = await executeProjectContextToolDirect(functionArgs);
|
|
269
|
+
}
|
|
270
|
+
else if (functionName === 'architecture_context') {
|
|
271
|
+
result = await executeArchitectureContextToolDirect(functionArgs);
|
|
272
|
+
}
|
|
215
273
|
return {
|
|
216
274
|
tool_call_id: toolCall.id,
|
|
217
275
|
content: JSON.stringify({
|
|
@@ -234,8 +292,9 @@ async function executeToolCalls(toolCalls) {
|
|
|
234
292
|
});
|
|
235
293
|
return Promise.all(resultPromises);
|
|
236
294
|
}
|
|
295
|
+
// Proposals tool is READ-ONLY - only list and get operations are supported
|
|
237
296
|
async function executeProposalsToolDirect(args, storage) {
|
|
238
|
-
const { operation, id, filters
|
|
297
|
+
const { operation, id, filters } = args;
|
|
239
298
|
try {
|
|
240
299
|
switch (operation) {
|
|
241
300
|
case 'list': {
|
|
@@ -260,121 +319,8 @@ async function executeProposalsToolDirect(args, storage) {
|
|
|
260
319
|
message: `Retrieved proposal ${id}`
|
|
261
320
|
};
|
|
262
321
|
}
|
|
263
|
-
case 'create': {
|
|
264
|
-
if (!proposal) {
|
|
265
|
-
return { success: false, error: 'Proposal data is required for create operation' };
|
|
266
|
-
}
|
|
267
|
-
// Validate proposal data - check required fields
|
|
268
|
-
if (!proposal.intent || !proposal.content) {
|
|
269
|
-
return { success: false, error: 'Proposal intent and content are required' };
|
|
270
|
-
}
|
|
271
|
-
// Get author info
|
|
272
|
-
const author = await getAuthorInfo();
|
|
273
|
-
// Normalize and validate incoming proposal
|
|
274
|
-
const validatedProposal = validateProposalData(proposal);
|
|
275
|
-
// Normalize plan steps: accept strings or objects
|
|
276
|
-
const planSteps = (validatedProposal.planSteps || []).map((step, index) => {
|
|
277
|
-
if (typeof step === 'string') {
|
|
278
|
-
return {
|
|
279
|
-
id: `step-${index + 1}`,
|
|
280
|
-
description: step,
|
|
281
|
-
status: 'pending'
|
|
282
|
-
};
|
|
283
|
-
}
|
|
284
|
-
return {
|
|
285
|
-
id: step.id || `step-${index + 1}`,
|
|
286
|
-
description: step.description || '',
|
|
287
|
-
status: step.status || 'pending',
|
|
288
|
-
command: step.command,
|
|
289
|
-
expectedOutcome: step.expectedOutcome,
|
|
290
|
-
output: step.output,
|
|
291
|
-
error: step.error,
|
|
292
|
-
executedAt: step.executedAt
|
|
293
|
-
};
|
|
294
|
-
});
|
|
295
|
-
const now = new Date();
|
|
296
|
-
const newProposal = {
|
|
297
|
-
id: validatedProposal.id || `cp-${Date.now()}`,
|
|
298
|
-
intent: validatedProposal.intent || '',
|
|
299
|
-
content: validatedProposal.content || '',
|
|
300
|
-
author: {
|
|
301
|
-
id: validatedProposal.author?.id || 'current-user',
|
|
302
|
-
name: validatedProposal.author?.name || author.name || 'Unknown User',
|
|
303
|
-
email: validatedProposal.author?.email || author.email || '',
|
|
304
|
-
type: validatedProposal.author?.type || 'human'
|
|
305
|
-
},
|
|
306
|
-
planSteps,
|
|
307
|
-
evidence: validatedProposal.evidence || [],
|
|
308
|
-
policies: validatedProposal.policies || [],
|
|
309
|
-
featureFlags: validatedProposal.featureFlags || [],
|
|
310
|
-
experiments: validatedProposal.experiments || [],
|
|
311
|
-
telemetryContracts: validatedProposal.telemetryContracts || [],
|
|
312
|
-
releasePlan: validatedProposal.releasePlan || { strategy: 'immediate' },
|
|
313
|
-
status: validatedProposal.status || 'draft',
|
|
314
|
-
metadata: {
|
|
315
|
-
createdAt: now,
|
|
316
|
-
updatedAt: now,
|
|
317
|
-
reviewers: validatedProposal.metadata?.reviewers || [],
|
|
318
|
-
aiInteractions: validatedProposal.metadata?.aiInteractions || [],
|
|
319
|
-
tags: validatedProposal.metadata?.tags || [],
|
|
320
|
-
priority: validatedProposal.metadata?.priority || 'medium'
|
|
321
|
-
},
|
|
322
|
-
productSpecRef: validatedProposal.productSpecRef
|
|
323
|
-
};
|
|
324
|
-
await storage.createCP(newProposal);
|
|
325
|
-
return {
|
|
326
|
-
success: true,
|
|
327
|
-
data: newProposal,
|
|
328
|
-
message: `Created proposal ${newProposal.id} (${newProposal.intent})`
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
case 'update': {
|
|
332
|
-
if (!id || !proposal) {
|
|
333
|
-
return { success: false, error: 'Proposal ID and data are required for update operation' };
|
|
334
|
-
}
|
|
335
|
-
// Validate proposal data - check required fields
|
|
336
|
-
if (!proposal.intent || !proposal.content) {
|
|
337
|
-
return { success: false, error: 'Proposal intent and content are required' };
|
|
338
|
-
}
|
|
339
|
-
// Get existing proposal to preserve author and creation date
|
|
340
|
-
const existingProposal = await storage.getCP(id);
|
|
341
|
-
if (!existingProposal) {
|
|
342
|
-
return { success: false, error: 'Proposal not found' };
|
|
343
|
-
}
|
|
344
|
-
// Update the proposal
|
|
345
|
-
const proposalToUpdate = {
|
|
346
|
-
...proposal,
|
|
347
|
-
author: existingProposal.author, // Preserve original author
|
|
348
|
-
metadata: {
|
|
349
|
-
...existingProposal.metadata,
|
|
350
|
-
...proposal.metadata,
|
|
351
|
-
updatedAt: new Date().toISOString()
|
|
352
|
-
}
|
|
353
|
-
};
|
|
354
|
-
await storage.updateCP(id, proposalToUpdate);
|
|
355
|
-
return {
|
|
356
|
-
success: true,
|
|
357
|
-
data: proposalToUpdate,
|
|
358
|
-
message: `Updated proposal ${id}`
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
|
-
case 'delete': {
|
|
362
|
-
if (!id) {
|
|
363
|
-
return { success: false, error: 'Proposal ID is required for delete operation' };
|
|
364
|
-
}
|
|
365
|
-
// Check if proposal exists before deleting
|
|
366
|
-
const existingProposal = await storage.getCP(id);
|
|
367
|
-
if (!existingProposal) {
|
|
368
|
-
return { success: false, error: 'Proposal not found' };
|
|
369
|
-
}
|
|
370
|
-
await storage.deleteCP(id);
|
|
371
|
-
return {
|
|
372
|
-
success: true,
|
|
373
|
-
message: `Deleted proposal ${id}`
|
|
374
|
-
};
|
|
375
|
-
}
|
|
376
322
|
default:
|
|
377
|
-
return { success: false, error: `
|
|
323
|
+
return { success: false, 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.` };
|
|
378
324
|
}
|
|
379
325
|
}
|
|
380
326
|
catch (error) {
|
|
@@ -382,49 +328,21 @@ async function executeProposalsToolDirect(args, storage) {
|
|
|
382
328
|
return { success: false, error: error.message || 'Tool execution failed' };
|
|
383
329
|
}
|
|
384
330
|
}
|
|
385
|
-
async function executeListProposalsToolDirect(args, storage) {
|
|
386
|
-
const { filters } = args;
|
|
387
|
-
try {
|
|
388
|
-
const proposals = await storage.listCPs(filters || {});
|
|
389
|
-
// Return only metadata, not full content
|
|
390
|
-
const metadataOnly = proposals.map(proposal => ({
|
|
391
|
-
id: proposal.id,
|
|
392
|
-
intent: proposal.intent,
|
|
393
|
-
status: proposal.status,
|
|
394
|
-
priority: proposal.metadata?.priority || 'medium',
|
|
395
|
-
tags: proposal.metadata?.tags || [],
|
|
396
|
-
author: {
|
|
397
|
-
name: proposal.author.name,
|
|
398
|
-
email: proposal.author.email
|
|
399
|
-
},
|
|
400
|
-
createdAt: proposal.metadata.createdAt,
|
|
401
|
-
updatedAt: proposal.metadata.updatedAt,
|
|
402
|
-
reviewers: proposal.metadata.reviewers || [],
|
|
403
|
-
productSpecRef: proposal.productSpecRef
|
|
404
|
-
}));
|
|
405
|
-
return {
|
|
406
|
-
success: true,
|
|
407
|
-
data: metadataOnly,
|
|
408
|
-
message: `Found ${metadataOnly.length} proposals`
|
|
409
|
-
};
|
|
410
|
-
}
|
|
411
|
-
catch (error) {
|
|
412
|
-
console.error('Error executing list proposals tool:', error);
|
|
413
|
-
return { success: false, error: error.message || 'Failed to list proposals' };
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
331
|
async function executeKnowledgeToolDirect(args) {
|
|
417
|
-
const { operation, filename, title, content, metadata, summary } = args;
|
|
332
|
+
const { operation, filename, title, content, metadata, summary, query } = args;
|
|
418
333
|
try {
|
|
419
334
|
const knowledgePath = getKnowledgeBasePath();
|
|
420
335
|
await fs.mkdir(knowledgePath, { recursive: true });
|
|
421
336
|
switch (operation) {
|
|
422
337
|
case 'list': {
|
|
423
|
-
const
|
|
338
|
+
const searchQuery = query && typeof query === 'string' ? query : undefined;
|
|
339
|
+
const documents = await listKnowledgeDocuments(knowledgePath, searchQuery);
|
|
424
340
|
return {
|
|
425
341
|
success: true,
|
|
426
342
|
data: documents,
|
|
427
|
-
message:
|
|
343
|
+
message: searchQuery
|
|
344
|
+
? `Found ${documents.length} knowledge documents matching "${searchQuery}"`
|
|
345
|
+
: `Found ${documents.length} knowledge documents`
|
|
428
346
|
};
|
|
429
347
|
}
|
|
430
348
|
case 'get': {
|
|
@@ -563,6 +481,315 @@ async function executeKnowledgeToolDirect(args) {
|
|
|
563
481
|
return { success: false, error: error.message || 'Knowledge tool execution failed' };
|
|
564
482
|
}
|
|
565
483
|
}
|
|
484
|
+
// Events tool is READ-ONLY - returns recent activity events
|
|
485
|
+
async function executeEventsToolDirect(args) {
|
|
486
|
+
const { operation, limit = 20, kind } = args;
|
|
487
|
+
if (operation !== 'list') {
|
|
488
|
+
return {
|
|
489
|
+
success: false,
|
|
490
|
+
error: `Events are read-only. Only 'list' operation is supported.`
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
try {
|
|
494
|
+
const eventsDir = getLogsDir();
|
|
495
|
+
const currentFile = join(eventsDir, 'events-current.jsonl');
|
|
496
|
+
// Check if file exists
|
|
497
|
+
try {
|
|
498
|
+
await fs.access(currentFile);
|
|
499
|
+
}
|
|
500
|
+
catch {
|
|
501
|
+
return {
|
|
502
|
+
success: true,
|
|
503
|
+
data: [],
|
|
504
|
+
message: 'No events found'
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
// Read all events from file
|
|
508
|
+
const allEvents = [];
|
|
509
|
+
const fileStream = createReadStream(currentFile);
|
|
510
|
+
const rl = readline.createInterface({
|
|
511
|
+
input: fileStream,
|
|
512
|
+
crlfDelay: Infinity,
|
|
513
|
+
});
|
|
514
|
+
for await (const line of rl) {
|
|
515
|
+
if (!line.trim())
|
|
516
|
+
continue;
|
|
517
|
+
try {
|
|
518
|
+
const event = JSON.parse(line);
|
|
519
|
+
// Apply kind filter if specified
|
|
520
|
+
if (kind && typeof kind === 'string') {
|
|
521
|
+
if (!event.kind || !event.kind.startsWith(kind)) {
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
allEvents.push(event);
|
|
526
|
+
}
|
|
527
|
+
catch {
|
|
528
|
+
// Skip malformed lines
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
// Sort by timestamp descending (most recent first) and limit
|
|
533
|
+
const cappedLimit = Math.min(Math.max(1, limit), 100);
|
|
534
|
+
const sortedEvents = allEvents
|
|
535
|
+
.sort((a, b) => {
|
|
536
|
+
// Sort by seq descending (higher seq = more recent)
|
|
537
|
+
return (b.seq || 0) - (a.seq || 0);
|
|
538
|
+
})
|
|
539
|
+
.slice(0, cappedLimit);
|
|
540
|
+
// Format events for readability
|
|
541
|
+
const formattedEvents = sortedEvents.map(event => ({
|
|
542
|
+
seq: event.seq,
|
|
543
|
+
kind: event.kind,
|
|
544
|
+
actor: event.actor,
|
|
545
|
+
subject: event.subject,
|
|
546
|
+
timestamp: event.ts,
|
|
547
|
+
level: event.level,
|
|
548
|
+
tags: event.tags,
|
|
549
|
+
summary: event.payload?.summary || event.payload?.message || event.payload?.title || undefined
|
|
550
|
+
}));
|
|
551
|
+
return {
|
|
552
|
+
success: true,
|
|
553
|
+
data: formattedEvents,
|
|
554
|
+
message: kind
|
|
555
|
+
? `Found ${formattedEvents.length} recent '${kind}' events`
|
|
556
|
+
: `Found ${formattedEvents.length} recent events`
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
catch (error) {
|
|
560
|
+
console.error('Error executing events tool:', error);
|
|
561
|
+
return { success: false, error: error.message || 'Events tool execution failed' };
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
// Project context tool - read/write the project definition document
|
|
565
|
+
async function executeProjectContextToolDirect(args) {
|
|
566
|
+
const { operation, content, old_text, new_text } = args;
|
|
567
|
+
try {
|
|
568
|
+
const contextPath = getContextBasePath();
|
|
569
|
+
const filePath = join(contextPath, 'project.md');
|
|
570
|
+
switch (operation) {
|
|
571
|
+
case 'get': {
|
|
572
|
+
try {
|
|
573
|
+
const fileContent = await fs.readFile(filePath, 'utf-8');
|
|
574
|
+
const parsed = matter(fileContent);
|
|
575
|
+
return {
|
|
576
|
+
success: true,
|
|
577
|
+
data: {
|
|
578
|
+
content: parsed.content,
|
|
579
|
+
frontmatter: parsed.data,
|
|
580
|
+
raw: fileContent
|
|
581
|
+
},
|
|
582
|
+
message: 'Retrieved project context'
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
catch (err) {
|
|
586
|
+
if (err.code === 'ENOENT') {
|
|
587
|
+
return {
|
|
588
|
+
success: true,
|
|
589
|
+
data: { content: '', frontmatter: {}, raw: '' },
|
|
590
|
+
message: 'Project context document does not exist yet. You can create it with an update operation.'
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
throw err;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
case 'append': {
|
|
597
|
+
if (!content) {
|
|
598
|
+
return { success: false, error: 'Content is required for append operation' };
|
|
599
|
+
}
|
|
600
|
+
await fs.mkdir(contextPath, { recursive: true });
|
|
601
|
+
// Read existing content if file exists
|
|
602
|
+
let existingContent = '';
|
|
603
|
+
try {
|
|
604
|
+
existingContent = await fs.readFile(filePath, 'utf-8');
|
|
605
|
+
}
|
|
606
|
+
catch (err) {
|
|
607
|
+
if (err.code !== 'ENOENT')
|
|
608
|
+
throw err;
|
|
609
|
+
}
|
|
610
|
+
// Append new content with a newline separator
|
|
611
|
+
const newContent = existingContent
|
|
612
|
+
? existingContent.trimEnd() + '\n\n' + content
|
|
613
|
+
: content;
|
|
614
|
+
await fs.writeFile(filePath, newContent, 'utf-8');
|
|
615
|
+
return {
|
|
616
|
+
success: true,
|
|
617
|
+
message: 'Appended content to project context document'
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
case 'replace_section': {
|
|
621
|
+
if (!old_text || !new_text) {
|
|
622
|
+
return { success: false, error: 'Both old_text and new_text are required for replace_section operation' };
|
|
623
|
+
}
|
|
624
|
+
// Read existing content
|
|
625
|
+
let existingContent = '';
|
|
626
|
+
try {
|
|
627
|
+
existingContent = await fs.readFile(filePath, 'utf-8');
|
|
628
|
+
}
|
|
629
|
+
catch (err) {
|
|
630
|
+
if (err.code === 'ENOENT') {
|
|
631
|
+
return { success: false, error: 'Project context document does not exist. Use append or update to create it first.' };
|
|
632
|
+
}
|
|
633
|
+
throw err;
|
|
634
|
+
}
|
|
635
|
+
// Check if old_text exists in the document
|
|
636
|
+
if (!existingContent.includes(old_text)) {
|
|
637
|
+
return {
|
|
638
|
+
success: false,
|
|
639
|
+
error: 'Could not find the specified text in the document. The text may have been paraphrased or changed.',
|
|
640
|
+
fallback_markdown: new_text,
|
|
641
|
+
suggestion: 'Here is the replacement text. You can copy it and manually edit the document at /context/project'
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
// Replace the text
|
|
645
|
+
const updatedContent = existingContent.replace(old_text, new_text);
|
|
646
|
+
await fs.writeFile(filePath, updatedContent, 'utf-8');
|
|
647
|
+
return {
|
|
648
|
+
success: true,
|
|
649
|
+
message: 'Replaced section in project context document'
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
case 'update': {
|
|
653
|
+
if (!content) {
|
|
654
|
+
return { success: false, error: 'Content is required for update operation' };
|
|
655
|
+
}
|
|
656
|
+
await fs.mkdir(contextPath, { recursive: true });
|
|
657
|
+
await fs.writeFile(filePath, content, 'utf-8');
|
|
658
|
+
return {
|
|
659
|
+
success: true,
|
|
660
|
+
message: 'Updated project context document'
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
default:
|
|
664
|
+
return { success: false, error: `Unknown operation: ${operation}. Use 'get', 'append', 'replace_section', or 'update'.` };
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
catch (error) {
|
|
668
|
+
console.error('Error executing project context tool:', error);
|
|
669
|
+
return { success: false, error: error.message || 'Project context tool execution failed' };
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
// Architecture context tool - read/write the architecture document
|
|
673
|
+
async function executeArchitectureContextToolDirect(args) {
|
|
674
|
+
const { operation, content, old_text, new_text } = args;
|
|
675
|
+
try {
|
|
676
|
+
const contextPath = getContextBasePath();
|
|
677
|
+
const filePath = join(contextPath, 'architecture.md');
|
|
678
|
+
switch (operation) {
|
|
679
|
+
case 'get': {
|
|
680
|
+
try {
|
|
681
|
+
const fileContent = await fs.readFile(filePath, 'utf-8');
|
|
682
|
+
const parsed = matter(fileContent);
|
|
683
|
+
return {
|
|
684
|
+
success: true,
|
|
685
|
+
data: {
|
|
686
|
+
content: parsed.content,
|
|
687
|
+
frontmatter: parsed.data,
|
|
688
|
+
raw: fileContent
|
|
689
|
+
},
|
|
690
|
+
message: 'Retrieved architecture context'
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
catch (err) {
|
|
694
|
+
if (err.code === 'ENOENT') {
|
|
695
|
+
return {
|
|
696
|
+
success: true,
|
|
697
|
+
data: { content: '', frontmatter: {}, raw: '' },
|
|
698
|
+
message: 'Architecture context document does not exist yet. You can create it with an update operation.'
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
throw err;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
case 'append': {
|
|
705
|
+
if (!content) {
|
|
706
|
+
return { success: false, error: 'Content is required for append operation' };
|
|
707
|
+
}
|
|
708
|
+
await fs.mkdir(contextPath, { recursive: true });
|
|
709
|
+
// Read existing content if file exists
|
|
710
|
+
let existingContent = '';
|
|
711
|
+
try {
|
|
712
|
+
existingContent = await fs.readFile(filePath, 'utf-8');
|
|
713
|
+
}
|
|
714
|
+
catch (err) {
|
|
715
|
+
if (err.code !== 'ENOENT')
|
|
716
|
+
throw err;
|
|
717
|
+
}
|
|
718
|
+
// Append new content with a newline separator
|
|
719
|
+
const newContent = existingContent
|
|
720
|
+
? existingContent.trimEnd() + '\n\n' + content
|
|
721
|
+
: content;
|
|
722
|
+
await fs.writeFile(filePath, newContent, 'utf-8');
|
|
723
|
+
return {
|
|
724
|
+
success: true,
|
|
725
|
+
message: 'Appended content to architecture context document'
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
case 'replace_section': {
|
|
729
|
+
if (!old_text || !new_text) {
|
|
730
|
+
return { success: false, error: 'Both old_text and new_text are required for replace_section operation' };
|
|
731
|
+
}
|
|
732
|
+
// Read existing content
|
|
733
|
+
let existingContent = '';
|
|
734
|
+
try {
|
|
735
|
+
existingContent = await fs.readFile(filePath, 'utf-8');
|
|
736
|
+
}
|
|
737
|
+
catch (err) {
|
|
738
|
+
if (err.code === 'ENOENT') {
|
|
739
|
+
return { success: false, error: 'Architecture context document does not exist. Use append or update to create it first.' };
|
|
740
|
+
}
|
|
741
|
+
throw err;
|
|
742
|
+
}
|
|
743
|
+
// Check if old_text exists in the document
|
|
744
|
+
if (!existingContent.includes(old_text)) {
|
|
745
|
+
return {
|
|
746
|
+
success: false,
|
|
747
|
+
error: 'Could not find the specified text in the document. The text may have been paraphrased or changed.',
|
|
748
|
+
fallback_markdown: new_text,
|
|
749
|
+
suggestion: 'Here is the replacement text. You can copy it and manually edit the document at /context/architecture'
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
// Replace the text
|
|
753
|
+
const updatedContent = existingContent.replace(old_text, new_text);
|
|
754
|
+
await fs.writeFile(filePath, updatedContent, 'utf-8');
|
|
755
|
+
return {
|
|
756
|
+
success: true,
|
|
757
|
+
message: 'Replaced section in architecture context document'
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
case 'update': {
|
|
761
|
+
if (!content) {
|
|
762
|
+
return { success: false, error: 'Content is required for update operation' };
|
|
763
|
+
}
|
|
764
|
+
await fs.mkdir(contextPath, { recursive: true });
|
|
765
|
+
await fs.writeFile(filePath, content, 'utf-8');
|
|
766
|
+
return {
|
|
767
|
+
success: true,
|
|
768
|
+
message: 'Updated architecture context document'
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
default:
|
|
772
|
+
return { success: false, error: `Unknown operation: ${operation}. Use 'get', 'append', 'replace_section', or 'update'.` };
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
catch (error) {
|
|
776
|
+
console.error('Error executing architecture context tool:', error);
|
|
777
|
+
return { success: false, error: error.message || 'Architecture context tool execution failed' };
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
function getContextBasePath() {
|
|
781
|
+
let basePath;
|
|
782
|
+
if (process.env.NODE_ENV === 'development' && process.env.GAIT_DEV_ROOT) {
|
|
783
|
+
basePath = process.env.GAIT_DEV_ROOT;
|
|
784
|
+
}
|
|
785
|
+
else if (process.env.GAIT_DATA_PATH) {
|
|
786
|
+
basePath = pathResolve(process.env.GAIT_DATA_PATH, '.nut');
|
|
787
|
+
}
|
|
788
|
+
else {
|
|
789
|
+
basePath = pathResolve(process.cwd(), '.nut');
|
|
790
|
+
}
|
|
791
|
+
return join(basePath, 'context');
|
|
792
|
+
}
|
|
566
793
|
function getKnowledgeBasePath() {
|
|
567
794
|
let basePath;
|
|
568
795
|
if (process.env.NODE_ENV === 'development' && process.env.GAIT_DEV_ROOT) {
|
|
@@ -620,7 +847,54 @@ async function readKnowledgeDocument(basePath, filename) {
|
|
|
620
847
|
content
|
|
621
848
|
};
|
|
622
849
|
}
|
|
623
|
-
|
|
850
|
+
function extractSnippet(content, query, maxLength = 200) {
|
|
851
|
+
const lowerContent = content.toLowerCase();
|
|
852
|
+
const lowerQuery = query.toLowerCase();
|
|
853
|
+
const terms = lowerQuery.split(/\s+/);
|
|
854
|
+
// Find the first occurrence of any search term
|
|
855
|
+
let bestIndex = -1;
|
|
856
|
+
let matchedTerm = '';
|
|
857
|
+
for (const term of terms) {
|
|
858
|
+
const index = lowerContent.indexOf(term);
|
|
859
|
+
if (index !== -1 && (bestIndex === -1 || index < bestIndex)) {
|
|
860
|
+
bestIndex = index;
|
|
861
|
+
matchedTerm = term;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
if (bestIndex === -1) {
|
|
865
|
+
// No match found, return beginning of content
|
|
866
|
+
return content.slice(0, maxLength) + (content.length > maxLength ? '...' : '');
|
|
867
|
+
}
|
|
868
|
+
// Extract snippet around the match
|
|
869
|
+
const start = Math.max(0, bestIndex - 60);
|
|
870
|
+
const end = Math.min(content.length, bestIndex + matchedTerm.length + 140);
|
|
871
|
+
let snippet = content.slice(start, end);
|
|
872
|
+
// Add ellipsis if needed
|
|
873
|
+
if (start > 0)
|
|
874
|
+
snippet = '...' + snippet;
|
|
875
|
+
if (end < content.length)
|
|
876
|
+
snippet = snippet + '...';
|
|
877
|
+
return snippet;
|
|
878
|
+
}
|
|
879
|
+
function findMatchedFields(doc, query) {
|
|
880
|
+
const lowerQuery = query.toLowerCase();
|
|
881
|
+
const terms = lowerQuery.split(/\s+/);
|
|
882
|
+
const matched = [];
|
|
883
|
+
const checkField = (value, fieldName) => {
|
|
884
|
+
if (value && terms.some(term => value.toLowerCase().includes(term))) {
|
|
885
|
+
matched.push(fieldName);
|
|
886
|
+
}
|
|
887
|
+
};
|
|
888
|
+
checkField(doc.title, 'title');
|
|
889
|
+
checkField(doc.content, 'content');
|
|
890
|
+
checkField(doc.metadata?.category, 'category');
|
|
891
|
+
checkField(doc.metadata?.summary, 'summary');
|
|
892
|
+
if (doc.metadata?.tags?.some(tag => terms.some(term => tag.toLowerCase().includes(term)))) {
|
|
893
|
+
matched.push('tags');
|
|
894
|
+
}
|
|
895
|
+
return matched;
|
|
896
|
+
}
|
|
897
|
+
async function listKnowledgeDocuments(basePath, query) {
|
|
624
898
|
const files = await fs.readdir(basePath);
|
|
625
899
|
const markdownFiles = files.filter(file => file.endsWith('.md'));
|
|
626
900
|
const documents = await Promise.all(markdownFiles.map(async (file) => {
|
|
@@ -632,9 +906,33 @@ async function listKnowledgeDocuments(basePath) {
|
|
|
632
906
|
return null;
|
|
633
907
|
}
|
|
634
908
|
}));
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
909
|
+
const validDocs = documents.filter((doc) => doc !== null);
|
|
910
|
+
// If no query, return all documents sorted by filename
|
|
911
|
+
if (!query?.trim()) {
|
|
912
|
+
return validDocs.sort((a, b) => a.filename.localeCompare(b.filename));
|
|
913
|
+
}
|
|
914
|
+
// Use Fuse.js for fuzzy search
|
|
915
|
+
const fuse = new Fuse(validDocs, {
|
|
916
|
+
keys: [
|
|
917
|
+
{ name: 'title', weight: 2 },
|
|
918
|
+
{ name: 'content', weight: 1 },
|
|
919
|
+
{ name: 'metadata.summary', weight: 1.5 },
|
|
920
|
+
{ name: 'metadata.category', weight: 1 },
|
|
921
|
+
{ name: 'metadata.tags', weight: 1.5 }
|
|
922
|
+
],
|
|
923
|
+
threshold: 0.4,
|
|
924
|
+
includeScore: true,
|
|
925
|
+
ignoreLocation: true,
|
|
926
|
+
minMatchCharLength: 2
|
|
927
|
+
});
|
|
928
|
+
const results = fuse.search(query);
|
|
929
|
+
// Enhance results with snippets and matched fields
|
|
930
|
+
return results.map(result => ({
|
|
931
|
+
...result.item,
|
|
932
|
+
snippet: extractSnippet(result.item.content, query),
|
|
933
|
+
matchedIn: findMatchedFields(result.item, query),
|
|
934
|
+
score: result.score
|
|
935
|
+
}));
|
|
638
936
|
}
|
|
639
937
|
function readSharedPrompt(name) {
|
|
640
938
|
// Try multiple candidate paths depending on cwd when running via Turbo
|