@lovelybunch/api 1.0.35 → 1.0.37

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,7 +1,11 @@
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';
5
+ import { FileStorageAdapter } from '../../../../lib/storage/file-storage.js';
6
+ import { getAuthorInfo } from '../../../../lib/user-preferences.js';
4
7
  const app = new Hono();
8
+ const storage = new FileStorageAdapter();
5
9
  function resolveGaitPath() {
6
10
  let basePath;
7
11
  if (process.env.NODE_ENV === 'development' && process.env.GAIT_DEV_ROOT) {
@@ -17,25 +21,317 @@ function resolveGaitPath() {
17
21
  }
18
22
  /**
19
23
  * GET /api/v1/mcp
20
- * Returns the list of MCP server names defined in .gait/mcp/config.json
24
+ * Returns the list of MCP server names and available tools
21
25
  */
22
26
  app.get('/', async (c) => {
23
27
  try {
24
28
  const gaitPath = resolveGaitPath();
25
29
  const mcpConfigPath = path.join(gaitPath, 'mcp', 'config.json');
26
- const raw = await fs.readFile(mcpConfigPath, 'utf-8');
27
- const json = JSON.parse(raw);
28
- const map = json.mcpServers || {};
29
- const names = Object.keys(map);
30
- return c.json({ success: true, servers: names, mcpServers: map });
30
+ let externalServers = {};
31
+ try {
32
+ const raw = await fs.readFile(mcpConfigPath, 'utf-8');
33
+ const json = JSON.parse(raw);
34
+ externalServers = json.mcpServers || {};
35
+ }
36
+ catch (err) {
37
+ // If file missing, continue with empty external servers
38
+ if (err.code !== 'ENOENT') {
39
+ console.error('Error reading MCP config:', err);
40
+ }
41
+ }
42
+ const names = Object.keys(externalServers);
43
+ // Add built-in tools
44
+ const builtInTools = {
45
+ proposals: proposalsTool,
46
+ listProposals: listProposalsTool
47
+ };
48
+ return c.json({
49
+ success: true,
50
+ servers: names,
51
+ mcpServers: externalServers,
52
+ tools: builtInTools
53
+ });
31
54
  }
32
55
  catch (err) {
33
- // If file missing, respond with empty list rather than 500
34
- if (err && err.code === 'ENOENT') {
35
- return c.json({ success: true, servers: [] });
56
+ console.error('Error loading MCP servers:', err);
57
+ return c.json({ success: false, error: 'Failed to load MCP servers', servers: [], tools: {} }, 500);
58
+ }
59
+ });
60
+ /**
61
+ * POST /api/v1/mcp/execute
62
+ * Execute a tool call
63
+ */
64
+ app.post('/execute', async (c) => {
65
+ try {
66
+ const { tool, arguments: args } = await c.req.json();
67
+ if (!tool || !args) {
68
+ return c.json({ success: false, error: 'Tool and arguments are required' }, 400);
69
+ }
70
+ if (tool === 'change_proposals') {
71
+ return await executeProposalsTool(c, args);
72
+ }
73
+ if (tool === 'list_proposals') {
74
+ return await executeListProposalsTool(c, args);
75
+ }
76
+ return c.json({ success: false, error: 'Unknown tool' }, 400);
77
+ }
78
+ catch (error) {
79
+ console.error('Error executing tool:', error);
80
+ return c.json({ success: false, error: 'Tool execution failed' }, 500);
81
+ }
82
+ });
83
+ async function executeProposalsTool(c, args) {
84
+ const { operation, id, filters, proposal } = args;
85
+ try {
86
+ switch (operation) {
87
+ case 'list': {
88
+ const proposals = await storage.listCPs(filters || {});
89
+ return c.json({
90
+ success: true,
91
+ data: proposals,
92
+ message: `Found ${proposals.length} proposals`
93
+ });
94
+ }
95
+ case 'get': {
96
+ if (!id) {
97
+ return c.json({ success: false, error: 'Proposal ID is required for get operation' }, 400);
98
+ }
99
+ const proposal = await storage.getCP(id);
100
+ if (!proposal) {
101
+ return c.json({ success: false, error: 'Proposal not found' }, 404);
102
+ }
103
+ return c.json({
104
+ success: true,
105
+ data: proposal,
106
+ message: `Retrieved proposal ${id}`
107
+ });
108
+ }
109
+ case 'create': {
110
+ if (!proposal) {
111
+ return c.json({ success: false, error: 'Proposal data is required for create operation' }, 400);
112
+ }
113
+ const validatedProposal = validateProposalData(proposal);
114
+ const now = new Date();
115
+ const authorInfo = await getAuthorInfo();
116
+ // Ensure planSteps are properly structured
117
+ const planSteps = (validatedProposal.planSteps || []).map((step, index) => {
118
+ if (typeof step === 'string') {
119
+ return {
120
+ id: `step-${index + 1}`,
121
+ description: step,
122
+ status: 'pending'
123
+ };
124
+ }
125
+ return {
126
+ id: step.id || `step-${index + 1}`,
127
+ description: step.description || '',
128
+ status: step.status || 'pending',
129
+ command: step.command,
130
+ expectedOutcome: step.expectedOutcome,
131
+ output: step.output,
132
+ error: step.error,
133
+ executedAt: step.executedAt
134
+ };
135
+ });
136
+ const newProposal = {
137
+ id: `cp-${Date.now()}`,
138
+ intent: validatedProposal.intent || '',
139
+ content: validatedProposal.content || '',
140
+ author: {
141
+ id: validatedProposal.author?.id || 'current-user',
142
+ name: validatedProposal.author?.name || authorInfo.name || 'Unknown User',
143
+ email: validatedProposal.author?.email || authorInfo.email || '',
144
+ type: validatedProposal.author?.type || 'human'
145
+ },
146
+ planSteps,
147
+ evidence: validatedProposal.evidence || [],
148
+ policies: validatedProposal.policies || [],
149
+ featureFlags: validatedProposal.featureFlags || [],
150
+ experiments: validatedProposal.experiments || [],
151
+ telemetryContracts: validatedProposal.telemetryContracts || [],
152
+ releasePlan: validatedProposal.releasePlan || { strategy: 'immediate' },
153
+ status: validatedProposal.status || 'draft',
154
+ metadata: {
155
+ createdAt: now,
156
+ updatedAt: now,
157
+ reviewers: validatedProposal.metadata?.reviewers || [],
158
+ aiInteractions: validatedProposal.metadata?.aiInteractions || [],
159
+ tags: validatedProposal.metadata?.tags || [],
160
+ priority: validatedProposal.metadata?.priority || 'medium'
161
+ },
162
+ productSpecRef: validatedProposal.productSpecRef
163
+ };
164
+ // Debug logging to identify undefined values
165
+ console.log('Proposal data before storage:', JSON.stringify(newProposal, null, 2));
166
+ await storage.createCP(newProposal);
167
+ return c.json({
168
+ success: true,
169
+ data: newProposal,
170
+ message: `Created proposal ${newProposal.id}`
171
+ });
172
+ }
173
+ case 'update': {
174
+ if (!id) {
175
+ return c.json({ success: false, error: 'Proposal ID is required for update operation' }, 400);
176
+ }
177
+ if (!proposal) {
178
+ return c.json({ success: false, error: 'Proposal data is required for update operation' }, 400);
179
+ }
180
+ const existing = await storage.getCP(id);
181
+ if (!existing) {
182
+ return c.json({ success: false, error: 'Proposal not found' }, 404);
183
+ }
184
+ const validatedUpdates = validateProposalData(proposal);
185
+ // Ensure planSteps are properly structured if they're being updated
186
+ let planSteps = existing.planSteps;
187
+ if (validatedUpdates.planSteps) {
188
+ planSteps = validatedUpdates.planSteps.map((step, index) => {
189
+ if (typeof step === 'string') {
190
+ return {
191
+ id: `step-${index + 1}`,
192
+ description: step,
193
+ status: 'pending'
194
+ };
195
+ }
196
+ return {
197
+ id: step.id || `step-${index + 1}`,
198
+ description: step.description || '',
199
+ status: step.status || 'pending',
200
+ command: step.command,
201
+ expectedOutcome: step.expectedOutcome,
202
+ output: step.output,
203
+ error: step.error,
204
+ executedAt: step.executedAt
205
+ };
206
+ });
207
+ }
208
+ const updatedProposal = {
209
+ ...existing,
210
+ ...validatedUpdates,
211
+ planSteps,
212
+ metadata: {
213
+ ...existing.metadata,
214
+ ...validatedUpdates.metadata,
215
+ updatedAt: new Date()
216
+ }
217
+ };
218
+ await storage.updateCP(id, updatedProposal);
219
+ return c.json({
220
+ success: true,
221
+ data: updatedProposal,
222
+ message: `Updated proposal ${id}`
223
+ });
224
+ }
225
+ case 'delete': {
226
+ if (!id) {
227
+ return c.json({ success: false, error: 'Proposal ID is required for delete operation' }, 400);
228
+ }
229
+ const existing = await storage.getCP(id);
230
+ if (!existing) {
231
+ return c.json({ success: false, error: 'Proposal not found' }, 404);
232
+ }
233
+ await storage.deleteCP(id);
234
+ return c.json({
235
+ success: true,
236
+ message: `Deleted proposal ${id}`
237
+ });
238
+ }
239
+ default:
240
+ return c.json({ success: false, error: 'Invalid operation' }, 400);
241
+ }
242
+ }
243
+ catch (error) {
244
+ console.error('Error executing proposals tool:', error);
245
+ return c.json({ success: false, error: error.message || 'Tool execution failed' }, 500);
246
+ }
247
+ }
248
+ async function executeListProposalsTool(c, args) {
249
+ const { filters } = args;
250
+ try {
251
+ const proposals = await storage.listCPs(filters || {});
252
+ // Return only metadata, not full content
253
+ const metadataOnly = proposals.map(proposal => ({
254
+ id: proposal.id,
255
+ intent: proposal.intent,
256
+ status: proposal.status,
257
+ priority: proposal.metadata?.priority || 'medium',
258
+ tags: proposal.metadata?.tags || [],
259
+ author: {
260
+ name: proposal.author.name,
261
+ email: proposal.author.email
262
+ },
263
+ createdAt: proposal.metadata.createdAt,
264
+ updatedAt: proposal.metadata.updatedAt,
265
+ reviewers: proposal.metadata.reviewers || [],
266
+ productSpecRef: proposal.productSpecRef
267
+ }));
268
+ return c.json({
269
+ success: true,
270
+ data: metadataOnly,
271
+ message: `Found ${metadataOnly.length} proposals`
272
+ });
273
+ }
274
+ catch (error) {
275
+ console.error('Error executing list proposals tool:', error);
276
+ return c.json({ success: false, error: error.message || 'Failed to list proposals' }, 500);
277
+ }
278
+ }
279
+ /**
280
+ * GET /api/v1/mcp/raw-config
281
+ * Returns the raw MCP configuration for editing in settings UI
282
+ */
283
+ app.get('/raw-config', async (c) => {
284
+ try {
285
+ const gaitPath = resolveGaitPath();
286
+ const mcpConfigPath = path.join(gaitPath, 'mcp', 'config.json');
287
+ try {
288
+ const configData = await fs.readFile(mcpConfigPath, 'utf-8');
289
+ const config = JSON.parse(configData);
290
+ return c.json(config);
291
+ }
292
+ catch (error) {
293
+ // If file doesn't exist or is invalid, return empty config
294
+ if (error.code === 'ENOENT') {
295
+ return c.json({});
296
+ }
297
+ console.error('Error reading MCP config for settings:', error);
298
+ return c.json({});
299
+ }
300
+ }
301
+ catch (error) {
302
+ console.error('Error reading MCP raw config:', error);
303
+ return c.json({ error: 'Failed to read MCP configuration' }, 500);
304
+ }
305
+ });
306
+ /**
307
+ * PUT /api/v1/mcp/raw-config
308
+ * Updates the raw MCP configuration from settings UI
309
+ */
310
+ app.put('/raw-config', async (c) => {
311
+ try {
312
+ const body = await c.req.json();
313
+ const gaitPath = resolveGaitPath();
314
+ const mcpConfigPath = path.join(gaitPath, 'mcp', 'config.json');
315
+ // Ensure directory exists
316
+ const mcpDir = path.dirname(mcpConfigPath);
317
+ try {
318
+ await fs.access(mcpDir);
319
+ }
320
+ catch {
321
+ await fs.mkdir(mcpDir, { recursive: true });
36
322
  }
37
- console.error('Error reading MCP config:', err);
38
- return c.json({ success: false, error: 'Failed to load MCP servers', servers: [] }, 500);
323
+ // Validate the configuration structure
324
+ if (typeof body !== 'object') {
325
+ return c.json({ error: 'Invalid configuration format' }, 400);
326
+ }
327
+ // Write the configuration to file
328
+ const configData = JSON.stringify(body, null, 2);
329
+ await fs.writeFile(mcpConfigPath, configData, 'utf-8');
330
+ return c.json({ message: 'MCP configuration updated successfully' });
331
+ }
332
+ catch (error) {
333
+ console.error('Error updating MCP raw config:', error);
334
+ return c.json({ error: 'Failed to update MCP configuration' }, 500);
39
335
  }
40
336
  });
41
337
  export default app;
@@ -0,0 +1,3 @@
1
+ import { Hono } from 'hono';
2
+ declare const app: Hono<import("hono/types").BlankEnv, import("hono/types").BlankSchema, "/">;
3
+ export default app;
@@ -0,0 +1,5 @@
1
+ import { Hono } from 'hono';
2
+ import config from '../config/index.js';
3
+ const app = new Hono();
4
+ app.route('/config', config);
5
+ export default app;
@@ -16,6 +16,7 @@ export declare function GET(c: Context): Promise<(Response & import("hono").Type
16
16
  id: string;
17
17
  name: string;
18
18
  email?: string;
19
+ role?: string;
19
20
  };
20
21
  productSpecRef?: string;
21
22
  planSteps: {
@@ -173,6 +174,7 @@ export declare function PATCH(c: Context): Promise<(Response & import("hono").Ty
173
174
  id: string;
174
175
  name: string;
175
176
  email?: string;
177
+ role?: string;
176
178
  };
177
179
  productSpecRef?: string;
178
180
  planSteps: {
@@ -10,6 +10,7 @@ export declare function GET(c: Context): Promise<(Response & import("hono").Type
10
10
  id: string;
11
11
  name: string;
12
12
  email?: string;
13
+ role?: string;
13
14
  };
14
15
  productSpecRef?: string;
15
16
  planSteps: {
@@ -167,6 +168,7 @@ export declare function POST(c: Context): Promise<(Response & import("hono").Typ
167
168
  id: string;
168
169
  name: string;
169
170
  email?: string;
171
+ role?: string;
170
172
  };
171
173
  productSpecRef?: string;
172
174
  planSteps: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovelybunch/api",
3
- "version": "1.0.35",
3
+ "version": "1.0.37",
4
4
  "type": "module",
5
5
  "main": "dist/server-with-static.js",
6
6
  "exports": {
@@ -32,8 +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.35",
36
- "@lovelybunch/types": "^1.0.35",
35
+ "@lovelybunch/core": "^1.0.37",
36
+ "@lovelybunch/mcp": "^1.0.35",
37
+ "@lovelybunch/types": "^1.0.37",
37
38
  "dotenv": "^17.2.1",
38
39
  "fuse.js": "^7.0.0",
39
40
  "gray-matter": "^4.0.3",