@lovelybunch/api 1.0.69 → 1.0.71-alpha.0
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/lib/storage/file-storage.d.ts +1 -0
- package/dist/lib/storage/file-storage.js +16 -1
- package/dist/routes/api/v1/ai/index.js +2 -0
- package/dist/routes/api/v1/ai/route.js +468 -170
- package/dist/routes/api/v1/ai/tools.d.ts +17 -0
- package/dist/routes/api/v1/ai/tools.js +926 -0
- package/dist/routes/api/v1/mcp/index.js +410 -184
- package/dist/routes/api/v1/resources/generate/route.d.ts +1 -1
- package/dist/routes/api/v1/resources/generate/route.js +66 -16
- package/package.json +4 -4
- package/static/assets/index-B2wygg2W.css +33 -0
- package/static/assets/index-CYlVRUG2.js +969 -0
- package/static/index.html +2 -2
- package/static/assets/index-CfRmV6nM.css +0 -33
- package/static/assets/index-DzYTksNb.js +0 -969
|
@@ -0,0 +1,926 @@
|
|
|
1
|
+
import { homedir } from 'os';
|
|
2
|
+
import { join, resolve as pathResolve, basename } from 'path';
|
|
3
|
+
import { existsSync, readFileSync, promises as fs, createReadStream } from 'fs';
|
|
4
|
+
import readline from 'readline';
|
|
5
|
+
import { getLogsDir } from '@lovelybunch/core';
|
|
6
|
+
import { proposalsReadOnlyTool, knowledgeTool, normalizeKnowledgeMetadata, eventsTool, projectContextTool, architectureContextTool } from '@lovelybunch/mcp';
|
|
7
|
+
import matter from 'gray-matter';
|
|
8
|
+
import { FileStorageAdapter } from '../../../../lib/storage/file-storage.js';
|
|
9
|
+
// Function to get global config API key as fallback
|
|
10
|
+
function getGlobalApiKey(provider) {
|
|
11
|
+
try {
|
|
12
|
+
const platform = process.platform;
|
|
13
|
+
let configDir;
|
|
14
|
+
if (platform === 'win32') {
|
|
15
|
+
configDir = join(process.env.APPDATA || homedir(), 'coconuts');
|
|
16
|
+
}
|
|
17
|
+
else if (platform === 'darwin') {
|
|
18
|
+
configDir = join(homedir(), 'Library', 'Application Support', 'coconuts');
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
configDir = join(process.env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'coconuts');
|
|
22
|
+
}
|
|
23
|
+
const configFile = join(configDir, 'config.json');
|
|
24
|
+
if (!existsSync(configFile)) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const config = JSON.parse(readFileSync(configFile, 'utf-8'));
|
|
28
|
+
return config.apiKeys?.[provider] || null;
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* POST /api/v1/ai/tools
|
|
36
|
+
* Execute tool calls that were detected during streaming
|
|
37
|
+
*/
|
|
38
|
+
export async function POST(c) {
|
|
39
|
+
try {
|
|
40
|
+
const { toolCalls, model, history, context, contextContent, agentPersona, attachedContextFiles, previousContent } = await c.req.json();
|
|
41
|
+
if (!toolCalls || !Array.isArray(toolCalls) || toolCalls.length === 0) {
|
|
42
|
+
return c.json({ error: "Tool calls are required" }, 400);
|
|
43
|
+
}
|
|
44
|
+
let openRouterKey = getGlobalApiKey('openrouter') || process.env.OPENROUTER_API_KEY;
|
|
45
|
+
if (!openRouterKey) {
|
|
46
|
+
return c.json({
|
|
47
|
+
error: "OpenRouter API key not configured",
|
|
48
|
+
hint: "Set via 'coconut config set-key -p openrouter -k <KEY>' or set OPENROUTER_API_KEY env var"
|
|
49
|
+
}, 500);
|
|
50
|
+
}
|
|
51
|
+
// Execute all tool calls
|
|
52
|
+
const toolResults = await executeToolCalls(toolCalls);
|
|
53
|
+
// Build the system prompt
|
|
54
|
+
const baseSystem = getSystemPrompt(context, contextContent);
|
|
55
|
+
const systemPrompt = agentPersona
|
|
56
|
+
? `${baseSystem}\n\nThe following persona is authoritative and overrides general guidance above. You must strictly follow it.\n\n${agentPersona}`
|
|
57
|
+
: baseSystem;
|
|
58
|
+
// Build context messages from attached files
|
|
59
|
+
const contextMessages = Array.isArray(attachedContextFiles)
|
|
60
|
+
? attachedContextFiles
|
|
61
|
+
.filter((file) => file && typeof file.content === 'string' && file.content.trim().length > 0)
|
|
62
|
+
.map((file) => ({
|
|
63
|
+
role: 'system',
|
|
64
|
+
content: `[ATTACHED CONTEXT - FULL CONTENT INCLUDED BELOW - DO NOT USE TOOLS TO FETCH THIS FILE]\nDocument: ${file.name || file.path || 'reference'}\nType: ${file.type || 'context'}\n\n--- BEGIN CONTENT ---\n${file.content}\n--- END CONTENT ---`
|
|
65
|
+
}))
|
|
66
|
+
: [];
|
|
67
|
+
// Reconstruct the conversation with tool results
|
|
68
|
+
const sanitizedHistory = Array.isArray(history)
|
|
69
|
+
? history.filter((m) => m && (m.role === 'user' || m.role === 'assistant') && typeof m.content === 'string')
|
|
70
|
+
: [];
|
|
71
|
+
// Build the assistant message that triggered the tool calls
|
|
72
|
+
const assistantMessage = {
|
|
73
|
+
role: 'assistant',
|
|
74
|
+
content: previousContent || null,
|
|
75
|
+
tool_calls: toolCalls.map((tc) => ({
|
|
76
|
+
id: tc.id,
|
|
77
|
+
type: tc.type || 'function',
|
|
78
|
+
function: tc.function
|
|
79
|
+
}))
|
|
80
|
+
};
|
|
81
|
+
// Build tool result messages
|
|
82
|
+
const toolResultMessages = toolResults.map((result) => ({
|
|
83
|
+
role: 'tool',
|
|
84
|
+
content: result.content,
|
|
85
|
+
tool_call_id: result.tool_call_id
|
|
86
|
+
}));
|
|
87
|
+
let messagesPayload = [
|
|
88
|
+
{ role: 'system', content: systemPrompt },
|
|
89
|
+
...contextMessages,
|
|
90
|
+
...sanitizedHistory,
|
|
91
|
+
assistantMessage,
|
|
92
|
+
...toolResultMessages
|
|
93
|
+
];
|
|
94
|
+
// Prepare tools for potential additional tool calls
|
|
95
|
+
// Note: proposals is read-only, knowledge/project/architecture are read+write, events is read-only
|
|
96
|
+
const tools = [
|
|
97
|
+
{
|
|
98
|
+
type: "function",
|
|
99
|
+
function: proposalsReadOnlyTool
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
type: "function",
|
|
103
|
+
function: knowledgeTool
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
type: "function",
|
|
107
|
+
function: eventsTool
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
type: "function",
|
|
111
|
+
function: projectContextTool
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
type: "function",
|
|
115
|
+
function: architectureContextTool
|
|
116
|
+
}
|
|
117
|
+
];
|
|
118
|
+
// Accumulate all tool calls and results across multiple rounds
|
|
119
|
+
const allToolCalls = [...toolCalls];
|
|
120
|
+
const allToolResults = [...toolResults];
|
|
121
|
+
// Maximum iterations - keep at 1 for simplicity and reliability
|
|
122
|
+
// The AI is instructed to do one operation at a time
|
|
123
|
+
const MAX_TOOL_ITERATIONS = 1;
|
|
124
|
+
let iterations = 0;
|
|
125
|
+
let finalContent = '';
|
|
126
|
+
// Loop until AI gives a response without tool calls
|
|
127
|
+
while (iterations < MAX_TOOL_ITERATIONS) {
|
|
128
|
+
iterations++;
|
|
129
|
+
const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
|
130
|
+
method: "POST",
|
|
131
|
+
headers: {
|
|
132
|
+
"Authorization": `Bearer ${openRouterKey}`,
|
|
133
|
+
"Content-Type": "application/json",
|
|
134
|
+
"HTTP-Referer": process.env.NEXT_PUBLIC_SITE_URL || "http://localhost:3001",
|
|
135
|
+
"X-Title": "Coconut AI Assistant",
|
|
136
|
+
},
|
|
137
|
+
body: JSON.stringify({
|
|
138
|
+
model: model || "openai/gpt-4o",
|
|
139
|
+
messages: messagesPayload,
|
|
140
|
+
temperature: 0.5,
|
|
141
|
+
max_tokens: 4000,
|
|
142
|
+
tools: tools,
|
|
143
|
+
tool_choice: "auto"
|
|
144
|
+
}),
|
|
145
|
+
});
|
|
146
|
+
if (!response.ok) {
|
|
147
|
+
const error = await response.text();
|
|
148
|
+
console.error("OpenRouter API error for tool follow-up:", error);
|
|
149
|
+
return c.json({ error: "Failed to get AI response after tool execution" }, 500);
|
|
150
|
+
}
|
|
151
|
+
const data = await response.json();
|
|
152
|
+
const message = data.choices?.[0]?.message;
|
|
153
|
+
if (!message) {
|
|
154
|
+
console.error("No message in tool follow-up response");
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
// Accumulate content if present
|
|
158
|
+
if (message.content) {
|
|
159
|
+
finalContent += (finalContent ? '\n\n' : '') + message.content;
|
|
160
|
+
}
|
|
161
|
+
// Check if AI wants to make more tool calls
|
|
162
|
+
if (message.tool_calls && message.tool_calls.length > 0) {
|
|
163
|
+
console.log(`Tool iteration ${iterations}: AI requested ${message.tool_calls.length} more tool call(s)`);
|
|
164
|
+
// Execute the new tool calls
|
|
165
|
+
const newToolResults = await executeToolCalls(message.tool_calls);
|
|
166
|
+
// Accumulate tool calls and results
|
|
167
|
+
allToolCalls.push(...message.tool_calls);
|
|
168
|
+
allToolResults.push(...newToolResults);
|
|
169
|
+
// Add the assistant's tool call message and results to the conversation
|
|
170
|
+
messagesPayload = [
|
|
171
|
+
...messagesPayload,
|
|
172
|
+
{
|
|
173
|
+
role: 'assistant',
|
|
174
|
+
content: message.content || null,
|
|
175
|
+
tool_calls: message.tool_calls
|
|
176
|
+
},
|
|
177
|
+
...newToolResults.map((result) => ({
|
|
178
|
+
role: 'tool',
|
|
179
|
+
content: result.content,
|
|
180
|
+
tool_call_id: result.tool_call_id
|
|
181
|
+
}))
|
|
182
|
+
];
|
|
183
|
+
// Continue loop to get next response
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
// No more tool calls - we have the final response
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
if (iterations >= MAX_TOOL_ITERATIONS && allToolCalls.length > 0) {
|
|
190
|
+
// Don't add the note if we completed successfully
|
|
191
|
+
const hasErrors = allToolResults.some(r => {
|
|
192
|
+
try {
|
|
193
|
+
return !JSON.parse(r.content).success;
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
if (!hasErrors) {
|
|
200
|
+
finalContent += '\n\nOperation completed. Let me know if you need anything else created.';
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// Check if any tool results had errors
|
|
204
|
+
const failedResults = allToolResults.filter(r => {
|
|
205
|
+
try {
|
|
206
|
+
const parsed = JSON.parse(r.content);
|
|
207
|
+
return !parsed.success;
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
if (failedResults.length > 0 && !finalContent.includes('encountered')) {
|
|
214
|
+
finalContent += `\n\n*Note: ${failedResults.length} tool operation(s) encountered errors. You can ask me to retry those.*`;
|
|
215
|
+
}
|
|
216
|
+
return c.json({
|
|
217
|
+
response: finalContent || "Tool execution completed",
|
|
218
|
+
toolCalls: allToolCalls,
|
|
219
|
+
toolResults: allToolResults
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
console.error("Tool execution API error:", error);
|
|
224
|
+
return c.json({ error: "Internal server error" }, 500);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
async function executeToolCalls(toolCalls) {
|
|
228
|
+
const storage = new FileStorageAdapter();
|
|
229
|
+
const resultPromises = toolCalls.map(async (toolCall) => {
|
|
230
|
+
try {
|
|
231
|
+
const functionName = toolCall.function?.name || toolCall.name;
|
|
232
|
+
// Parse function arguments with error handling for malformed JSON
|
|
233
|
+
let functionArgs;
|
|
234
|
+
try {
|
|
235
|
+
if (toolCall.function?.arguments && typeof toolCall.function.arguments === 'string') {
|
|
236
|
+
// Try to sanitize common JSON issues before parsing
|
|
237
|
+
let argsStr = toolCall.function.arguments;
|
|
238
|
+
// Remove trailing commas before ] or }
|
|
239
|
+
argsStr = argsStr.replace(/,(\s*[}\]])/g, '$1');
|
|
240
|
+
// Try parsing the sanitized string
|
|
241
|
+
functionArgs = JSON.parse(argsStr);
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
functionArgs = toolCall.function?.arguments || toolCall.arguments;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
catch (parseError) {
|
|
248
|
+
console.error('Failed to parse tool arguments:', {
|
|
249
|
+
functionName,
|
|
250
|
+
arguments: toolCall.function?.arguments?.substring(0, 500),
|
|
251
|
+
error: parseError instanceof Error ? parseError.message : 'Unknown parse error'
|
|
252
|
+
});
|
|
253
|
+
return {
|
|
254
|
+
tool_call_id: toolCall.id,
|
|
255
|
+
content: JSON.stringify({
|
|
256
|
+
success: false,
|
|
257
|
+
error: `Invalid tool arguments: ${parseError instanceof Error ? parseError.message : 'JSON parse error'}. Please retry with simpler content.`
|
|
258
|
+
})
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
let result = { success: false, error: 'Unknown tool' };
|
|
262
|
+
if (functionName === 'change_proposals') {
|
|
263
|
+
result = await executeProposalsToolDirect(functionArgs, storage);
|
|
264
|
+
}
|
|
265
|
+
else if (functionName === 'knowledge_documents') {
|
|
266
|
+
result = await executeKnowledgeToolDirect(functionArgs);
|
|
267
|
+
}
|
|
268
|
+
else if (functionName === 'activity_events') {
|
|
269
|
+
result = await executeEventsToolDirect(functionArgs);
|
|
270
|
+
}
|
|
271
|
+
else if (functionName === 'project_context') {
|
|
272
|
+
result = await executeProjectContextToolDirect(functionArgs);
|
|
273
|
+
}
|
|
274
|
+
else if (functionName === 'architecture_context') {
|
|
275
|
+
result = await executeArchitectureContextToolDirect(functionArgs);
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
tool_call_id: toolCall.id,
|
|
279
|
+
content: JSON.stringify({
|
|
280
|
+
success: result.success,
|
|
281
|
+
data: result.data,
|
|
282
|
+
message: result.message,
|
|
283
|
+
error: result.error
|
|
284
|
+
})
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
return {
|
|
289
|
+
tool_call_id: toolCall.id,
|
|
290
|
+
content: JSON.stringify({
|
|
291
|
+
success: false,
|
|
292
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
293
|
+
})
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
return Promise.all(resultPromises);
|
|
298
|
+
}
|
|
299
|
+
// Proposals tool is READ-ONLY - only list and get operations are supported
|
|
300
|
+
async function executeProposalsToolDirect(args, storage) {
|
|
301
|
+
const { operation, id, filters } = args;
|
|
302
|
+
try {
|
|
303
|
+
switch (operation) {
|
|
304
|
+
case 'list': {
|
|
305
|
+
const proposals = await storage.listCPs(filters || {});
|
|
306
|
+
return {
|
|
307
|
+
success: true,
|
|
308
|
+
data: proposals,
|
|
309
|
+
message: `Found ${proposals.length} proposals`
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
case 'get': {
|
|
313
|
+
if (!id) {
|
|
314
|
+
return { success: false, error: 'Proposal ID is required for get operation' };
|
|
315
|
+
}
|
|
316
|
+
const proposal = await storage.getCP(id);
|
|
317
|
+
if (!proposal) {
|
|
318
|
+
return { success: false, error: 'Proposal not found' };
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
success: true,
|
|
322
|
+
data: proposal,
|
|
323
|
+
message: `Retrieved proposal ${id}`
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
default:
|
|
327
|
+
return { success: false, 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.` };
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
catch (error) {
|
|
331
|
+
console.error('Error executing proposals tool:', error);
|
|
332
|
+
return { success: false, error: error.message || 'Tool execution failed' };
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
// Helper to strip undefined/null values from an object (gray-matter can't handle them)
|
|
336
|
+
function stripUndefined(obj) {
|
|
337
|
+
const result = {};
|
|
338
|
+
for (const key of Object.keys(obj)) {
|
|
339
|
+
const value = obj[key];
|
|
340
|
+
if (value !== undefined && value !== null) {
|
|
341
|
+
if (Array.isArray(value)) {
|
|
342
|
+
// Filter out undefined/null from arrays too
|
|
343
|
+
result[key] = value.filter(v => v !== undefined && v !== null);
|
|
344
|
+
}
|
|
345
|
+
else if (typeof value === 'object' && !Array.isArray(value)) {
|
|
346
|
+
// Recursively strip nested objects
|
|
347
|
+
const stripped = stripUndefined(value);
|
|
348
|
+
if (Object.keys(stripped).length > 0) {
|
|
349
|
+
result[key] = stripped;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
result[key] = value;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return result;
|
|
358
|
+
}
|
|
359
|
+
async function executeKnowledgeToolDirect(args) {
|
|
360
|
+
const { operation, filename, title, content, metadata, summary, query } = args;
|
|
361
|
+
try {
|
|
362
|
+
const knowledgePath = getKnowledgeBasePath();
|
|
363
|
+
await fs.mkdir(knowledgePath, { recursive: true });
|
|
364
|
+
switch (operation) {
|
|
365
|
+
case 'list': {
|
|
366
|
+
const searchQuery = query && typeof query === 'string' ? query : undefined;
|
|
367
|
+
const documents = await listKnowledgeDocuments(knowledgePath, searchQuery);
|
|
368
|
+
return {
|
|
369
|
+
success: true,
|
|
370
|
+
data: documents,
|
|
371
|
+
message: searchQuery
|
|
372
|
+
? `Found ${documents.length} knowledge documents matching "${searchQuery}"`
|
|
373
|
+
: `Found ${documents.length} knowledge documents`
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
case 'get': {
|
|
377
|
+
if (!filename) {
|
|
378
|
+
return { success: false, error: 'Filename is required for get operation' };
|
|
379
|
+
}
|
|
380
|
+
const document = await readKnowledgeDocument(knowledgePath, filename);
|
|
381
|
+
if (!document) {
|
|
382
|
+
return { success: false, error: 'Knowledge document not found' };
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
success: true,
|
|
386
|
+
data: document,
|
|
387
|
+
message: `Loaded knowledge document ${document.filename}`
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
case 'create': {
|
|
391
|
+
if (!title || !content) {
|
|
392
|
+
return { success: false, error: 'Title and content are required for create operation' };
|
|
393
|
+
}
|
|
394
|
+
const targetFilename = resolveKnowledgeFilename(filename, title);
|
|
395
|
+
const filePath = join(knowledgePath, targetFilename);
|
|
396
|
+
try {
|
|
397
|
+
await fs.access(filePath);
|
|
398
|
+
return { success: false, error: 'A knowledge document with this filename already exists' };
|
|
399
|
+
}
|
|
400
|
+
catch {
|
|
401
|
+
// File does not exist - good
|
|
402
|
+
}
|
|
403
|
+
// Build clean metadata with defaults, stripping any undefined values
|
|
404
|
+
const baseMetadata = {
|
|
405
|
+
version: '1.0',
|
|
406
|
+
updated: new Date().toISOString().split('T')[0],
|
|
407
|
+
type: 'knowledge',
|
|
408
|
+
tags: [],
|
|
409
|
+
sources: [],
|
|
410
|
+
related: [],
|
|
411
|
+
audience: []
|
|
412
|
+
};
|
|
413
|
+
// Only add summary if it's a valid string
|
|
414
|
+
if (typeof summary === 'string' && summary.trim()) {
|
|
415
|
+
baseMetadata.summary = summary.trim();
|
|
416
|
+
}
|
|
417
|
+
// Merge with provided metadata, but strip undefined values
|
|
418
|
+
const safeMetadata = metadata && typeof metadata === 'object'
|
|
419
|
+
? stripUndefined(metadata)
|
|
420
|
+
: {};
|
|
421
|
+
const finalMetadata = stripUndefined({
|
|
422
|
+
...baseMetadata,
|
|
423
|
+
...safeMetadata
|
|
424
|
+
});
|
|
425
|
+
const markdown = matter.stringify(content, finalMetadata);
|
|
426
|
+
await fs.writeFile(filePath, markdown, 'utf-8');
|
|
427
|
+
return {
|
|
428
|
+
success: true,
|
|
429
|
+
data: {
|
|
430
|
+
filename: targetFilename,
|
|
431
|
+
title,
|
|
432
|
+
metadata: finalMetadata,
|
|
433
|
+
content
|
|
434
|
+
},
|
|
435
|
+
message: `Created knowledge document ${targetFilename}`
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
case 'update': {
|
|
439
|
+
if (!filename || !content) {
|
|
440
|
+
return { success: false, error: 'Filename and content are required for update operation' };
|
|
441
|
+
}
|
|
442
|
+
const currentFilename = resolveKnowledgeFilename(filename, title || 'knowledge-entry');
|
|
443
|
+
const currentPath = join(knowledgePath, currentFilename);
|
|
444
|
+
let existingDocument = null;
|
|
445
|
+
try {
|
|
446
|
+
existingDocument = await readKnowledgeDocument(knowledgePath, currentFilename);
|
|
447
|
+
}
|
|
448
|
+
catch (error) {
|
|
449
|
+
if (error.code === 'ENOENT') {
|
|
450
|
+
return { success: false, error: 'Knowledge document not found' };
|
|
451
|
+
}
|
|
452
|
+
throw error;
|
|
453
|
+
}
|
|
454
|
+
if (!existingDocument) {
|
|
455
|
+
return { success: false, error: 'Knowledge document not found' };
|
|
456
|
+
}
|
|
457
|
+
const nextTitle = title || existingDocument.title;
|
|
458
|
+
const shouldRename = Boolean(title && slugify(title) !== slugify(existingDocument.title));
|
|
459
|
+
const nextFilename = shouldRename ? resolveKnowledgeFilename(undefined, nextTitle) : currentFilename;
|
|
460
|
+
const nextPath = join(knowledgePath, nextFilename);
|
|
461
|
+
if (nextFilename !== currentFilename) {
|
|
462
|
+
try {
|
|
463
|
+
await fs.access(nextPath);
|
|
464
|
+
return { success: false, error: 'A document with the requested title already exists' };
|
|
465
|
+
}
|
|
466
|
+
catch {
|
|
467
|
+
// Safe to rename
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
// Safely merge metadata, stripping undefined values
|
|
471
|
+
const safeExistingMetadata = stripUndefined(existingDocument.metadata || {});
|
|
472
|
+
const safeNewMetadata = metadata && typeof metadata === 'object'
|
|
473
|
+
? stripUndefined(metadata)
|
|
474
|
+
: {};
|
|
475
|
+
const mergedMetadata = stripUndefined({
|
|
476
|
+
...safeExistingMetadata,
|
|
477
|
+
...safeNewMetadata,
|
|
478
|
+
updated: new Date().toISOString().split('T')[0],
|
|
479
|
+
...(typeof summary === 'string' && summary.trim() ? { summary: summary.trim() } : {})
|
|
480
|
+
});
|
|
481
|
+
const markdown = matter.stringify(content, mergedMetadata);
|
|
482
|
+
await fs.writeFile(nextPath, markdown, 'utf-8');
|
|
483
|
+
if (nextFilename !== currentFilename) {
|
|
484
|
+
await fs.unlink(currentPath);
|
|
485
|
+
}
|
|
486
|
+
return {
|
|
487
|
+
success: true,
|
|
488
|
+
data: {
|
|
489
|
+
filename: nextFilename,
|
|
490
|
+
title: nextTitle,
|
|
491
|
+
metadata: mergedMetadata,
|
|
492
|
+
content
|
|
493
|
+
},
|
|
494
|
+
message: `Updated knowledge document ${nextFilename}`
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
default:
|
|
498
|
+
return { success: false, error: `Unknown operation: ${operation}` };
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
catch (error) {
|
|
502
|
+
console.error('Error executing knowledge tool:', error);
|
|
503
|
+
return { success: false, error: error.message || 'Knowledge tool execution failed' };
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
// Events tool is READ-ONLY - returns recent activity events
|
|
507
|
+
async function executeEventsToolDirect(args) {
|
|
508
|
+
const { operation, limit = 20, kind } = args;
|
|
509
|
+
if (operation !== 'list') {
|
|
510
|
+
return {
|
|
511
|
+
success: false,
|
|
512
|
+
error: `Events are read-only. Only 'list' operation is supported.`
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
try {
|
|
516
|
+
const eventsDir = getLogsDir();
|
|
517
|
+
const currentFile = join(eventsDir, 'events-current.jsonl');
|
|
518
|
+
// Check if file exists
|
|
519
|
+
try {
|
|
520
|
+
await fs.access(currentFile);
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
return {
|
|
524
|
+
success: true,
|
|
525
|
+
data: [],
|
|
526
|
+
message: 'No events found'
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
// Read all events from file
|
|
530
|
+
const allEvents = [];
|
|
531
|
+
const fileStream = createReadStream(currentFile);
|
|
532
|
+
const rl = readline.createInterface({
|
|
533
|
+
input: fileStream,
|
|
534
|
+
crlfDelay: Infinity,
|
|
535
|
+
});
|
|
536
|
+
for await (const line of rl) {
|
|
537
|
+
if (!line.trim())
|
|
538
|
+
continue;
|
|
539
|
+
try {
|
|
540
|
+
const event = JSON.parse(line);
|
|
541
|
+
// Apply kind filter if specified
|
|
542
|
+
if (kind && typeof kind === 'string') {
|
|
543
|
+
if (!event.kind || !event.kind.startsWith(kind)) {
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
allEvents.push(event);
|
|
548
|
+
}
|
|
549
|
+
catch {
|
|
550
|
+
// Skip malformed lines
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
// Sort by seq descending (most recent first) and limit
|
|
555
|
+
const cappedLimit = Math.min(Math.max(1, limit), 100);
|
|
556
|
+
const sortedEvents = allEvents
|
|
557
|
+
.sort((a, b) => (b.seq || 0) - (a.seq || 0))
|
|
558
|
+
.slice(0, cappedLimit);
|
|
559
|
+
// Format events for readability
|
|
560
|
+
const formattedEvents = sortedEvents.map(event => ({
|
|
561
|
+
seq: event.seq,
|
|
562
|
+
kind: event.kind,
|
|
563
|
+
actor: event.actor,
|
|
564
|
+
subject: event.subject,
|
|
565
|
+
timestamp: event.ts,
|
|
566
|
+
level: event.level,
|
|
567
|
+
tags: event.tags,
|
|
568
|
+
summary: event.payload?.summary || event.payload?.message || event.payload?.title || undefined
|
|
569
|
+
}));
|
|
570
|
+
return {
|
|
571
|
+
success: true,
|
|
572
|
+
data: formattedEvents,
|
|
573
|
+
message: kind
|
|
574
|
+
? `Found ${formattedEvents.length} recent '${kind}' events`
|
|
575
|
+
: `Found ${formattedEvents.length} recent events`
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
catch (error) {
|
|
579
|
+
console.error('Error executing events tool:', error);
|
|
580
|
+
return { success: false, error: error.message || 'Events tool execution failed' };
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
// Project context tool - read/write the project definition document
|
|
584
|
+
async function executeProjectContextToolDirect(args) {
|
|
585
|
+
const { operation, content, old_text, new_text } = args;
|
|
586
|
+
try {
|
|
587
|
+
const contextPath = getContextBasePath();
|
|
588
|
+
const filePath = join(contextPath, 'project.md');
|
|
589
|
+
switch (operation) {
|
|
590
|
+
case 'get': {
|
|
591
|
+
try {
|
|
592
|
+
const fileContent = await fs.readFile(filePath, 'utf-8');
|
|
593
|
+
const parsed = matter(fileContent);
|
|
594
|
+
return {
|
|
595
|
+
success: true,
|
|
596
|
+
data: {
|
|
597
|
+
content: parsed.content,
|
|
598
|
+
frontmatter: parsed.data,
|
|
599
|
+
raw: fileContent
|
|
600
|
+
},
|
|
601
|
+
message: 'Retrieved project context'
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
catch (err) {
|
|
605
|
+
if (err.code === 'ENOENT') {
|
|
606
|
+
return {
|
|
607
|
+
success: true,
|
|
608
|
+
data: { content: '', frontmatter: {}, raw: '' },
|
|
609
|
+
message: 'Project context document does not exist yet. You can create it with an update operation.'
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
throw err;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
case 'append': {
|
|
616
|
+
if (!content) {
|
|
617
|
+
return { success: false, error: 'Content is required for append operation' };
|
|
618
|
+
}
|
|
619
|
+
await fs.mkdir(contextPath, { recursive: true });
|
|
620
|
+
// Read existing content if file exists
|
|
621
|
+
let existingContent = '';
|
|
622
|
+
try {
|
|
623
|
+
existingContent = await fs.readFile(filePath, 'utf-8');
|
|
624
|
+
}
|
|
625
|
+
catch (err) {
|
|
626
|
+
if (err.code !== 'ENOENT')
|
|
627
|
+
throw err;
|
|
628
|
+
}
|
|
629
|
+
// Append new content with a newline separator
|
|
630
|
+
const newContent = existingContent
|
|
631
|
+
? existingContent.trimEnd() + '\n\n' + content
|
|
632
|
+
: content;
|
|
633
|
+
await fs.writeFile(filePath, newContent, 'utf-8');
|
|
634
|
+
return {
|
|
635
|
+
success: true,
|
|
636
|
+
message: 'Appended content to project context document'
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
case 'replace_section': {
|
|
640
|
+
if (!old_text || !new_text) {
|
|
641
|
+
return { success: false, error: 'Both old_text and new_text are required for replace_section operation' };
|
|
642
|
+
}
|
|
643
|
+
// Read existing content
|
|
644
|
+
let existingContent = '';
|
|
645
|
+
try {
|
|
646
|
+
existingContent = await fs.readFile(filePath, 'utf-8');
|
|
647
|
+
}
|
|
648
|
+
catch (err) {
|
|
649
|
+
if (err.code === 'ENOENT') {
|
|
650
|
+
return { success: false, error: 'Project context document does not exist. Use append or update to create it first.' };
|
|
651
|
+
}
|
|
652
|
+
throw err;
|
|
653
|
+
}
|
|
654
|
+
// Check if old_text exists in the document
|
|
655
|
+
if (!existingContent.includes(old_text)) {
|
|
656
|
+
return {
|
|
657
|
+
success: false,
|
|
658
|
+
error: 'Could not find the specified text in the document. The text may have been paraphrased or changed.',
|
|
659
|
+
fallback_markdown: new_text,
|
|
660
|
+
suggestion: 'Here is the replacement text. You can copy it and manually edit the document at /context/project'
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
// Replace the text
|
|
664
|
+
const updatedContent = existingContent.replace(old_text, new_text);
|
|
665
|
+
await fs.writeFile(filePath, updatedContent, 'utf-8');
|
|
666
|
+
return {
|
|
667
|
+
success: true,
|
|
668
|
+
message: 'Replaced section in project context document'
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
case 'update': {
|
|
672
|
+
if (!content) {
|
|
673
|
+
return { success: false, error: 'Content is required for update operation' };
|
|
674
|
+
}
|
|
675
|
+
await fs.mkdir(contextPath, { recursive: true });
|
|
676
|
+
await fs.writeFile(filePath, content, 'utf-8');
|
|
677
|
+
return {
|
|
678
|
+
success: true,
|
|
679
|
+
message: 'Updated project context document'
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
default:
|
|
683
|
+
return { success: false, error: `Unknown operation: ${operation}. Use 'get', 'append', 'replace_section', or 'update'.` };
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
catch (error) {
|
|
687
|
+
console.error('Error executing project context tool:', error);
|
|
688
|
+
return { success: false, error: error.message || 'Project context tool execution failed' };
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
// Architecture context tool - read/write the architecture document
|
|
692
|
+
async function executeArchitectureContextToolDirect(args) {
|
|
693
|
+
const { operation, content, old_text, new_text } = args;
|
|
694
|
+
try {
|
|
695
|
+
const contextPath = getContextBasePath();
|
|
696
|
+
const filePath = join(contextPath, 'architecture.md');
|
|
697
|
+
switch (operation) {
|
|
698
|
+
case 'get': {
|
|
699
|
+
try {
|
|
700
|
+
const fileContent = await fs.readFile(filePath, 'utf-8');
|
|
701
|
+
const parsed = matter(fileContent);
|
|
702
|
+
return {
|
|
703
|
+
success: true,
|
|
704
|
+
data: {
|
|
705
|
+
content: parsed.content,
|
|
706
|
+
frontmatter: parsed.data,
|
|
707
|
+
raw: fileContent
|
|
708
|
+
},
|
|
709
|
+
message: 'Retrieved architecture context'
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
catch (err) {
|
|
713
|
+
if (err.code === 'ENOENT') {
|
|
714
|
+
return {
|
|
715
|
+
success: true,
|
|
716
|
+
data: { content: '', frontmatter: {}, raw: '' },
|
|
717
|
+
message: 'Architecture context document does not exist yet. You can create it with an update operation.'
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
throw err;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
case 'append': {
|
|
724
|
+
if (!content) {
|
|
725
|
+
return { success: false, error: 'Content is required for append operation' };
|
|
726
|
+
}
|
|
727
|
+
await fs.mkdir(contextPath, { recursive: true });
|
|
728
|
+
// Read existing content if file exists
|
|
729
|
+
let existingContent = '';
|
|
730
|
+
try {
|
|
731
|
+
existingContent = await fs.readFile(filePath, 'utf-8');
|
|
732
|
+
}
|
|
733
|
+
catch (err) {
|
|
734
|
+
if (err.code !== 'ENOENT')
|
|
735
|
+
throw err;
|
|
736
|
+
}
|
|
737
|
+
// Append new content with a newline separator
|
|
738
|
+
const newContent = existingContent
|
|
739
|
+
? existingContent.trimEnd() + '\n\n' + content
|
|
740
|
+
: content;
|
|
741
|
+
await fs.writeFile(filePath, newContent, 'utf-8');
|
|
742
|
+
return {
|
|
743
|
+
success: true,
|
|
744
|
+
message: 'Appended content to architecture context document'
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
case 'replace_section': {
|
|
748
|
+
if (!old_text || !new_text) {
|
|
749
|
+
return { success: false, error: 'Both old_text and new_text are required for replace_section operation' };
|
|
750
|
+
}
|
|
751
|
+
// Read existing content
|
|
752
|
+
let existingContent = '';
|
|
753
|
+
try {
|
|
754
|
+
existingContent = await fs.readFile(filePath, 'utf-8');
|
|
755
|
+
}
|
|
756
|
+
catch (err) {
|
|
757
|
+
if (err.code === 'ENOENT') {
|
|
758
|
+
return { success: false, error: 'Architecture context document does not exist. Use append or update to create it first.' };
|
|
759
|
+
}
|
|
760
|
+
throw err;
|
|
761
|
+
}
|
|
762
|
+
// Check if old_text exists in the document
|
|
763
|
+
if (!existingContent.includes(old_text)) {
|
|
764
|
+
return {
|
|
765
|
+
success: false,
|
|
766
|
+
error: 'Could not find the specified text in the document. The text may have been paraphrased or changed.',
|
|
767
|
+
fallback_markdown: new_text,
|
|
768
|
+
suggestion: 'Here is the replacement text. You can copy it and manually edit the document at /context/architecture'
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
// Replace the text
|
|
772
|
+
const updatedContent = existingContent.replace(old_text, new_text);
|
|
773
|
+
await fs.writeFile(filePath, updatedContent, 'utf-8');
|
|
774
|
+
return {
|
|
775
|
+
success: true,
|
|
776
|
+
message: 'Replaced section in architecture context document'
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
case 'update': {
|
|
780
|
+
if (!content) {
|
|
781
|
+
return { success: false, error: 'Content is required for update operation' };
|
|
782
|
+
}
|
|
783
|
+
await fs.mkdir(contextPath, { recursive: true });
|
|
784
|
+
await fs.writeFile(filePath, content, 'utf-8');
|
|
785
|
+
return {
|
|
786
|
+
success: true,
|
|
787
|
+
message: 'Updated architecture context document'
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
default:
|
|
791
|
+
return { success: false, error: `Unknown operation: ${operation}. Use 'get', 'append', 'replace_section', or 'update'.` };
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
catch (error) {
|
|
795
|
+
console.error('Error executing architecture context tool:', error);
|
|
796
|
+
return { success: false, error: error.message || 'Architecture context tool execution failed' };
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
function getContextBasePath() {
|
|
800
|
+
let basePath;
|
|
801
|
+
if (process.env.NODE_ENV === 'development' && process.env.GAIT_DEV_ROOT) {
|
|
802
|
+
basePath = process.env.GAIT_DEV_ROOT;
|
|
803
|
+
}
|
|
804
|
+
else if (process.env.GAIT_DATA_PATH) {
|
|
805
|
+
basePath = pathResolve(process.env.GAIT_DATA_PATH, '.nut');
|
|
806
|
+
}
|
|
807
|
+
else {
|
|
808
|
+
basePath = pathResolve(process.cwd(), '.nut');
|
|
809
|
+
}
|
|
810
|
+
return join(basePath, 'context');
|
|
811
|
+
}
|
|
812
|
+
function getKnowledgeBasePath() {
|
|
813
|
+
let basePath;
|
|
814
|
+
if (process.env.NODE_ENV === 'development' && process.env.GAIT_DEV_ROOT) {
|
|
815
|
+
basePath = process.env.GAIT_DEV_ROOT;
|
|
816
|
+
}
|
|
817
|
+
else if (process.env.GAIT_DATA_PATH) {
|
|
818
|
+
basePath = pathResolve(process.env.GAIT_DATA_PATH, '.nut');
|
|
819
|
+
}
|
|
820
|
+
else {
|
|
821
|
+
basePath = pathResolve(process.cwd(), '.nut');
|
|
822
|
+
}
|
|
823
|
+
return join(basePath, 'context', 'knowledge');
|
|
824
|
+
}
|
|
825
|
+
function slugify(value) {
|
|
826
|
+
const slug = value
|
|
827
|
+
.toLowerCase()
|
|
828
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
829
|
+
.replace(/\s+/g, '-')
|
|
830
|
+
.replace(/--+/g, '-')
|
|
831
|
+
.replace(/^-|-$/g, '');
|
|
832
|
+
return slug || 'knowledge-entry';
|
|
833
|
+
}
|
|
834
|
+
function resolveKnowledgeFilename(input, fallbackTitle) {
|
|
835
|
+
if (input && input.trim().length > 0) {
|
|
836
|
+
const trimmed = basename(input.trim());
|
|
837
|
+
if (trimmed.endsWith('.md')) {
|
|
838
|
+
return trimmed;
|
|
839
|
+
}
|
|
840
|
+
return `${slugify(trimmed)}.md`;
|
|
841
|
+
}
|
|
842
|
+
const fallback = fallbackTitle && fallbackTitle.trim().length > 0
|
|
843
|
+
? basename(fallbackTitle.trim())
|
|
844
|
+
: 'knowledge-entry';
|
|
845
|
+
return `${slugify(fallback)}.md`;
|
|
846
|
+
}
|
|
847
|
+
function extractKnowledgeTitle(content, fallback) {
|
|
848
|
+
const headingMatch = content.match(/^#\s+(.+)$/m);
|
|
849
|
+
if (headingMatch && headingMatch[1]) {
|
|
850
|
+
return headingMatch[1].trim();
|
|
851
|
+
}
|
|
852
|
+
const baseName = fallback.replace('.md', '').replace(/[_-]/g, ' ');
|
|
853
|
+
return baseName.replace(/\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.slice(1).toLowerCase());
|
|
854
|
+
}
|
|
855
|
+
async function readKnowledgeDocument(basePath, filename) {
|
|
856
|
+
const actualFilename = resolveKnowledgeFilename(filename, filename);
|
|
857
|
+
const filePath = join(basePath, actualFilename);
|
|
858
|
+
const raw = await fs.readFile(filePath, 'utf-8');
|
|
859
|
+
const { data, content } = matter(raw);
|
|
860
|
+
const normalizedMetadata = normalizeKnowledgeMetadata(data);
|
|
861
|
+
const title = extractKnowledgeTitle(content, actualFilename);
|
|
862
|
+
return {
|
|
863
|
+
filename: actualFilename,
|
|
864
|
+
title,
|
|
865
|
+
metadata: normalizedMetadata,
|
|
866
|
+
content
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
async function listKnowledgeDocuments(basePath, query) {
|
|
870
|
+
const files = await fs.readdir(basePath);
|
|
871
|
+
const markdownFiles = files.filter(file => file.endsWith('.md'));
|
|
872
|
+
const documents = await Promise.all(markdownFiles.map(async (file) => {
|
|
873
|
+
try {
|
|
874
|
+
return await readKnowledgeDocument(basePath, file);
|
|
875
|
+
}
|
|
876
|
+
catch (error) {
|
|
877
|
+
console.error(`Failed to read knowledge document ${file}:`, error);
|
|
878
|
+
return null;
|
|
879
|
+
}
|
|
880
|
+
}));
|
|
881
|
+
const validDocs = documents.filter((doc) => doc !== null);
|
|
882
|
+
if (!query?.trim()) {
|
|
883
|
+
return validDocs.sort((a, b) => a.filename.localeCompare(b.filename));
|
|
884
|
+
}
|
|
885
|
+
// Simple text search for the tools endpoint
|
|
886
|
+
const lowerQuery = query.toLowerCase();
|
|
887
|
+
return validDocs
|
|
888
|
+
.filter(doc => doc.title.toLowerCase().includes(lowerQuery) ||
|
|
889
|
+
doc.content.toLowerCase().includes(lowerQuery) ||
|
|
890
|
+
doc.metadata?.summary?.toLowerCase().includes(lowerQuery))
|
|
891
|
+
.sort((a, b) => a.filename.localeCompare(b.filename));
|
|
892
|
+
}
|
|
893
|
+
function getSystemPrompt(context, contextContent) {
|
|
894
|
+
const basePrompt = `You are the Coconut 🥥 AI assistant for agent-native development workflows. You help developers with proposals, architecture decisions, planning, and execution.`;
|
|
895
|
+
switch (context) {
|
|
896
|
+
case "proposals":
|
|
897
|
+
case "proposals-edit":
|
|
898
|
+
case "proposals-view":
|
|
899
|
+
let proposalPrompt = `${basePrompt} You are currently in the proposals section. Help users:
|
|
900
|
+
- Refine and improve their change proposals
|
|
901
|
+
- Suggest implementation approaches
|
|
902
|
+
- Identify potential challenges or risks
|
|
903
|
+
- Break down complex changes into manageable steps
|
|
904
|
+
- Provide feedback on proposal clarity and completeness`;
|
|
905
|
+
if (contextContent) {
|
|
906
|
+
proposalPrompt += `\n\nYou have access to the following proposal data:\n\n${contextContent}`;
|
|
907
|
+
}
|
|
908
|
+
return proposalPrompt;
|
|
909
|
+
case "context":
|
|
910
|
+
return `${basePrompt} You are currently in the context section (architecture, decisions, knowledge). Help users:
|
|
911
|
+
- Analyze architectural decisions and patterns
|
|
912
|
+
- Suggest improvements to documentation
|
|
913
|
+
- Identify gaps in knowledge or decisions
|
|
914
|
+
- Recommend best practices
|
|
915
|
+
- Help maintain consistency across the project`;
|
|
916
|
+
case "settings":
|
|
917
|
+
return `${basePrompt} You are currently in the settings section. Help users:
|
|
918
|
+
- Configure their GAIT environment
|
|
919
|
+
- Set up integrations and workflows
|
|
920
|
+
- Optimize their development process
|
|
921
|
+
- Understand configuration options
|
|
922
|
+
- Troubleshoot setup issues`;
|
|
923
|
+
default:
|
|
924
|
+
return `${basePrompt} Provide helpful, contextual assistance based on the user's current needs in their GAIT workflow.`;
|
|
925
|
+
}
|
|
926
|
+
}
|