@lovelybunch/api 1.0.73 → 1.0.74

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.
Files changed (92) hide show
  1. package/dist/routes/api/v1/ai/route.js +79 -18
  2. package/dist/routes/api/v1/ai/tools.js +76 -16
  3. package/dist/routes/api/v1/mcp/index.js +129 -573
  4. package/dist/routes/api/v1/proposals/[id]/route.d.ts +12 -0
  5. package/dist/routes/api/v1/proposals/route.d.ts +12 -0
  6. package/package.json +6 -5
  7. package/static/assets/ActivityPage-AWTVFLmK.js +1 -0
  8. package/static/assets/{AgentDetailPage-WLAnnZep.js → AgentDetailPage-DtGFrHzZ.js} +1 -1
  9. package/static/assets/{AgentEditPage-DOemUkvg.js → AgentEditPage-1026aJox.js} +1 -1
  10. package/static/assets/{AgentsPage-Bage8eYW.js → AgentsPage-9Hre8AD1.js} +2 -2
  11. package/static/assets/{AgentsSettingsPage-Cw2MTnHU.js → AgentsSettingsPage-CILCHaO9.js} +1 -1
  12. package/static/assets/{ApiKeysSettingsPage-DCKd4LXE.js → ApiKeysSettingsPage-D-l8q30N.js} +1 -1
  13. package/static/assets/{ArchitectureEditPage-WY9k_1tR.js → ArchitectureEditPage-DiRR28Rx.js} +1 -1
  14. package/static/assets/{ArchitecturePage-Cj4dVDWO.js → ArchitecturePage-FcRJGw8O.js} +1 -1
  15. package/static/assets/{AuthSettingsPage-Bs5wL5Yj.js → AuthSettingsPage-D528tGhc.js} +1 -1
  16. package/static/assets/{CallbackPage-CtydJ4j3.js → CallbackPage-BMZOjCy_.js} +1 -1
  17. package/static/assets/CodePage-CCNmmkv_.js +2 -0
  18. package/static/assets/{CollapsibleSection-5qSoX47l.js → CollapsibleSection-CG5gAVWB.js} +1 -1
  19. package/static/assets/DashboardPage-D5C34QbO.js +41 -0
  20. package/static/assets/{GitPage-BG-ZSGfu.js → GitPage-ofZrdSSl.js} +1 -1
  21. package/static/assets/{GitSettingsPage-Cn-MciXq.js → GitSettingsPage-Cld_sN5t.js} +1 -1
  22. package/static/assets/{IdentityPage-DCpoDF2j.js → IdentityPage-Dj-Do8q7.js} +1 -1
  23. package/static/assets/{ImplementationStepsEditor-PM47REBn.js → ImplementationStepsEditor-BZQKPJ1C.js} +1 -1
  24. package/static/assets/{IntegrationsSettingsPage-DJpf2gZn.js → IntegrationsSettingsPage-DTT6nX3v.js} +1 -1
  25. package/static/assets/JobDetailPage-CWexORzH.js +1 -0
  26. package/static/assets/{KnowledgeDetailPage-CLizVIxC.js → KnowledgeDetailPage-C-a0njhy.js} +1 -1
  27. package/static/assets/{KnowledgeEditPage-B-sdY_DS.js → KnowledgeEditPage-B93ffsgz.js} +1 -1
  28. package/static/assets/{KnowledgePage-DIlUufGq.js → KnowledgePage-DS7xi1qJ.js} +1 -1
  29. package/static/assets/{LoginPage-DcTU__Dc.js → LoginPage-FUflG9B8.js} +1 -1
  30. package/static/assets/{McpSettingsPage-BPDLne4q.js → McpSettingsPage-A6uws8gQ.js} +1 -1
  31. package/static/assets/{NewAgentPage-8-abvhkI.js → NewAgentPage-CqBlVqdy.js} +1 -1
  32. package/static/assets/{NewKnowledgePage-Rvj741x1.js → NewKnowledgePage-ue0ZRgKf.js} +1 -1
  33. package/static/assets/{NewProposalPage-BLIqmuks.js → NewProposalPage-BDbStKts.js} +1 -1
  34. package/static/assets/{ProjectEditPage-DC5-v_2a.js → ProjectEditPage-_a9hJTgi.js} +1 -1
  35. package/static/assets/{ProjectPage-_L4wqwrB.js → ProjectPage-DAlEu9Af.js} +1 -1
  36. package/static/assets/{PromptsSettingsPage-K-qqpF2S.js → PromptsSettingsPage-4_C5zvq6.js} +1 -1
  37. package/static/assets/{ProposalDetailPage-BVXTjXw2.js → ProposalDetailPage-mZ9qj0wJ.js} +1 -1
  38. package/static/assets/{ProposalEditPage-Bs3ak0PG.js → ProposalEditPage-DqJ3Po23.js} +1 -1
  39. package/static/assets/{ProposalsPage-BvKlKvuo.js → ProposalsPage-DaR_KuZx.js} +1 -1
  40. package/static/assets/{ResourcesPage-BSyX_kHV.js → ResourcesPage-WMPlFn0S.js} +1 -1
  41. package/static/assets/{RoleEditPage-CnWierul.js → RoleEditPage-DIoamJfW.js} +1 -1
  42. package/static/assets/{RolePage-D8xB0I-F.js → RolePage-4aL0vR7F.js} +1 -1
  43. package/static/assets/{RulesSettingsPage-CZBQ0u-x.js → RulesSettingsPage-CerRq_kE.js} +1 -1
  44. package/static/assets/SchedulePage-DF9TqOA6.js +4 -0
  45. package/static/assets/{SourceInput-BoRGYtye.js → SourceInput-CXv23vkE.js} +1 -1
  46. package/static/assets/{TagInput-CdXzv6hj.js → TagInput-DL_48L0p.js} +1 -1
  47. package/static/assets/{TerminalPage-CqPXFOIN.js → TerminalPage-BL8pa_9k.js} +1 -1
  48. package/static/assets/{TerminalSessionPage-DR2cApWv.js → TerminalSessionPage-DcC_o_sA.js} +1 -1
  49. package/static/assets/{UserPreferencesPage-CPkulDiM.js → UserPreferencesPage-DoMyvwD4.js} +1 -1
  50. package/static/assets/{UserSettingsPage-BuMnU394.js → UserSettingsPage-CqibuEiq.js} +1 -1
  51. package/static/assets/{UtilitiesPage-BGiBB5PT.js → UtilitiesPage-6BGeolw9.js} +1 -1
  52. package/static/assets/{alert-CHpdnk6F.js → alert-DQ9shGSz.js} +1 -1
  53. package/static/assets/{arrow-down-BjGtxyJ7.js → arrow-down-D5oSUxtq.js} +1 -1
  54. package/static/assets/{arrow-left-DR0foZvi.js → arrow-left-BJ9vzpff.js} +1 -1
  55. package/static/assets/{arrow-up-DCGMV1HG.js → arrow-up-DWNinaN_.js} +1 -1
  56. package/static/assets/{badge-Cds9UgAB.js → badge-BLbqPEov.js} +1 -1
  57. package/static/assets/{browser-modal-Ck4-s_jh.js → browser-modal-Dh2dy_2x.js} +1 -1
  58. package/static/assets/{calendar-CWhxfYG2.js → calendar-CaBty4QE.js} +1 -1
  59. package/static/assets/{card-BdOrf6VU.js → card-BTg0Fecj.js} +1 -1
  60. package/static/assets/{chevron-left-Wnwkk8g7.js → chevron-left-V0REBjky.js} +1 -1
  61. package/static/assets/{chevrons-up-DbnuGl5g.js → chevrons-up-BvR9KFeO.js} +1 -1
  62. package/static/assets/{circle-alert-BPvQggmh.js → circle-alert-DR1DiLh5.js} +1 -1
  63. package/static/assets/{circle-check-CTlDV9K4.js → circle-check-DED-Ne-m.js} +1 -1
  64. package/static/assets/{circle-check-big-KMhw8TDM.js → circle-check-big-CFsUUo2_.js} +1 -1
  65. package/static/assets/{circle-play-tZLgOcAc.js → circle-play-DQ32zg60.js} +1 -1
  66. package/static/assets/{circle-x-D3tazxxl.js → circle-x-CDTWutj4.js} +1 -1
  67. package/static/assets/{clipboard-C86rsVqL.js → clipboard-CTFAlaYo.js} +1 -1
  68. package/static/assets/{clock-Qm5u6CAm.js → clock-pVWUoJDo.js} +1 -1
  69. package/static/assets/{download-Bo3vdlNo.js → download-Bt7lWWYL.js} +1 -1
  70. package/static/assets/{eye-Cr9Hfzxo.js → eye-o7gPa7E6.js} +1 -1
  71. package/static/assets/{folder-git-2-Em4ZglGs.js → folder-git-2-CHwnYB8a.js} +1 -1
  72. package/static/assets/{index-CQpPrvm_.js → index-DaqYJNAM.js} +30 -30
  73. package/static/assets/{label-nG86oxuW.js → label-BtnLfquf.js} +1 -1
  74. package/static/assets/{markdown-editor-CEQMlLWe.js → markdown-editor-Cmsc5nNu.js} +1 -1
  75. package/static/assets/{pause-DuR7ql7H.js → pause-EIDnvByV.js} +1 -1
  76. package/static/assets/{play-p4m8WHP3.js → play-D_fLXyLR.js} +1 -1
  77. package/static/assets/{plus-CD075YlZ.js → plus-ZCd709aN.js} +1 -1
  78. package/static/assets/{radio-group-DCKkxIgI.js → radio-group-STvlCDl-.js} +1 -1
  79. package/static/assets/{refresh-cw-D-IgYQ7y.js → refresh-cw-BF0hXtxu.js} +1 -1
  80. package/static/assets/{search-W3H-E0eW.js → search-kmqzieAc.js} +1 -1
  81. package/static/assets/{switch-E0GrYmgh.js → switch-CjDKvzd0.js} +1 -1
  82. package/static/assets/{tabs-DjZiD9WD.js → tabs-DPohYGac.js} +1 -1
  83. package/static/assets/{tag-D0iVh1-U.js → tag-Dg9notds.js} +1 -1
  84. package/static/assets/{terminal-preview-BQBOEop2.js → terminal-preview-CnytxbzD.js} +1 -1
  85. package/static/assets/{use-terminal-0TezQnxO.js → use-terminal-LpFJK0k7.js} +1 -1
  86. package/static/assets/{zap-C_5I7lLi.js → zap-DBKgrFQ_.js} +1 -1
  87. package/static/index.html +1 -1
  88. package/static/assets/ActivityPage-C_HqpJt2.js +0 -1
  89. package/static/assets/CodePage-BiRf5q_q.js +0 -2
  90. package/static/assets/DashboardPage-e9hNRsi2.js +0 -41
  91. package/static/assets/JobDetailPage-9shaUPlO.js +0 -1
  92. package/static/assets/SchedulePage-HBFJT_19.js +0 -4
