@lovelybunch/api 1.0.35 → 1.0.38
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 +121 -12
- package/dist/routes/api/v1/config/index.js +2 -1
- package/dist/routes/api/v1/config/route.d.ts +13 -3
- package/dist/routes/api/v1/config/route.js +114 -14
- package/dist/routes/api/v1/mcp/index.js +307 -11
- package/dist/routes/api/v1/mcp/route.d.ts +3 -0
- package/dist/routes/api/v1/mcp/route.js +5 -0
- package/dist/routes/api/v1/proposals/[id]/route.d.ts +2 -0
- package/dist/routes/api/v1/proposals/route.d.ts +2 -0
- package/package.json +4 -3
- package/static/assets/index-BlvppCH9.js +719 -0
- package/static/assets/index-D4cXZATc.js +719 -0
- package/static/assets/index-DJ49lCi9.css +33 -0
- package/static/index.html +2 -2
|
@@ -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
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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;
|
|
@@ -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.
|
|
3
|
+
"version": "1.0.38",
|
|
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.
|
|
36
|
-
"@lovelybunch/
|
|
35
|
+
"@lovelybunch/core": "^1.0.38",
|
|
36
|
+
"@lovelybunch/mcp": "^1.0.35",
|
|
37
|
+
"@lovelybunch/types": "^1.0.38",
|
|
37
38
|
"dotenv": "^17.2.1",
|
|
38
39
|
"fuse.js": "^7.0.0",
|
|
39
40
|
"gray-matter": "^4.0.3",
|