@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.
@@ -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 { proposalsTool, listProposalsTool, validateProposalData } from '@lovelybunch/mcp';
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.53",
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.53",
36
- "@lovelybunch/mcp": "^1.0.53",
37
- "@lovelybunch/types": "^1.0.53",
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",