@@ -1,11 +1,9 @@
1
1
  import { Hono } from 'hono';
2
- import { promises as fs, createReadStream } from 'fs';
2
+ import { promises as fs } from 'fs';
3
3
  import path from 'path';
4
- import readline from 'readline';
5
- import matter from 'gray-matter';
6
- import Fuse from 'fuse.js';
7
- import { getLogsDir } from '@lovelybunch/core';
8
- import { proposalsReadOnlyTool, knowledgeTool, normalizeKnowledgeMetadata, eventsTool, projectContextTool, architectureContextTool, roleContextTool } from '@lovelybunch/mcp';
4
+ import { ZodError } from 'zod';
5
+ import { listProposals, getProposal, createProposal, updateProposal, deleteProposal, getContext, updateContext, appendContext, replaceContextSection, listKnowledge, getKnowledge, createKnowledge, updateKnowledge, listEvents, } from '@lovelybunch/core';
6
+ import { proposalsFullTool, knowledgeTool, normalizeKnowledgeMetadata, eventsTool, projectContextTool, architectureContextTool, roleContextTool } from '@lovelybunch/mcp';
9
7
  import { FileStorageAdapter } from '../../../../lib/storage/file-storage.js';
10
8
  const app = new Hono();
11
9
  const storage = new FileStorageAdapter();
