@lovelybunch/api 1.0.53 → 1.0.55
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/routes/api/v1/ai/route.js +245 -4
- package/dist/routes/api/v1/mcp/index.js +221 -3
- package/package.json +4 -4
- package/static/assets/{index-BmF_6BRj.js → index-CXdqQvuB.js} +195 -184
- package/static/assets/index-DthyPP76.css +33 -0
- package/static/assets/index-f9JbHO5w.js +755 -0
- package/static/index.html +2 -2
- package/static/assets/index-D_1UlOMs.css +0 -33
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { homedir } from 'os';
|
|
2
|
-
import { join } from 'path';
|
|
3
|
-
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { join, resolve as pathResolve, basename } from 'path';
|
|
3
|
+
import { existsSync, readFileSync, promises as fs } from 'fs';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
|
-
import { proposalsTool, listProposalsTool, validateProposalData } from '@lovelybunch/mcp';
|
|
5
|
+
import { proposalsTool, listProposalsTool, validateProposalData, knowledgeTool, normalizeKnowledgeMetadata } from '@lovelybunch/mcp';
|
|
6
|
+
import matter from 'gray-matter';
|
|
6
7
|
import { FileStorageAdapter } from '../../../../lib/storage/file-storage.js';
|
|
7
8
|
import { getAuthorInfo } from '../../../../lib/user-preferences.js';
|
|
8
9
|
// Function to get global config API key as fallback
|
|
@@ -33,7 +34,7 @@ function getGlobalApiKey(provider) {
|
|
|
33
34
|
}
|
|
34
35
|
export async function POST(c) {
|
|
35
36
|
try {
|
|
36
|
-
const { message: userMessage, history, model, context, contextContent, agentPersona, systemPrompt: systemOverride, maxTokens, enableTools } = await c.req.json();
|
|
37
|
+
const { message: userMessage, history, model, context, contextContent, agentPersona, systemPrompt: systemOverride, maxTokens, enableTools, attachedContextFiles } = await c.req.json();
|
|
37
38
|
if (!userMessage) {
|
|
38
39
|
return c.json({ error: "Message is required" }, 400);
|
|
39
40
|
}
|
|
@@ -58,8 +59,20 @@ export async function POST(c) {
|
|
|
58
59
|
{
|
|
59
60
|
type: "function",
|
|
60
61
|
function: proposalsTool
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
type: "function",
|
|
65
|
+
function: knowledgeTool
|
|
61
66
|
}
|
|
62
67
|
] : null;
|
|
68
|
+
const contextMessages = Array.isArray(attachedContextFiles)
|
|
69
|
+
? attachedContextFiles
|
|
70
|
+
.filter((file) => file && typeof file.content === 'string' && file.content.trim().length > 0)
|
|
71
|
+
.map((file) => ({
|
|
72
|
+
role: 'system',
|
|
73
|
+
content: `Context from ${file.name || file.path || 'reference'}:\n${file.content}`
|
|
74
|
+
}))
|
|
75
|
+
: [];
|
|
63
76
|
// Compose the message list: system + history (preferred) or single message
|
|
64
77
|
let messagesPayload;
|
|
65
78
|
if (Array.isArray(history) && history.length > 0) {
|
|
@@ -67,12 +80,14 @@ export async function POST(c) {
|
|
|
67
80
|
const sanitized = history.filter(m => m && (m.role === 'user' || m.role === 'assistant') && typeof m.content === 'string');
|
|
68
81
|
messagesPayload = [
|
|
69
82
|
{ role: 'system', content: systemPrompt },
|
|
83
|
+
...contextMessages,
|
|
70
84
|
...sanitized,
|
|
71
85
|
];
|
|
72
86
|
}
|
|
73
87
|
else {
|
|
74
88
|
messagesPayload = [
|
|
75
89
|
{ role: 'system', content: systemPrompt },
|
|
90
|
+
...contextMessages,
|
|
76
91
|
{ role: 'user', content: userMessage },
|
|
77
92
|
];
|
|
78
93
|
}
|
|
@@ -181,6 +196,9 @@ async function executeToolCalls(toolCalls) {
|
|
|
181
196
|
else if (functionName === 'list_proposals') {
|
|
182
197
|
result = await executeListProposalsToolDirect(functionArgs, storage);
|
|
183
198
|
}
|
|
199
|
+
else if (functionName === 'knowledge_documents') {
|
|
200
|
+
result = await executeKnowledgeToolDirect(functionArgs);
|
|
201
|
+
}
|
|
184
202
|
results.push({
|
|
185
203
|
tool_call_id: toolCall.id,
|
|
186
204
|
content: JSON.stringify({
|
|
@@ -382,6 +400,229 @@ async function executeListProposalsToolDirect(args, storage) {
|
|
|
382
400
|
return { success: false, error: error.message || 'Failed to list proposals' };
|
|
383
401
|
}
|
|
384
402
|
}
|
|
403
|
+
async function executeKnowledgeToolDirect(args) {
|
|
404
|
+
const { operation, filename, title, content, metadata, summary } = args;
|
|
405
|
+
try {
|
|
406
|
+
const knowledgePath = getKnowledgeBasePath();
|
|
407
|
+
await fs.mkdir(knowledgePath, { recursive: true });
|
|
408
|
+
switch (operation) {
|
|
409
|
+
case 'list': {
|
|
410
|
+
const documents = await listKnowledgeDocuments(knowledgePath);
|
|
411
|
+
return {
|
|
412
|
+
success: true,
|
|
413
|
+
data: documents,
|
|
414
|
+
message: `Found ${documents.length} knowledge documents`
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
case 'get': {
|
|
418
|
+
if (!filename) {
|
|
419
|
+
return { success: false, error: 'Filename is required for get operation' };
|
|
420
|
+
}
|
|
421
|
+
const document = await readKnowledgeDocument(knowledgePath, filename);
|
|
422
|
+
if (!document) {
|
|
423
|
+
return { success: false, error: 'Knowledge document not found' };
|
|
424
|
+
}
|
|
425
|
+
return {
|
|
426
|
+
success: true,
|
|
427
|
+
data: document,
|
|
428
|
+
message: `Loaded knowledge document ${document.filename}`
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
case 'create': {
|
|
432
|
+
if (!title || !content) {
|
|
433
|
+
return { success: false, error: 'Title and content are required for create operation' };
|
|
434
|
+
}
|
|
435
|
+
const targetFilename = resolveKnowledgeFilename(filename, title);
|
|
436
|
+
const filePath = join(knowledgePath, targetFilename);
|
|
437
|
+
try {
|
|
438
|
+
await fs.access(filePath);
|
|
439
|
+
return { success: false, error: 'A knowledge document with this filename already exists' };
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
// File does not exist
|
|
443
|
+
}
|
|
444
|
+
const normalizedMetadata = normalizeKnowledgeMetadata({
|
|
445
|
+
version: '1.0',
|
|
446
|
+
summary,
|
|
447
|
+
...metadata
|
|
448
|
+
});
|
|
449
|
+
const markdown = matter.stringify(content, normalizedMetadata);
|
|
450
|
+
await fs.writeFile(filePath, markdown, 'utf-8');
|
|
451
|
+
return {
|
|
452
|
+
success: true,
|
|
453
|
+
data: {
|
|
454
|
+
filename: targetFilename,
|
|
455
|
+
title,
|
|
456
|
+
metadata: normalizedMetadata,
|
|
457
|
+
content
|
|
458
|
+
},
|
|
459
|
+
message: `Created knowledge document ${targetFilename}`
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
case 'update': {
|
|
463
|
+
if (!filename || !content) {
|
|
464
|
+
return { success: false, error: 'Filename and content are required for update operation' };
|
|
465
|
+
}
|
|
466
|
+
const currentFilename = resolveKnowledgeFilename(filename, title || 'knowledge-entry');
|
|
467
|
+
const currentPath = join(knowledgePath, currentFilename);
|
|
468
|
+
let existingDocument = null;
|
|
469
|
+
try {
|
|
470
|
+
existingDocument = await readKnowledgeDocument(knowledgePath, currentFilename);
|
|
471
|
+
}
|
|
472
|
+
catch (error) {
|
|
473
|
+
if (error.code === 'ENOENT') {
|
|
474
|
+
return { success: false, error: 'Knowledge document not found' };
|
|
475
|
+
}
|
|
476
|
+
throw error;
|
|
477
|
+
}
|
|
478
|
+
if (!existingDocument) {
|
|
479
|
+
return { success: false, error: 'Knowledge document not found' };
|
|
480
|
+
}
|
|
481
|
+
const nextTitle = title || existingDocument.title;
|
|
482
|
+
const shouldRename = Boolean(title && slugify(title) !== slugify(existingDocument.title));
|
|
483
|
+
const nextFilename = shouldRename ? resolveKnowledgeFilename(undefined, nextTitle) : currentFilename;
|
|
484
|
+
const nextPath = join(knowledgePath, nextFilename);
|
|
485
|
+
if (nextFilename !== currentFilename) {
|
|
486
|
+
try {
|
|
487
|
+
await fs.access(nextPath);
|
|
488
|
+
return { success: false, error: 'A document with the requested title already exists' };
|
|
489
|
+
}
|
|
490
|
+
catch {
|
|
491
|
+
// Safe to rename
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
const mergedMetadata = normalizeKnowledgeMetadata({
|
|
495
|
+
...existingDocument.metadata,
|
|
496
|
+
...metadata,
|
|
497
|
+
summary,
|
|
498
|
+
updated: new Date().toISOString().split('T')[0]
|
|
499
|
+
});
|
|
500
|
+
const markdown = matter.stringify(content, mergedMetadata);
|
|
501
|
+
await fs.writeFile(nextPath, markdown, 'utf-8');
|
|
502
|
+
if (nextFilename !== currentFilename) {
|
|
503
|
+
await fs.unlink(currentPath);
|
|
504
|
+
}
|
|
505
|
+
return {
|
|
506
|
+
success: true,
|
|
507
|
+
data: {
|
|
508
|
+
filename: nextFilename,
|
|
509
|
+
title: nextTitle,
|
|
510
|
+
metadata: mergedMetadata,
|
|
511
|
+
content
|
|
512
|
+
},
|
|
513
|
+
message: `Updated knowledge document ${nextFilename}`
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
case 'preview_update': {
|
|
517
|
+
if (!filename) {
|
|
518
|
+
return { success: false, error: 'Filename is required for preview update operation' };
|
|
519
|
+
}
|
|
520
|
+
const currentFilename = resolveKnowledgeFilename(filename, filename);
|
|
521
|
+
const existingDocument = await readKnowledgeDocument(knowledgePath, currentFilename);
|
|
522
|
+
if (!existingDocument) {
|
|
523
|
+
return { success: false, error: 'Knowledge document not found' };
|
|
524
|
+
}
|
|
525
|
+
const proposedTitle = title || existingDocument.title;
|
|
526
|
+
const previewMetadata = normalizeKnowledgeMetadata({
|
|
527
|
+
...existingDocument.metadata,
|
|
528
|
+
...metadata,
|
|
529
|
+
summary
|
|
530
|
+
});
|
|
531
|
+
return {
|
|
532
|
+
success: true,
|
|
533
|
+
data: {
|
|
534
|
+
filename: currentFilename,
|
|
535
|
+
title: proposedTitle,
|
|
536
|
+
metadata: previewMetadata,
|
|
537
|
+
originalContent: existingDocument.content,
|
|
538
|
+
proposedContent: content || existingDocument.content,
|
|
539
|
+
summary: summary || null
|
|
540
|
+
},
|
|
541
|
+
message: `Prepared preview for ${currentFilename}`
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
default:
|
|
545
|
+
return { success: false, error: `Unknown operation: ${operation}` };
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
catch (error) {
|
|
549
|
+
console.error('Error executing knowledge tool:', error);
|
|
550
|
+
return { success: false, error: error.message || 'Knowledge tool execution failed' };
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
function getKnowledgeBasePath() {
|
|
554
|
+
let basePath;
|
|
555
|
+
if (process.env.NODE_ENV === 'development' && process.env.GAIT_DEV_ROOT) {
|
|
556
|
+
basePath = process.env.GAIT_DEV_ROOT;
|
|
557
|
+
}
|
|
558
|
+
else if (process.env.GAIT_DATA_PATH) {
|
|
559
|
+
basePath = pathResolve(process.env.GAIT_DATA_PATH, '.nut');
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
basePath = pathResolve(process.cwd(), '.nut');
|
|
563
|
+
}
|
|
564
|
+
return join(basePath, 'context', 'knowledge');
|
|
565
|
+
}
|
|
566
|
+
function slugify(value) {
|
|
567
|
+
const slug = value
|
|
568
|
+
.toLowerCase()
|
|
569
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
570
|
+
.replace(/\s+/g, '-')
|
|
571
|
+
.replace(/--+/g, '-')
|
|
572
|
+
.replace(/^-|-$/g, '');
|
|
573
|
+
return slug || 'knowledge-entry';
|
|
574
|
+
}
|
|
575
|
+
function resolveKnowledgeFilename(input, fallbackTitle) {
|
|
576
|
+
if (input && input.trim().length > 0) {
|
|
577
|
+
const trimmed = basename(input.trim());
|
|
578
|
+
if (trimmed.endsWith('.md')) {
|
|
579
|
+
return trimmed;
|
|
580
|
+
}
|
|
581
|
+
return `${slugify(trimmed)}.md`;
|
|
582
|
+
}
|
|
583
|
+
const fallback = fallbackTitle && fallbackTitle.trim().length > 0
|
|
584
|
+
? basename(fallbackTitle.trim())
|
|
585
|
+
: 'knowledge-entry';
|
|
586
|
+
return `${slugify(fallback)}.md`;
|
|
587
|
+
}
|
|
588
|
+
function extractKnowledgeTitle(content, fallback) {
|
|
589
|
+
const headingMatch = content.match(/^#\s+(.+)$/m);
|
|
590
|
+
if (headingMatch && headingMatch[1]) {
|
|
591
|
+
return headingMatch[1].trim();
|
|
592
|
+
}
|
|
593
|
+
const baseName = fallback.replace('.md', '').replace(/[_-]/g, ' ');
|
|
594
|
+
return baseName.replace(/\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.slice(1).toLowerCase());
|
|
595
|
+
}
|
|
596
|
+
async function readKnowledgeDocument(basePath, filename) {
|
|
597
|
+
const actualFilename = resolveKnowledgeFilename(filename, filename);
|
|
598
|
+
const filePath = join(basePath, actualFilename);
|
|
599
|
+
const raw = await fs.readFile(filePath, 'utf-8');
|
|
600
|
+
const { data, content } = matter(raw);
|
|
601
|
+
const metadata = normalizeKnowledgeMetadata(data);
|
|
602
|
+
const title = extractKnowledgeTitle(content, actualFilename);
|
|
603
|
+
return {
|
|
604
|
+
filename: actualFilename,
|
|
605
|
+
title,
|
|
606
|
+
metadata,
|
|
607
|
+
content
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
async function listKnowledgeDocuments(basePath) {
|
|
611
|
+
const files = await fs.readdir(basePath);
|
|
612
|
+
const markdownFiles = files.filter(file => file.endsWith('.md'));
|
|
613
|
+
const documents = await Promise.all(markdownFiles.map(async (file) => {
|
|
614
|
+
try {
|
|
615
|
+
return await readKnowledgeDocument(basePath, file);
|
|
616
|
+
}
|
|
617
|
+
catch (error) {
|
|
618
|
+
console.error(`Failed to read knowledge document ${file}:`, error);
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
}));
|
|
622
|
+
return documents
|
|
623
|
+
.filter((doc) => doc !== null)
|
|
624
|
+
.sort((a, b) => a.filename.localeCompare(b.filename));
|
|
625
|
+
}
|
|
385
626
|
function readSharedPrompt(name) {
|
|
386
627
|
// Try multiple candidate paths depending on cwd when running via Turbo
|
|
387
628
|
const moduleDir = fileURLToPath(new URL('.', import.meta.url));
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
2
|
import { promises as fs } from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
import
|
|
4
|
+
import matter from 'gray-matter';
|
|
5
|
+
import { proposalsTool, listProposalsTool, validateProposalData, knowledgeTool, normalizeKnowledgeMetadata } from '@lovelybunch/mcp';
|
|
5
6
|
import { FileStorageAdapter } from '../../../../lib/storage/file-storage.js';
|
|
6
7
|
import { getAuthorInfo } from '../../../../lib/user-preferences.js';
|
|
7
8
|
const app = new Hono();
|
|
@@ -43,7 +44,8 @@ app.get('/', async (c) => {
|
|
|
43
44
|
// Add built-in tools (include full JSON schema for parameters)
|
|
44
45
|
const builtInTools = {
|
|
45
46
|
change_proposals: proposalsTool,
|
|
46
|
-
list_proposals: listProposalsTool
|
|
47
|
+
list_proposals: listProposalsTool,
|
|
48
|
+
knowledge_documents: knowledgeTool
|
|
47
49
|
};
|
|
48
50
|
return c.json({
|
|
49
51
|
success: true,
|
|
@@ -66,7 +68,8 @@ app.get('/schema', async (c) => {
|
|
|
66
68
|
const schema = {
|
|
67
69
|
tools: {
|
|
68
70
|
change_proposals: proposalsTool,
|
|
69
|
-
list_proposals: listProposalsTool
|
|
71
|
+
list_proposals: listProposalsTool,
|
|
72
|
+
knowledge_documents: knowledgeTool
|
|
70
73
|
}
|
|
71
74
|
};
|
|
72
75
|
return c.json(schema);
|
|
@@ -92,6 +95,9 @@ app.post('/execute', async (c) => {
|
|
|
92
95
|
if (tool === 'list_proposals') {
|
|
93
96
|
return await executeListProposalsTool(c, args);
|
|
94
97
|
}
|
|
98
|
+
if (tool === 'knowledge_documents') {
|
|
99
|
+
return await executeKnowledgeTool(c, args);
|
|
100
|
+
}
|
|
95
101
|
return c.json({ success: false, error: 'Unknown tool' }, 400);
|
|
96
102
|
}
|
|
97
103
|
catch (error) {
|
|
@@ -295,6 +301,218 @@ async function executeListProposalsTool(c, args) {
|
|
|
295
301
|
return c.json({ success: false, error: error.message || 'Failed to list proposals' }, 500);
|
|
296
302
|
}
|
|
297
303
|
}
|
|
304
|
+
async function executeKnowledgeTool(c, args) {
|
|
305
|
+
const { operation, filename, title, content, metadata, summary } = args;
|
|
306
|
+
try {
|
|
307
|
+
const knowledgePath = getKnowledgePath();
|
|
308
|
+
await fs.mkdir(knowledgePath, { recursive: true });
|
|
309
|
+
switch (operation) {
|
|
310
|
+
case 'list': {
|
|
311
|
+
const documents = await listKnowledgeDocuments(knowledgePath);
|
|
312
|
+
return c.json({
|
|
313
|
+
success: true,
|
|
314
|
+
data: documents,
|
|
315
|
+
message: `Found ${documents.length} knowledge documents`
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
case 'get': {
|
|
319
|
+
if (!filename) {
|
|
320
|
+
return c.json({ success: false, error: 'Filename is required for get operation' }, 400);
|
|
321
|
+
}
|
|
322
|
+
const document = await readKnowledgeDocument(knowledgePath, filename);
|
|
323
|
+
if (!document) {
|
|
324
|
+
return c.json({ success: false, error: 'Knowledge document not found' }, 404);
|
|
325
|
+
}
|
|
326
|
+
return c.json({ success: true, data: document, message: `Loaded knowledge document ${document.filename}` });
|
|
327
|
+
}
|
|
328
|
+
case 'create': {
|
|
329
|
+
if (!title || !content) {
|
|
330
|
+
return c.json({ success: false, error: 'Title and content are required for create operation' }, 400);
|
|
331
|
+
}
|
|
332
|
+
const targetFilename = resolveFilename(filename, title);
|
|
333
|
+
const filePath = path.join(knowledgePath, targetFilename);
|
|
334
|
+
try {
|
|
335
|
+
await fs.access(filePath);
|
|
336
|
+
return c.json({ success: false, error: 'A knowledge document with this filename already exists' }, 409);
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
// File does not exist, safe to create
|
|
340
|
+
}
|
|
341
|
+
const normalizedMetadata = normalizeKnowledgeMetadata({
|
|
342
|
+
version: '1.0',
|
|
343
|
+
summary,
|
|
344
|
+
...metadata
|
|
345
|
+
});
|
|
346
|
+
const markdown = matter.stringify(content, normalizedMetadata);
|
|
347
|
+
await fs.writeFile(filePath, markdown, 'utf-8');
|
|
348
|
+
const created = {
|
|
349
|
+
filename: targetFilename,
|
|
350
|
+
title,
|
|
351
|
+
metadata: normalizedMetadata,
|
|
352
|
+
content
|
|
353
|
+
};
|
|
354
|
+
return c.json({
|
|
355
|
+
success: true,
|
|
356
|
+
data: created,
|
|
357
|
+
message: `Created knowledge document ${targetFilename}`
|
|
358
|
+
}, 201);
|
|
359
|
+
}
|
|
360
|
+
case 'update': {
|
|
361
|
+
if (!filename || !content) {
|
|
362
|
+
return c.json({ success: false, error: 'Filename and content are required for update operation' }, 400);
|
|
363
|
+
}
|
|
364
|
+
const currentFilename = resolveFilename(filename, title || 'knowledge-entry');
|
|
365
|
+
const currentPath = path.join(knowledgePath, currentFilename);
|
|
366
|
+
let existingDocument = null;
|
|
367
|
+
try {
|
|
368
|
+
existingDocument = await readKnowledgeDocument(knowledgePath, currentFilename);
|
|
369
|
+
}
|
|
370
|
+
catch (error) {
|
|
371
|
+
if (error.code === 'ENOENT') {
|
|
372
|
+
return c.json({ success: false, error: 'Knowledge document not found' }, 404);
|
|
373
|
+
}
|
|
374
|
+
throw error;
|
|
375
|
+
}
|
|
376
|
+
if (!existingDocument) {
|
|
377
|
+
return c.json({ success: false, error: 'Knowledge document not found' }, 404);
|
|
378
|
+
}
|
|
379
|
+
const nextTitle = title || existingDocument.title;
|
|
380
|
+
const shouldRename = Boolean(title && slugify(title) !== slugify(existingDocument.title));
|
|
381
|
+
const nextFilename = shouldRename ? resolveFilename(undefined, nextTitle) : currentFilename;
|
|
382
|
+
const nextPath = path.join(knowledgePath, nextFilename);
|
|
383
|
+
if (nextFilename !== currentFilename) {
|
|
384
|
+
try {
|
|
385
|
+
await fs.access(nextPath);
|
|
386
|
+
return c.json({ success: false, error: 'A document with the requested title already exists' }, 409);
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
// OK to rename
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
const mergedMetadata = normalizeKnowledgeMetadata({
|
|
393
|
+
...existingDocument.metadata,
|
|
394
|
+
...metadata,
|
|
395
|
+
summary,
|
|
396
|
+
updated: new Date().toISOString().split('T')[0]
|
|
397
|
+
});
|
|
398
|
+
const markdown = matter.stringify(content, mergedMetadata);
|
|
399
|
+
await fs.writeFile(nextPath, markdown, 'utf-8');
|
|
400
|
+
if (nextFilename !== currentFilename) {
|
|
401
|
+
await fs.unlink(currentPath);
|
|
402
|
+
}
|
|
403
|
+
const updated = {
|
|
404
|
+
filename: nextFilename,
|
|
405
|
+
title: nextTitle,
|
|
406
|
+
metadata: mergedMetadata,
|
|
407
|
+
content
|
|
408
|
+
};
|
|
409
|
+
return c.json({
|
|
410
|
+
success: true,
|
|
411
|
+
data: updated,
|
|
412
|
+
message: `Updated knowledge document ${nextFilename}`
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
case 'preview_update': {
|
|
416
|
+
if (!filename) {
|
|
417
|
+
return c.json({ success: false, error: 'Filename is required for preview update operation' }, 400);
|
|
418
|
+
}
|
|
419
|
+
const currentFilename = resolveFilename(filename, filename);
|
|
420
|
+
const existingDocument = await readKnowledgeDocument(knowledgePath, currentFilename);
|
|
421
|
+
if (!existingDocument) {
|
|
422
|
+
return c.json({ success: false, error: 'Knowledge document not found' }, 404);
|
|
423
|
+
}
|
|
424
|
+
const proposedTitle = title || existingDocument.title;
|
|
425
|
+
const previewMetadata = normalizeKnowledgeMetadata({
|
|
426
|
+
...existingDocument.metadata,
|
|
427
|
+
...metadata,
|
|
428
|
+
summary
|
|
429
|
+
});
|
|
430
|
+
return c.json({
|
|
431
|
+
success: true,
|
|
432
|
+
data: {
|
|
433
|
+
filename: currentFilename,
|
|
434
|
+
title: proposedTitle,
|
|
435
|
+
metadata: previewMetadata,
|
|
436
|
+
originalContent: existingDocument.content,
|
|
437
|
+
proposedContent: content || existingDocument.content,
|
|
438
|
+
summary: summary || null
|
|
439
|
+
},
|
|
440
|
+
message: `Prepared preview for ${currentFilename}`
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
default:
|
|
444
|
+
return c.json({ success: false, error: `Unknown operation: ${operation}` }, 400);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
catch (error) {
|
|
448
|
+
console.error('Error executing knowledge tool:', error);
|
|
449
|
+
return c.json({ success: false, error: error.message || 'Knowledge tool execution failed' }, 500);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
function getKnowledgePath() {
|
|
453
|
+
return path.join(resolveGaitPath(), 'context', 'knowledge');
|
|
454
|
+
}
|
|
455
|
+
function slugify(value) {
|
|
456
|
+
const slug = value
|
|
457
|
+
.toLowerCase()
|
|
458
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
459
|
+
.replace(/\s+/g, '-')
|
|
460
|
+
.replace(/--+/g, '-')
|
|
461
|
+
.replace(/^-|-$/g, '');
|
|
462
|
+
return slug || 'knowledge-entry';
|
|
463
|
+
}
|
|
464
|
+
function resolveFilename(input, fallbackTitle) {
|
|
465
|
+
if (input && input.trim().length > 0) {
|
|
466
|
+
const trimmed = path.basename(input.trim());
|
|
467
|
+
if (trimmed.endsWith('.md')) {
|
|
468
|
+
return trimmed;
|
|
469
|
+
}
|
|
470
|
+
return `${slugify(trimmed)}.md`;
|
|
471
|
+
}
|
|
472
|
+
const fallback = fallbackTitle && fallbackTitle.trim().length > 0
|
|
473
|
+
? path.basename(fallbackTitle.trim())
|
|
474
|
+
: 'knowledge-entry';
|
|
475
|
+
return `${slugify(fallback)}.md`;
|
|
476
|
+
}
|
|
477
|
+
function extractTitle(content, fallback) {
|
|
478
|
+
const headingMatch = content.match(/^#\s+(.+)$/m);
|
|
479
|
+
if (headingMatch && headingMatch[1]) {
|
|
480
|
+
return headingMatch[1].trim();
|
|
481
|
+
}
|
|
482
|
+
const baseName = fallback.replace('.md', '').replace(/[_-]/g, ' ');
|
|
483
|
+
return baseName.replace(/\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.slice(1).toLowerCase());
|
|
484
|
+
}
|
|
485
|
+
async function readKnowledgeDocument(basePath, filename) {
|
|
486
|
+
const actualFilename = resolveFilename(filename, filename);
|
|
487
|
+
const filePath = path.join(basePath, actualFilename);
|
|
488
|
+
const raw = await fs.readFile(filePath, 'utf-8');
|
|
489
|
+
const { data, content } = matter(raw);
|
|
490
|
+
const metadata = normalizeKnowledgeMetadata(data);
|
|
491
|
+
const title = extractTitle(content, actualFilename);
|
|
492
|
+
return {
|
|
493
|
+
filename: actualFilename,
|
|
494
|
+
title,
|
|
495
|
+
metadata,
|
|
496
|
+
content
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
async function listKnowledgeDocuments(basePath) {
|
|
500
|
+
const files = await fs.readdir(basePath);
|
|
501
|
+
const markdownFiles = files.filter(file => file.endsWith('.md'));
|
|
502
|
+
const documents = await Promise.all(markdownFiles.map(async (file) => {
|
|
503
|
+
try {
|
|
504
|
+
const doc = await readKnowledgeDocument(basePath, file);
|
|
505
|
+
return doc;
|
|
506
|
+
}
|
|
507
|
+
catch (error) {
|
|
508
|
+
console.error(`Failed to read knowledge document ${file}:`, error);
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
}));
|
|
512
|
+
return documents
|
|
513
|
+
.filter((doc) => doc !== null)
|
|
514
|
+
.sort((a, b) => a.filename.localeCompare(b.filename));
|
|
515
|
+
}
|
|
298
516
|
/**
|
|
299
517
|
* GET /api/v1/mcp/raw-config
|
|
300
518
|
* Returns the raw MCP configuration for editing in settings UI
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lovelybunch/api",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.55",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/server-with-static.js",
|
|
6
6
|
"exports": {
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@hono/node-server": "^1.13.7",
|
|
34
34
|
"@hono/node-ws": "^1.0.6",
|
|
35
|
-
"@lovelybunch/core": "^1.0.
|
|
36
|
-
"@lovelybunch/mcp": "^1.0.
|
|
37
|
-
"@lovelybunch/types": "^1.0.
|
|
35
|
+
"@lovelybunch/core": "^1.0.55",
|
|
36
|
+
"@lovelybunch/mcp": "^1.0.55",
|
|
37
|
+
"@lovelybunch/types": "^1.0.55",
|
|
38
38
|
"dotenv": "^17.2.1",
|
|
39
39
|
"fuse.js": "^7.0.0",
|
|
40
40
|
"gray-matter": "^4.0.3",
|