@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.
@@ -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 { proposalsTool, listProposalsTool, validateProposalData, knowledgeTool, normalizeKnowledgeMetadata } from '@lovelybunch/mcp';
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: proposalsTool,
47
- list_proposals: listProposalsTool,
48
- knowledge_documents: knowledgeTool
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: proposalsTool,
71
- list_proposals: listProposalsTool,
72
- knowledge_documents: knowledgeTool
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, proposal } = args;
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({ success: false, error: 'Invalid operation' }, 400);
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 documents = await listKnowledgeDocuments(knowledgePath);
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: `Found ${documents.length} knowledge documents`
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
- async function listKnowledgeDocuments(basePath) {
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
- const doc = await readKnowledgeDocument(basePath, file);
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
- return documents
513
- .filter((doc) => doc !== null)
514
- .sort((a, b) => a.filename.localeCompare(b.filename));
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
@@ -14,6 +14,6 @@ export declare function POST(c: Context): Promise<(Response & import("hono").Typ
14
14
  success: false;
15
15
  error: {
16
16
  code: string;
17
- message: any;
17
+ message: string;
18
18
  };
19
19
  }, 500, "json">)>;