@@ -80,9 +78,9 @@ app.get('/', async (c) => {
80
78
  }
81
79
  const names = Object.keys(externalServers);
82
80
  // Add built-in tools (include full JSON schema for parameters)
83
- // Note: proposals is read-only, knowledge/project/architecture are read+write, events is read-only
81
+ // Note: proposals now supports full CRUD, knowledge/project/architecture are read+write, events is read-only
84
82
  const builtInTools = {
85
- change_proposals: proposalsReadOnlyTool,
83
+ change_proposals: proposalsFullTool,
86
84
  knowledge_documents: knowledgeTool,
87
85
  activity_events: eventsTool,
88
86
  project_context: projectContextTool,
@@ -107,10 +105,10 @@ app.get('/', async (c) => {
107
105
  */
108
106
  app.get('/schema', async (c) => {
109
107
  try {
110
- // Note: proposals is read-only, knowledge/project/architecture are read+write, events is read-only
108
+ // Note: proposals now supports full CRUD, knowledge/project/architecture are read+write, events is read-only
111
109
  const schema = {
112
110
  tools: {
113
- change_proposals: proposalsReadOnlyTool,
111
+ change_proposals: proposalsFullTool,
114
112
  knowledge_documents: knowledgeTool,
115
113
  activity_events: eventsTool,
116
114
  project_context: projectContextTool,
@@ -160,16 +158,17 @@ app.post('/execute', async (c) => {
160
158
  return c.json({ success: false, error: 'Tool execution failed' }, 500);
161
159
  }
162
160
  });
163
- // Proposals tool is READ-ONLY - only list and get operations are supported
161
+ // Proposals tool - full CRUD operations using @lovelybunch/core
164
162
  async function executeProposalsTool(c, args) {
165
- const { operation, id, filters } = args;
163
+ const { operation, id, filters, proposal, updates } = args;
166
164
  try {
167
165
  switch (operation) {
168
166
  case 'list': {
169
- const proposals = await storage.listCPs(filters || {});
167
+ const proposals = await listProposals(filters || {});
170
168
  return c.json({
171
169
  success: true,
172
170
  data: proposals,
171
+ count: proposals.length,
173
172
  message: `Found ${proposals.length} proposals`
174
173
  });
175
174
  }
@@ -177,37 +176,86 @@ async function executeProposalsTool(c, args) {
177
176
  if (!id) {
178
177
  return c.json({ success: false, error: 'Proposal ID is required for get operation' }, 400);
179
178
  }
180
- const proposal = await storage.getCP(id);
181
- if (!proposal) {
179
+ const result = await getProposal(id);
180
+ if (!result) {
182
181
  return c.json({ success: false, error: 'Proposal not found' }, 404);
183
182
  }
184
183
  return c.json({
185
184
  success: true,
186
- data: proposal,
185
+ data: result,
187
186
  message: `Retrieved proposal ${id}`
188
187
  });
189
188
  }
189
+ case 'create': {
190
+ if (!proposal) {
191
+ return c.json({ success: false, error: 'Proposal data is required for create operation' }, 400);
192
+ }
193
+ const created = await createProposal(proposal);
194
+ return c.json({
195
+ success: true,
196
+ data: created,
197
+ message: `Created proposal ${created.id}`
198
+ }, 201);
199
+ }
200
+ case 'update': {
201
+ if (!id) {
202
+ return c.json({ success: false, error: 'Proposal ID is required for update operation' }, 400);
203
+ }
204
+ const updateData = updates || proposal;
205
+ if (!updateData) {
206
+ return c.json({ success: false, error: 'Update data is required for update operation' }, 400);
207
+ }
208
+ const updated = await updateProposal(id, updateData);
209
+ return c.json({
210
+ success: true,
211
+ data: updated,
212
+ message: `Updated proposal ${id}`
213
+ });
214
+ }
215
+ case 'delete': {
216
+ if (!id) {
217
+ return c.json({ success: false, error: 'Proposal ID is required for delete operation' }, 400);
218
+ }
219
+ const deleted = await deleteProposal(id);
220
+ if (!deleted) {
221
+ return c.json({ success: false, error: 'Proposal not found' }, 404);
222
+ }
223
+ return c.json({
224
+ success: true,
225
+ message: `Deleted proposal ${id}`
226
+ });
227
+ }
190
228
  default:
191
229
  return c.json({
192
230
  success: false,
193
- 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.`
231
+ error: `Unknown operation: ${operation}. Supported operations: list, get, create, update, delete`
194
232
  }, 400);
195
233
  }
196
234
  }
197
235
  catch (error) {
236
+ // Handle Zod validation errors specially
237
+ if (error instanceof ZodError) {
238
+ return c.json({
239
+ success: false,
240
+ error: 'Validation failed',
241
+ details: error.issues.map(e => ({
242
+ path: e.path.join('.'),
243
+ message: e.message
244
+ }))
245
+ }, 400);
246
+ }
198
247
  console.error('Error executing proposals tool:', error);
199
248
  return c.json({ success: false, error: error.message || 'Tool execution failed' }, 500);
200
249
  }
201
250
  }
251
+ // Knowledge tool executor - uses core knowledge functions
202
252
  async function executeKnowledgeTool(c, args) {
203
253
  const { operation, filename, title, content, metadata, summary, query } = args;
204
254
  try {
205
- const knowledgePath = getKnowledgePath();
206
- await fs.mkdir(knowledgePath, { recursive: true });
207
255
  switch (operation) {
208
256
  case 'list': {
209
257
  const searchQuery = query && typeof query === 'string' ? query : undefined;
210
- const documents = await listKnowledgeDocuments(knowledgePath, searchQuery);
258
+ const documents = await listKnowledge({ query: searchQuery });
211
259
  return c.json({
212
260
  success: true,
213
261
  data: documents,
@@ -220,7 +268,7 @@ async function executeKnowledgeTool(c, args) {
220
268
  if (!filename) {
221
269
  return c.json({ success: false, error: 'Filename is required for get operation' }, 400);
222
270
  }
223
- const document = await readKnowledgeDocument(knowledgePath, filename);
271
+ const document = await getKnowledge(filename);
224
272
  if (!document) {
225
273
  return c.json({ success: false, error: 'Knowledge document not found' }, 404);
226
274
  }
@@ -230,95 +278,48 @@ async function executeKnowledgeTool(c, args) {
230
278
  if (!title || !content) {
231
279
  return c.json({ success: false, error: 'Title and content are required for create operation' }, 400);
232
280
  }
233
- const targetFilename = resolveFilename(filename, title);
234
- const filePath = path.join(knowledgePath, targetFilename);
235
281
  try {
236
- await fs.access(filePath);
237
- return c.json({ success: false, error: 'A knowledge document with this filename already exists' }, 409);
282
+ const created = await createKnowledge({ title, content, filename, summary, metadata });
283
+ return c.json({
284
+ success: true,
285
+ data: created,
286
+ message: `Created knowledge document ${created.filename}`
287
+ }, 201);
238
288
  }
239
- catch {
240
- // File does not exist, safe to create
289
+ catch (err) {
290
+ if (err.message?.includes('already exists')) {
291
+ return c.json({ success: false, error: err.message }, 409);
292
+ }
293
+ throw err;
241
294
  }
242
- const normalizedMetadata = normalizeKnowledgeMetadata({
243
- version: '1.0',
244
- summary,
245
- ...metadata
246
- });
247
- const markdown = matter.stringify(content, normalizedMetadata);
248
- await fs.writeFile(filePath, markdown, 'utf-8');
249
- const created = {
250
- filename: targetFilename,
251
- title,
252
- metadata: normalizedMetadata,
253
- content
254
- };
255
- return c.json({
256
- success: true,
257
- data: created,
258
- message: `Created knowledge document ${targetFilename}`
259
- }, 201);
260
295
  }
261
296
  case 'update': {
262
297
  if (!filename || !content) {
263
298
  return c.json({ success: false, error: 'Filename and content are required for update operation' }, 400);
264
299
  }
265
- const currentFilename = resolveFilename(filename, title || 'knowledge-entry');
266
- const currentPath = path.join(knowledgePath, currentFilename);
267
- let existingDocument = null;
268
300
  try {
269
- existingDocument = await readKnowledgeDocument(knowledgePath, currentFilename);
270
- }
271
- catch (error) {
272
- if (error.code === 'ENOENT') {
273
- return c.json({ success: false, error: 'Knowledge document not found' }, 404);
274
- }
275
- throw error;
276
- }
277
- if (!existingDocument) {
278
- return c.json({ success: false, error: 'Knowledge document not found' }, 404);
301
+ const updated = await updateKnowledge({ filename, content, title, summary, metadata });
302
+ return c.json({
303
+ success: true,
304
+ data: updated,
305
+ message: `Updated knowledge document ${updated.filename}`
306
+ });
279
307
  }
280
- const nextTitle = title || existingDocument.title;
281
- const shouldRename = Boolean(title && slugify(title) !== slugify(existingDocument.title));
282
- const nextFilename = shouldRename ? resolveFilename(undefined, nextTitle) : currentFilename;
283
- const nextPath = path.join(knowledgePath, nextFilename);
284
- if (nextFilename !== currentFilename) {
285
- try {
286
- await fs.access(nextPath);
287
- return c.json({ success: false, error: 'A document with the requested title already exists' }, 409);
308
+ catch (err) {
309
+ if (err.message?.includes('not found')) {
310
+ return c.json({ success: false, error: err.message }, 404);
288
311
  }
289
- catch {
290
- // OK to rename
312
+ if (err.message?.includes('already exists')) {
313
+ return c.json({ success: false, error: err.message }, 409);
291
314
  }
315
+ throw err;
292
316
  }
293
- const mergedMetadata = normalizeKnowledgeMetadata({
294
- ...existingDocument.metadata,
295
- ...metadata,
296
- summary,
297
- updated: new Date().toISOString().split('T')[0]
298
- });
299
- const markdown = matter.stringify(content, mergedMetadata);
300
- await fs.writeFile(nextPath, markdown, 'utf-8');
301
- if (nextFilename !== currentFilename) {
302
- await fs.unlink(currentPath);
303
- }
304
- const updated = {
305
- filename: nextFilename,
306
- title: nextTitle,
307
- metadata: mergedMetadata,
308
- content
309
- };
310
- return c.json({
311
- success: true,
312
- data: updated,
313
- message: `Updated knowledge document ${nextFilename}`
314
- });
315
317
  }
316
318
  case 'preview_update': {
317
319
  if (!filename) {
318
320
  return c.json({ success: false, error: 'Filename is required for preview update operation' }, 400);
319
321
  }
320
- const currentFilename = resolveFilename(filename, filename);
321
- const existingDocument = await readKnowledgeDocument(knowledgePath, currentFilename);
322
+ const existingDocument = await getKnowledge(filename);
322
323
  if (!existingDocument) {
323
324
  return c.json({ success: false, error: 'Knowledge document not found' }, 404);
324
325
  }
@@ -331,14 +332,14 @@ async function executeKnowledgeTool(c, args) {
331
332
  return c.json({
332
333
  success: true,
333
334
  data: {
334
- filename: currentFilename,
335
+ filename: existingDocument.filename,
335
336
  title: proposedTitle,
336
337
  metadata: previewMetadata,
337
338
  originalContent: existingDocument.content,
338
339
  proposedContent: content || existingDocument.content,
339
340
  summary: summary || null
340
341
  },
341
- message: `Prepared preview for ${currentFilename}`
342
+ message: `Prepared preview for ${existingDocument.filename}`
342
343
  });
343
344
  }
344
345
  default:
@@ -350,7 +351,7 @@ async function executeKnowledgeTool(c, args) {
350
351
  return c.json({ success: false, error: error.message || 'Knowledge tool execution failed' }, 500);
351
352
  }
352
353
  }
353
- // Events tool is READ-ONLY - returns recent activity events
354
+ // Events tool is READ-ONLY - uses core listEvents function
354
355
  async function executeEventsTool(c, args) {
355
356
  const { operation, limit = 20, kind } = args;
356
357
  if (operation !== 'list') {
@@ -360,66 +361,13 @@ async function executeEventsTool(c, args) {
360
361
  }, 400);
361
362
  }
362
363
  try {
363
- const eventsDir = getLogsDir();
364
- const currentFile = path.join(eventsDir, 'events-current.jsonl');
365
- // Check if file exists
366
- try {
367
- await fs.access(currentFile);
368
- }
369
- catch {
370
- return c.json({
371
- success: true,
372
- data: [],
373
- message: 'No events found'
374
- });
375
- }
376
- // Read all events from file
377
- const allEvents = [];
378
- const fileStream = createReadStream(currentFile);
379
- const rl = readline.createInterface({
380
- input: fileStream,
381
- crlfDelay: Infinity,
382
- });
383
- for await (const line of rl) {
384
- if (!line.trim())
385
- continue;
386
- try {
387
- const event = JSON.parse(line);
388
- // Apply kind filter if specified
389
- if (kind && typeof kind === 'string') {
390
- if (!event.kind || !event.kind.startsWith(kind)) {
391
- continue;
392
- }
393
- }
394
- allEvents.push(event);
395
- }
396
- catch {
397
- // Skip malformed lines
398
- continue;
399
- }
400
- }
401
- // Sort by seq descending (most recent first) and limit
402
- const cappedLimit = Math.min(Math.max(1, limit), 100);
403
- const sortedEvents = allEvents
404
- .sort((a, b) => (b.seq || 0) - (a.seq || 0))
405
- .slice(0, cappedLimit);
406
- // Format events for readability
407
- const formattedEvents = sortedEvents.map(event => ({
408
- seq: event.seq,
409
- kind: event.kind,
410
- actor: event.actor,
411
- subject: event.subject,
412
- timestamp: event.ts,
413
- level: event.level,
414
- tags: event.tags,
415
- summary: event.payload?.summary || event.payload?.message || event.payload?.title || undefined
416
- }));
364
+ const events = await listEvents({ limit, kind });
417
365
  return c.json({
418
366
  success: true,
419
- data: formattedEvents,
367
+ data: events,
420
368
  message: kind
421
- ? `Found ${formattedEvents.length} recent '${kind}' events`
422
- : `Found ${formattedEvents.length} recent events`
369
+ ? `Found ${events.length} recent '${kind}' events`
370
+ : `Found ${events.length} recent events`
423
371
  });
424
372
  }
425
373
  catch (error) {
@@ -427,319 +375,59 @@ async function executeEventsTool(c, args) {
427
375
  return c.json({ success: false, error: error.message || 'Events tool execution failed' }, 500);
428
376
  }
429
377
  }
430
- // Project context tool - read/write the project definition document
431
- async function executeProjectContextTool(c, args) {
432
- const { operation, content, old_text, new_text } = args;
433
- try {
434
- const contextPath = getContextPath();
435
- const filePath = path.join(contextPath, 'project.md');
436
- switch (operation) {
437
- case 'get': {
438
- try {
439
- const fileContent = await fs.readFile(filePath, 'utf-8');
440
- const parsed = matter(fileContent);
441
- return c.json({
442
- success: true,
443
- data: {
444
- content: parsed.content,
445
- frontmatter: parsed.data,
446
- raw: fileContent
447
- },
448
- message: 'Retrieved project context'
449
- });
450
- }
451
- catch (err) {
452
- if (err.code === 'ENOENT') {
453
- return c.json({
454
- success: true,
455
- data: { content: '', frontmatter: {}, raw: '' },
456
- message: 'Project context document does not exist yet. You can create it with an update operation.'
457
- });
458
- }
459
- throw err;
460
- }
461
- }
462
- case 'append': {
463
- if (!content) {
464
- return c.json({ success: false, error: 'Content is required for append operation' }, 400);
465
- }
466
- await fs.mkdir(contextPath, { recursive: true });
467
- // Read existing content if file exists
468
- let existingContent = '';
469
- try {
470
- existingContent = await fs.readFile(filePath, 'utf-8');
471
- }
472
- catch (err) {
473
- if (err.code !== 'ENOENT')
474
- throw err;
475
- }
476
- // Append new content with a newline separator
477
- const newContent = existingContent
478
- ? existingContent.trimEnd() + '\n\n' + content
479
- : content;
480
- await fs.writeFile(filePath, newContent, 'utf-8');
481
- return c.json({
482
- success: true,
483
- message: 'Appended content to project context document'
484
- });
485
- }
486
- case 'replace_section': {
487
- if (!old_text || !new_text) {
488
- return c.json({ success: false, error: 'Both old_text and new_text are required for replace_section operation' }, 400);
489
- }
490
- // Read existing content
491
- let existingContent = '';
492
- try {
493
- existingContent = await fs.readFile(filePath, 'utf-8');
494
- }
495
- catch (err) {
496
- if (err.code === 'ENOENT') {
497
- return c.json({ success: false, error: 'Project context document does not exist. Use append or update to create it first.' }, 404);
498
- }
499
- throw err;
500
- }
501
- // Check if old_text exists in the document
502
- if (!existingContent.includes(old_text)) {
503
- return c.json({
504
- success: false,
505
- error: 'Could not find the specified text in the document. The text may have been paraphrased or changed.',
506
- fallback_markdown: new_text,
507
- suggestion: 'Here is the replacement text. You can copy it and manually edit the document at /context/project'
508
- }, 400);
509
- }
510
- // Replace the text
511
- const updatedContent = existingContent.replace(old_text, new_text);
512
- await fs.writeFile(filePath, updatedContent, 'utf-8');
513
- return c.json({
514
- success: true,
515
- message: 'Replaced section in project context document'
516
- });
517
- }
518
- case 'update': {
519
- if (!content) {
520
- return c.json({ success: false, error: 'Content is required for update operation' }, 400);
521
- }
522
- await fs.mkdir(contextPath, { recursive: true });
523
- await fs.writeFile(filePath, content, 'utf-8');
524
- return c.json({
525
- success: true,
526
- message: 'Updated project context document'
527
- });
528
- }
529
- default:
530
- return c.json({ success: false, error: `Unknown operation: ${operation}. Use 'get', 'append', 'replace_section', or 'update'.` }, 400);
531
- }
532
- }
533
- catch (error) {
534
- console.error('Error executing project context tool:', error);
535
- return c.json({ success: false, error: error.message || 'Project context tool execution failed' }, 500);
536
- }
537
- }
538
- // Architecture context tool - read/write the architecture document
539
- async function executeArchitectureContextTool(c, args) {
378
+ // Generic context tool executor - uses core context functions
379
+ async function executeContextTool(c, contextType, args) {
540
380
  const { operation, content, old_text, new_text } = args;
381
+ const typeName = contextType.charAt(0).toUpperCase() + contextType.slice(1);
541
382
  try {
542
- const contextPath = getContextPath();
543
- const filePath = path.join(contextPath, 'architecture.md');
544
383
  switch (operation) {
545
384
  case 'get': {
546
- try {
547
- const fileContent = await fs.readFile(filePath, 'utf-8');
548
- const parsed = matter(fileContent);
549
- return c.json({
550
- success: true,
551
- data: {
552
- content: parsed.content,
553
- frontmatter: parsed.data,
554
- raw: fileContent
555
- },
556
- message: 'Retrieved architecture context'
557
- });
558
- }
559
- catch (err) {
560
- if (err.code === 'ENOENT') {
561
- return c.json({
562
- success: true,
563
- data: { content: '', frontmatter: {}, raw: '' },
564
- message: 'Architecture context document does not exist yet. You can create it with an update operation.'
565
- });
566
- }
567
- throw err;
568
- }
569
- }
570
- case 'append': {
571
- if (!content) {
572
- return c.json({ success: false, error: 'Content is required for append operation' }, 400);
573
- }
574
- await fs.mkdir(contextPath, { recursive: true });
575
- // Read existing content if file exists
576
- let existingContent = '';
577
- try {
578
- existingContent = await fs.readFile(filePath, 'utf-8');
579
- }
580
- catch (err) {
581
- if (err.code !== 'ENOENT')
582
- throw err;
583
- }
584
- // Append new content with a newline separator
585
- const newContent = existingContent
586
- ? existingContent.trimEnd() + '\n\n' + content
587
- : content;
588
- await fs.writeFile(filePath, newContent, 'utf-8');
589
- return c.json({
590
- success: true,
591
- message: 'Appended content to architecture context document'
592
- });
593
- }
594
- case 'replace_section': {
595
- if (!old_text || !new_text) {
596
- return c.json({ success: false, error: 'Both old_text and new_text are required for replace_section operation' }, 400);
597
- }
598
- // Read existing content
599
- let existingContent = '';
600
- try {
601
- existingContent = await fs.readFile(filePath, 'utf-8');
602
- }
603
- catch (err) {
604
- if (err.code === 'ENOENT') {
605
- return c.json({ success: false, error: 'Architecture context document does not exist. Use append or update to create it first.' }, 404);
606
- }
607
- throw err;
608
- }
609
- // Check if old_text exists in the document
610
- if (!existingContent.includes(old_text)) {
611
- return c.json({
612
- success: false,
613
- error: 'Could not find the specified text in the document. The text may have been paraphrased or changed.',
614
- fallback_markdown: new_text,
615
- suggestion: 'Here is the replacement text. You can copy it and manually edit the document at /context/architecture'
616
- }, 400);
617
- }
618
- // Replace the text
619
- const updatedContent = existingContent.replace(old_text, new_text);
620
- await fs.writeFile(filePath, updatedContent, 'utf-8');
621
- return c.json({
622
- success: true,
623
- message: 'Replaced section in architecture context document'
624
- });
625
- }
626
- case 'update': {
627
- if (!content) {
628
- return c.json({ success: false, error: 'Content is required for update operation' }, 400);
629
- }
630
- await fs.mkdir(contextPath, { recursive: true });
631
- await fs.writeFile(filePath, content, 'utf-8');
385
+ const doc = await getContext(contextType);
386
+ const message = doc.raw
387
+ ? `Retrieved ${contextType} context`
388
+ : `${typeName} context document does not exist yet. You can create it with an update operation.`;
632
389
  return c.json({
633
390
  success: true,
634
- message: 'Updated architecture context document'
391
+ data: doc,
392
+ message
635
393
  });
636
394
  }
637
- default:
638
- return c.json({ success: false, error: `Unknown operation: ${operation}. Use 'get', 'append', 'replace_section', or 'update'.` }, 400);
639
- }
640
- }
641
- catch (error) {
642
- console.error('Error executing architecture context tool:', error);
643
- return c.json({ success: false, error: error.message || 'Architecture context tool execution failed' }, 500);
644
- }
645
- }
646
- // Role context tool - read/write the role definition document
647
- async function executeRoleContextTool(c, args) {
648
- const { operation, content, old_text, new_text } = args;
649
- try {
650
- const contextPath = getContextPath();
651
- const filePath = path.join(contextPath, 'role.md');
652
- switch (operation) {
653
- case 'get': {
654
- try {
655
- const fileContent = await fs.readFile(filePath, 'utf-8');
656
- const parsed = matter(fileContent);
657
- return c.json({
658
- success: true,
659
- data: {
660
- content: parsed.content,
661
- frontmatter: parsed.data,
662
- raw: fileContent
663
- },
664
- message: 'Retrieved role context'
665
- });
666
- }
667
- catch (err) {
668
- if (err.code === 'ENOENT') {
669
- return c.json({
670
- success: true,
671
- data: { content: '', frontmatter: {}, raw: '' },
672
- message: 'Role context document does not exist yet. You can create it with an update operation.'
673
- });
674
- }
675
- throw err;
676
- }
677
- }
678
395
  case 'append': {
679
396
  if (!content) {
680
397
  return c.json({ success: false, error: 'Content is required for append operation' }, 400);
681
398
  }
682
- await fs.mkdir(contextPath, { recursive: true });
683
- // Read existing content if file exists
684
- let existingContent = '';
685
- try {
686
- existingContent = await fs.readFile(filePath, 'utf-8');
687
- }
688
- catch (err) {
689
- if (err.code !== 'ENOENT')
690
- throw err;
691
- }
692
- // Append new content with a newline separator
693
- const newContent = existingContent
694
- ? existingContent.trimEnd() + '\n\n' + content
695
- : content;
696
- await fs.writeFile(filePath, newContent, 'utf-8');
399
+ await appendContext(contextType, content);
697
400
  return c.json({
698
401
  success: true,
699
- message: 'Appended content to role context document'
402
+ message: `Appended content to ${contextType} context document`
700
403
  });
701
404
  }
702
405
  case 'replace_section': {
703
406
  if (!old_text || !new_text) {
704
407
  return c.json({ success: false, error: 'Both old_text and new_text are required for replace_section operation' }, 400);
705
408
  }
706
- // Read existing content
707
- let existingContent = '';
708
- try {
709
- existingContent = await fs.readFile(filePath, 'utf-8');
710
- }
711
- catch (err) {
712
- if (err.code === 'ENOENT') {
713
- return c.json({ success: false, error: 'Role context document does not exist. Use append or update to create it first.' }, 404);
714
- }
715
- throw err;
716
- }
717
- // Check if old_text exists in the document
718
- if (!existingContent.includes(old_text)) {
409
+ const result = await replaceContextSection(contextType, old_text, new_text);
410
+ if (!result.success) {
719
411
  return c.json({
720
412
  success: false,
721
- error: 'Could not find the specified text in the document. The text may have been paraphrased or changed.',
722
- fallback_markdown: new_text,
723
- suggestion: 'Here is the replacement text. You can copy it and manually edit the document at /context/role'
724
- }, 400);
413
+ error: result.error,
414
+ fallback_markdown: result.fallbackMarkdown,
415
+ suggestion: result.suggestion
416
+ }, result.error?.includes('does not exist') ? 404 : 400);
725
417
  }
726
- // Replace the text
727
- const updatedContent = existingContent.replace(old_text, new_text);
728
- await fs.writeFile(filePath, updatedContent, 'utf-8');
729
418
  return c.json({
730
419
  success: true,
731
- message: 'Replaced section in role context document'
420
+ message: `Replaced section in ${contextType} context document`
732
421
  });
733
422
  }
734
423
  case 'update': {
735
424
  if (!content) {
736
425
  return c.json({ success: false, error: 'Content is required for update operation' }, 400);
737
426
  }
738
- await fs.mkdir(contextPath, { recursive: true });
739
- await fs.writeFile(filePath, content, 'utf-8');
427
+ await updateContext(contextType, content);
740
428
  return c.json({
741
429
  success: true,
742
- message: 'Updated role context document'
430
+ message: `Updated ${contextType} context document`
743
431
  });
744
432
  }
745
433
  default:
@@ -747,151 +435,19 @@ async function executeRoleContextTool(c, args) {
747
435
  }
748
436
  }
749
437
  catch (error) {
750
- console.error('Error executing role context tool:', error);
751
- return c.json({ success: false, error: error.message || 'Role context tool execution failed' }, 500);
752
- }
753
- }
754
- function getContextPath() {
755
- return path.join(resolveGaitPath(), 'context');
756
- }
757
- function getKnowledgePath() {
758
- return path.join(resolveGaitPath(), 'context', 'knowledge');
759
- }
760
- function slugify(value) {
761
- const slug = value
762
- .toLowerCase()
763
- .replace(/[^a-z0-9\s-]/g, '')
764
- .replace(/\s+/g, '-')
765
- .replace(/--+/g, '-')
766
- .replace(/^-|-$/g, '');
767
- return slug || 'knowledge-entry';
768
- }
769
- function resolveFilename(input, fallbackTitle) {
770
- if (input && input.trim().length > 0) {
771
- const trimmed = path.basename(input.trim());
772
- if (trimmed.endsWith('.md')) {
773
- return trimmed;
774
- }
775
- return `${slugify(trimmed)}.md`;
776
- }
777
- const fallback = fallbackTitle && fallbackTitle.trim().length > 0
778
- ? path.basename(fallbackTitle.trim())
779
- : 'knowledge-entry';
780
- return `${slugify(fallback)}.md`;
781
- }
782
- function extractTitle(content, fallback) {
783
- const headingMatch = content.match(/^#\s+(.+)$/m);
784
- if (headingMatch && headingMatch[1]) {
785
- return headingMatch[1].trim();
438
+ console.error(`Error executing ${contextType} context tool:`, error);
439
+ return c.json({ success: false, error: error.message || `${typeName} context tool execution failed` }, 500);
786
440
  }
787
- const baseName = fallback.replace('.md', '').replace(/[_-]/g, ' ');
788
- return baseName.replace(/\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.slice(1).toLowerCase());
789
441
  }
790
- async function readKnowledgeDocument(basePath, filename) {
791
- const actualFilename = resolveFilename(filename, filename);
792
- const filePath = path.join(basePath, actualFilename);
793
- const [raw, stats] = await Promise.all([
794
- fs.readFile(filePath, 'utf-8'),
795
- fs.stat(filePath)
796
- ]);
797
- const { data, content } = matter(raw);
798
- const metadata = normalizeKnowledgeMetadata(data);
799
- // Use file mtime for updated timestamp instead of frontmatter
800
- metadata.updated = stats.mtime.toISOString();
801
- const title = extractTitle(content, actualFilename);
802
- return {
803
- filename: actualFilename,
804
- title,
805
- metadata,
806
- content
807
- };
808
- }
809
- function extractSnippet(content, query, maxLength = 200) {
810
- const lowerContent = content.toLowerCase();
811
- const lowerQuery = query.toLowerCase();
812
- const terms = lowerQuery.split(/\s+/);
813
- // Find the first occurrence of any search term
814
- let bestIndex = -1;
815
- let matchedTerm = '';
816
- for (const term of terms) {
817
- const index = lowerContent.indexOf(term);
818
- if (index !== -1 && (bestIndex === -1 || index < bestIndex)) {
819
- bestIndex = index;
820
- matchedTerm = term;
821
- }
822
- }
823
- if (bestIndex === -1) {
824
- // No match found, return beginning of content
825
- return content.slice(0, maxLength) + (content.length > maxLength ? '...' : '');
826
- }
827
- // Extract snippet around the match
828
- const start = Math.max(0, bestIndex - 60);
829
- const end = Math.min(content.length, bestIndex + matchedTerm.length + 140);
830
- let snippet = content.slice(start, end);
831
- // Add ellipsis if needed
832
- if (start > 0)
833
- snippet = '...' + snippet;
834
- if (end < content.length)
835
- snippet = snippet + '...';
836
- return snippet;
442
+ // Wrapper functions for each context type (maintain API compatibility)
443
+ async function executeProjectContextTool(c, args) {
444
+ return executeContextTool(c, 'project', args);
837
445
  }
838
- function findMatchedFields(doc, query) {
839
- const lowerQuery = query.toLowerCase();
840
- const terms = lowerQuery.split(/\s+/);
841
- const matched = [];
842
- const checkField = (value, fieldName) => {
843
- if (value && terms.some(term => value.toLowerCase().includes(term))) {
844
- matched.push(fieldName);
845
- }
846
- };
847
- checkField(doc.title, 'title');
848
- checkField(doc.content, 'content');
849
- checkField(doc.metadata?.category, 'category');
850
- checkField(doc.metadata?.summary, 'summary');
851
- if (doc.metadata?.tags?.some(tag => terms.some(term => tag.toLowerCase().includes(term)))) {
852
- matched.push('tags');
853
- }
854
- return matched;
446
+ async function executeArchitectureContextTool(c, args) {
447
+ return executeContextTool(c, 'architecture', args);
855
448
  }
856
- async function listKnowledgeDocuments(basePath, query) {
857
- const files = await fs.readdir(basePath);
858
- const markdownFiles = files.filter(file => file.endsWith('.md'));
859
- const documents = await Promise.all(markdownFiles.map(async (file) => {
860
- try {
861
- return await readKnowledgeDocument(basePath, file);
862
- }
863
- catch (error) {
864
- console.error(`Failed to read knowledge document ${file}:`, error);
865
- return null;
866
- }
867
- }));
868
- const validDocs = documents.filter((doc) => doc !== null);
869
- // If no query, return all documents sorted by filename
870
- if (!query?.trim()) {
871
- return validDocs.sort((a, b) => a.filename.localeCompare(b.filename));
872
- }
873
- // Use Fuse.js for fuzzy search
874
- const fuse = new Fuse(validDocs, {
875
- keys: [
876
- { name: 'title', weight: 2 },
877
- { name: 'content', weight: 1 },
878
- { name: 'metadata.summary', weight: 1.5 },
879
- { name: 'metadata.category', weight: 1 },
880
- { name: 'metadata.tags', weight: 1.5 }
881
- ],
882
- threshold: 0.4,
883
- includeScore: true,
884
- ignoreLocation: true,
885
- minMatchCharLength: 2
886
- });
887
- const results = fuse.search(query);
888
- // Enhance results with snippets and matched fields
889
- return results.map(result => ({
890
- ...result.item,
891
- snippet: extractSnippet(result.item.content, query),
892
- matchedIn: findMatchedFields(result.item, query),
893
- score: result.score
894
- }));
449
+ async function executeRoleContextTool(c, args) {
450
+ return executeContextTool(c, 'role', args);
895
451
  }
896
452
  /**
897
453
  * GET /api/v1/mcp/raw-config