@lovelybunch/api 1.0.75-alpha.4 → 1.0.75-alpha.6
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/index.js +0 -2
- package/dist/routes/api/v1/ai/route.js +108 -243
- package/dist/routes/api/v1/skills/[id]/index.d.ts +1 -0
- package/dist/routes/api/v1/skills/[id]/index.js +1 -0
- package/dist/routes/api/v1/skills/[id]/route.d.ts +3 -0
- package/dist/routes/api/v1/skills/[id]/route.js +199 -0
- package/dist/routes/api/v1/skills/index.d.ts +1 -0
- package/dist/routes/api/v1/skills/index.js +1 -0
- package/dist/routes/api/v1/skills/route.d.ts +3 -0
- package/dist/routes/api/v1/skills/route.js +329 -0
- package/dist/server-with-static.js +4 -4
- package/dist/server.js +4 -4
- package/package.json +8 -4
- package/static/assets/{ActivityPage-BI-4IFaP.js → ActivityPage-QmXUVEPm.js} +1 -1
- package/static/assets/{ApiKeysSettingsPage-CTPK48rU.js → ApiKeysSettingsPage-qie8F6gb.js} +2 -2
- package/static/assets/{ArchitectureEditPage-Cn4zVbPK.js → ArchitectureEditPage-CPQnvHk4.js} +1 -1
- package/static/assets/{ArchitecturePage-CCocwST0.js → ArchitecturePage-CJNygJU2.js} +1 -1
- package/static/assets/{AuthSettingsPage-C-nDwiO4.js → AuthSettingsPage-B204F8bT.js} +2 -2
- package/static/assets/{CallbackPage-ZcrfbMj2.js → CallbackPage-MGCRI7r_.js} +1 -1
- package/static/assets/CodePage-BxEsUJP-.js +2 -0
- package/static/assets/{CollapsibleSection-DreaKHR1.js → CollapsibleSection-CtALgJpu.js} +1 -1
- package/static/assets/{DashboardPage-Da0zfmye.js → DashboardPage-DbvVqN24.js} +2 -2
- package/static/assets/{GitPage-DE0oU3cj.js → GitPage-D2bnwXyJ.js} +1 -1
- package/static/assets/{GitSettingsPage-DndQUnzg.js → GitSettingsPage-ZDdKMZl1.js} +2 -2
- package/static/assets/{IdentityPage-DD-9upeG.js → IdentityPage-Bu-duoN6.js} +3 -3
- package/static/assets/{ImplementationStepsEditor-C3vh3Hc0.js → ImplementationStepsEditor-PDSNPMod.js} +1 -1
- package/static/assets/IntegrationsSettingsPage-DsM2vBgQ.js +1 -0
- package/static/assets/JobDetailPage-BBCASAAb.js +1 -0
- package/static/assets/KnowledgeDetailPage-D1EptY9f.js +1 -0
- package/static/assets/KnowledgeEditPage-Dd4B0Zol.js +1 -0
- package/static/assets/{KnowledgePage-cOsVzN4A.js → KnowledgePage-ibFc3y1K.js} +2 -2
- package/static/assets/{LoginPage-CvVZ8le4.js → LoginPage-v8StIMKr.js} +1 -1
- package/static/assets/{McpSettingsPage-_q1tdgv0.js → McpSettingsPage-m5Wzim08.js} +1 -1
- package/static/assets/NewKnowledgePage-rmXqT_ay.js +9 -0
- package/static/assets/{NewProposalPage-h91-EksD.js → NewProposalPage-CmMTQLo5.js} +2 -2
- package/static/assets/NewSkillPage-D3Fffhkj.js +1 -0
- package/static/assets/{ProjectEditPage-CcwGPvlc.js → ProjectEditPage-C37nAWFP.js} +1 -1
- package/static/assets/ProjectPage--_Q_QLC1.js +1 -0
- package/static/assets/{PromptsSettingsPage-BTOf3Xzc.js → PromptsSettingsPage-CGXj9CAs.js} +1 -1
- package/static/assets/ProposalDetailPage-BUHhxZ3l.js +1 -0
- package/static/assets/ProposalEditPage-C0eEdxJc.js +1 -0
- package/static/assets/{ProposalsPage-ekrWnqoo.js → ProposalsPage-DyQC5FFd.js} +1 -1
- package/static/assets/{ResourcesPage-90Wdfshd.js → ResourcesPage-IrMHiK53.js} +4 -4
- package/static/assets/{RoleEditPage-DY75XHIO.js → RoleEditPage-Bw35Wz-d.js} +1 -1
- package/static/assets/{RolePage-CXas69QY.js → RolePage-BOjMgEYI.js} +1 -1
- package/static/assets/{RulesSettingsPage-p6AjPCI0.js → RulesSettingsPage-B0eW6OmX.js} +2 -2
- package/static/assets/SchedulePage-CrfWjM5k.js +4 -0
- package/static/assets/SkillDetailPage-CKDNJYXm.js +1 -0
- package/static/assets/SkillEditPage-DnBv-wu9.js +1 -0
- package/static/assets/SkillsPage-C5VD2DNt.js +8 -0
- package/static/assets/SkillsSettingsPage-D3M6mo3a.js +1 -0
- package/static/assets/SourceInput-BN8ZVkqs.js +1 -0
- package/static/assets/{TagInput-Dfue2spU.js → TagInput-CICgcKUW.js} +1 -1
- package/static/assets/TerminalPage-PJ6jkMN5.js +1 -0
- package/static/assets/TerminalSessionPage-DuJ2n3mT.js +13 -0
- package/static/assets/{UserPreferencesPage-C9yXdIdZ.js → UserPreferencesPage-CyNuAJru.js} +1 -1
- package/static/assets/{UserSettingsPage-ba7UUKyG.js → UserSettingsPage-Bq2TlR-8.js} +1 -1
- package/static/assets/{UtilitiesPage-5Zot5Svs.js → UtilitiesPage-mZb2CtxF.js} +1 -1
- package/static/assets/{alert-BhJUdRgG.js → alert-CrqaDLRU.js} +1 -1
- package/static/assets/{arrow-down-BYk0k85B.js → arrow-down-CfqDgiIx.js} +1 -1
- package/static/assets/{arrow-left-TQT21N4S.js → arrow-left-CIkDScAO.js} +1 -1
- package/static/assets/{arrow-up-BkSi1XTr.js → arrow-up-DoOW0Uek.js} +1 -1
- package/static/assets/{badge-DS8SPg0q.js → badge-ikmjXCY_.js} +1 -1
- package/static/assets/{browser-modal-CgvW-bbQ.js → browser-modal-BJ16icY9.js} +2 -2
- package/static/assets/{calendar-AzmVUvxT.js → calendar-B3EFURS-.js} +1 -1
- package/static/assets/{card-C4vq6AMz.js → card-C0a11V0T.js} +1 -1
- package/static/assets/{chevron-left-CeRwwCqx.js → chevron-left-Cbq2vuK_.js} +1 -1
- package/static/assets/{chevrons-up-BdEukQhU.js → chevrons-up-C3zGaueJ.js} +1 -1
- package/static/assets/{circle-alert-C1JRie1k.js → circle-alert-BZyc7kNR.js} +1 -1
- package/static/assets/{circle-check-DxiLNIIS.js → circle-check-CC67C1b-.js} +1 -1
- package/static/assets/{circle-check-big-CQBGgL4s.js → circle-check-big-tNM4uoO8.js} +1 -1
- package/static/assets/{circle-play-qq_sLPT0.js → circle-play-DgDKRctX.js} +1 -1
- package/static/assets/{circle-x-kpttvnBF.js → circle-x-CbIXbCmI.js} +1 -1
- package/static/assets/{clipboard-CBBHo2mi.js → clipboard-Bhg2iYmV.js} +1 -1
- package/static/assets/{clock-Ck6whvmg.js → clock-DUwjJehj.js} +1 -1
- package/static/assets/{download-Cx4OcU33.js → download-CAr6POG8.js} +1 -1
- package/static/assets/{droid-BWtMgfPD.js → droid-DqWsM2dp.js} +5 -5
- package/static/assets/external-link-C552Tasx.js +6 -0
- package/static/assets/{eye-2CnfcAd3.js → eye-F-BxEo2B.js} +1 -1
- package/static/assets/{folder-git-2-CvwpDtwI.js → folder-git-2-BCO-gmji.js} +1 -1
- package/static/assets/index-Cb4mP03_.js +462 -0
- package/static/assets/index-DVTgTsDa.css +2 -0
- package/static/assets/{info-BO6_vv66.js → info-DZ6rfcGz.js} +1 -1
- package/static/assets/{label-B-1O5hdX.js → label-VXwOLYGm.js} +1 -1
- package/static/assets/{markdown-editor-H-rT6Hat.js → markdown-editor-DKBU3Sgf.js} +1 -1
- package/static/assets/{pause-CW39bpDf.js → pause-CJhh_zgy.js} +1 -1
- package/static/assets/{play-Bqd-yHZN.js → play-DqF_TESp.js} +1 -1
- package/static/assets/{plus-BWhZkuI2.js → plus-B-nP-2Bc.js} +1 -1
- package/static/assets/{radio-group-CHbb2Tdt.js → radio-group-CkXoX9re.js} +1 -1
- package/static/assets/{refresh-cw-CaOTs34G.js → refresh-cw-DNCHo2vd.js} +1 -1
- package/static/assets/{search-D2duvyuf.js → search-pT3zGN1p.js} +1 -1
- package/static/assets/{switch-BRsOxt7T.js → switch-DKgMJKx9.js} +1 -1
- package/static/assets/{tabs--rTULKlo.js → tabs-LRqYZP6q.js} +1 -1
- package/static/assets/{tag-BQ5pJD32.js → tag-CNJNVCjj.js} +1 -1
- package/static/assets/{terminal-preview-ewZC6O1I.js → terminal-preview-C_hF2vLs.js} +1 -1
- package/static/assets/{use-terminal-Cya6EYny.js → use-terminal-B3kcgG1Q.js} +1 -1
- package/static/assets/{zap-DQNov92q.js → zap-DeMKwLsi.js} +1 -1
- package/static/index.html +2 -2
- package/static/assets/AgentDetailPage-eVEOya9x.js +0 -1
- package/static/assets/AgentEditPage-Ds5XXaak.js +0 -1
- package/static/assets/AgentsPage-CE59G63x.js +0 -3
- package/static/assets/AgentsSettingsPage-uVKYxuov.js +0 -6
- package/static/assets/CodePage-DuHcwCCu.js +0 -2
- package/static/assets/IntegrationsSettingsPage-B4J4wDXt.js +0 -1
- package/static/assets/JobDetailPage-tHRLIpQI.js +0 -1
- package/static/assets/KnowledgeDetailPage-Dh315TpW.js +0 -1
- package/static/assets/KnowledgeEditPage-B5bNMMvh.js +0 -1
- package/static/assets/NewAgentPage-DIzdUSPb.js +0 -1
- package/static/assets/NewKnowledgePage-weR174CB.js +0 -9
- package/static/assets/ProjectPage-D9APD9HG.js +0 -1
- package/static/assets/ProposalDetailPage-DbUTHokj.js +0 -1
- package/static/assets/ProposalEditPage-97pQXtDD.js +0 -1
- package/static/assets/SchedulePage-C-Yevtfw.js +0 -4
- package/static/assets/SourceInput-CLzuDG61.js +0 -1
- package/static/assets/TerminalPage-BbA46X-G.js +0 -1
- package/static/assets/TerminalSessionPage-FueIeaIL.js +0 -13
- package/static/assets/index-C8_b70Ej.css +0 -2
- package/static/assets/index-Df8kpDhM.js +0 -468
|
@@ -4,6 +4,8 @@ import { existsSync, readFileSync, promises as fs, createReadStream } from 'fs';
|
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import readline from 'readline';
|
|
6
6
|
import { ZodError } from 'zod';
|
|
7
|
+
import { streamText, tool, jsonSchema, stepCountIs } from 'ai';
|
|
8
|
+
import { createAnthropic } from '@ai-sdk/anthropic';
|
|
7
9
|
import { getLogsDir, listProposals, getProposal, createProposal, updateProposal, deleteProposal, } from '@lovelybunch/core';
|
|
8
10
|
import { proposalsFullTool, knowledgeTool, normalizeKnowledgeMetadata, eventsTool, projectContextTool, architectureContextTool, roleContextTool } from '@lovelybunch/mcp';
|
|
9
11
|
import matter from 'gray-matter';
|
|
@@ -37,279 +39,142 @@ function getGlobalApiKey(provider) {
|
|
|
37
39
|
}
|
|
38
40
|
export async function POST(c) {
|
|
39
41
|
try {
|
|
40
|
-
const { message: userMessage, history,
|
|
42
|
+
const { message: userMessage, history, context, contextContent, skillPersona, agentPersona, systemPrompt: systemOverride, attachedContextFiles } = await c.req.json();
|
|
41
43
|
if (!userMessage) {
|
|
42
44
|
return c.json({ error: "Message is required" }, 400);
|
|
43
45
|
}
|
|
44
46
|
// Prefer global config key (set via CLI/UI), fallback to env var
|
|
45
|
-
|
|
46
|
-
if (!
|
|
47
|
+
const apiKey = getGlobalApiKey('anthropic') || process.env.ANTHROPIC_API_KEY;
|
|
48
|
+
if (!apiKey) {
|
|
47
49
|
return c.json({
|
|
48
|
-
error: "
|
|
49
|
-
hint: "Set via 'coconut config set-key -p
|
|
50
|
+
error: "Anthropic API key not configured",
|
|
51
|
+
hint: "Set via 'coconut config set-key -p anthropic -k <KEY>' (or 'nut config set-key -p anthropic -k <KEY>'), or set ANTHROPIC_API_KEY env var"
|
|
50
52
|
}, 500);
|
|
51
53
|
}
|
|
54
|
+
// Initialize Anthropic model via AI SDK
|
|
55
|
+
const anthropicClient = createAnthropic({ apiKey });
|
|
56
|
+
const model = anthropicClient('claude-sonnet-4-5-20250929');
|
|
52
57
|
const baseSystem = systemOverride || getSystemPrompt();
|
|
53
|
-
const
|
|
54
|
-
|
|
58
|
+
const persona = skillPersona || agentPersona; // Support both new and legacy field names
|
|
59
|
+
const systemPrompt = persona
|
|
60
|
+
? `${baseSystem}\n\nThe following persona is authoritative and overrides general guidance above. You must strictly follow it.\n\n${persona}`
|
|
55
61
|
: baseSystem;
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
{
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
{
|
|
72
|
-
type: "function",
|
|
73
|
-
function: projectContextTool
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
type: "function",
|
|
77
|
-
function: architectureContextTool
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
type: "function",
|
|
81
|
-
function: roleContextTool
|
|
62
|
+
// Build context messages from attached files
|
|
63
|
+
const contextMessages = [];
|
|
64
|
+
if (Array.isArray(attachedContextFiles)) {
|
|
65
|
+
for (const file of attachedContextFiles) {
|
|
66
|
+
if (file && typeof file.content === 'string' && file.content.trim().length > 0) {
|
|
67
|
+
contextMessages.push({
|
|
68
|
+
role: 'user',
|
|
69
|
+
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 ---`
|
|
70
|
+
});
|
|
71
|
+
// Add a brief assistant acknowledgment so the conversation alternates correctly
|
|
72
|
+
contextMessages.push({
|
|
73
|
+
role: 'assistant',
|
|
74
|
+
content: `I've noted the attached context document "${file.name || 'reference'}". I'll reference it as needed.`
|
|
75
|
+
});
|
|
76
|
+
}
|
|
82
77
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
? attachedContextFiles
|
|
86
|
-
.filter((file) => file && typeof file.content === 'string' && file.content.trim().length > 0)
|
|
87
|
-
.map((file) => ({
|
|
88
|
-
role: 'system',
|
|
89
|
-
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 ---`
|
|
90
|
-
}))
|
|
91
|
-
: [];
|
|
92
|
-
// Compose the message list: system + history (preferred) or single message
|
|
78
|
+
}
|
|
79
|
+
// Compose the message list: history (preferred) or single message
|
|
93
80
|
let messagesPayload;
|
|
94
81
|
if (Array.isArray(history) && history.length > 0) {
|
|
95
|
-
//
|
|
96
|
-
const sanitized = history.filter(m => m && (m.role === 'user' || m.role === 'assistant') && typeof m.content === 'string');
|
|
82
|
+
// Sanitize roles to user/assistant only, preserve order
|
|
83
|
+
const sanitized = history.filter((m) => m && (m.role === 'user' || m.role === 'assistant') && typeof m.content === 'string');
|
|
97
84
|
messagesPayload = [
|
|
98
|
-
{ role: 'system', content: systemPrompt },
|
|
99
85
|
...contextMessages,
|
|
100
86
|
...sanitized,
|
|
101
87
|
];
|
|
102
88
|
}
|
|
103
89
|
else {
|
|
104
90
|
messagesPayload = [
|
|
105
|
-
{ role: 'system', content: systemPrompt },
|
|
106
91
|
...contextMessages,
|
|
107
92
|
{ role: 'user', content: userMessage },
|
|
108
93
|
];
|
|
109
94
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
95
|
+
// Define tools with execute functions using AI SDK tool()
|
|
96
|
+
// Using tool<any, string>() to properly type jsonSchema-based tools
|
|
97
|
+
const storage = new FileStorageAdapter();
|
|
98
|
+
const aiTools = {
|
|
99
|
+
change_proposals: tool({
|
|
100
|
+
description: proposalsFullTool.description,
|
|
101
|
+
inputSchema: jsonSchema(proposalsFullTool.parameters),
|
|
102
|
+
execute: async (args) => {
|
|
103
|
+
const result = await executeProposalsToolDirect(args, storage);
|
|
104
|
+
return JSON.stringify(result);
|
|
105
|
+
},
|
|
106
|
+
}),
|
|
107
|
+
knowledge_documents: tool({
|
|
108
|
+
description: knowledgeTool.description,
|
|
109
|
+
inputSchema: jsonSchema(knowledgeTool.parameters),
|
|
110
|
+
execute: async (args) => {
|
|
111
|
+
const result = await executeKnowledgeToolDirect(args);
|
|
112
|
+
return JSON.stringify(result);
|
|
113
|
+
},
|
|
114
|
+
}),
|
|
115
|
+
activity_events: tool({
|
|
116
|
+
description: eventsTool.description,
|
|
117
|
+
inputSchema: jsonSchema(eventsTool.parameters),
|
|
118
|
+
execute: async (args) => {
|
|
119
|
+
const result = await executeEventsToolDirect(args);
|
|
120
|
+
return JSON.stringify(result);
|
|
121
|
+
},
|
|
122
|
+
}),
|
|
123
|
+
project_context: tool({
|
|
124
|
+
description: projectContextTool.description,
|
|
125
|
+
inputSchema: jsonSchema(projectContextTool.parameters),
|
|
126
|
+
execute: async (args) => {
|
|
127
|
+
const result = await executeProjectContextToolDirect(args);
|
|
128
|
+
return JSON.stringify(result);
|
|
129
|
+
},
|
|
130
|
+
}),
|
|
131
|
+
architecture_context: tool({
|
|
132
|
+
description: architectureContextTool.description,
|
|
133
|
+
inputSchema: jsonSchema(architectureContextTool.parameters),
|
|
134
|
+
execute: async (args) => {
|
|
135
|
+
const result = await executeArchitectureContextToolDirect(args);
|
|
136
|
+
return JSON.stringify(result);
|
|
137
|
+
},
|
|
138
|
+
}),
|
|
139
|
+
role_context: tool({
|
|
140
|
+
description: roleContextTool.description,
|
|
141
|
+
inputSchema: jsonSchema(roleContextTool.parameters),
|
|
142
|
+
execute: async (args) => {
|
|
143
|
+
const result = await executeRoleContextToolDirect(args);
|
|
144
|
+
return JSON.stringify(result);
|
|
145
|
+
},
|
|
146
|
+
}),
|
|
116
147
|
};
|
|
117
|
-
// Add tools if enabled
|
|
118
|
-
if (tools) {
|
|
119
|
-
requestBody.tools = tools;
|
|
120
|
-
requestBody.tool_choice = "auto";
|
|
121
|
-
}
|
|
122
148
|
// Debug logging
|
|
123
149
|
console.log('AI Request Debug:', {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
model: requestBody.model
|
|
150
|
+
toolCount: Object.keys(aiTools).length,
|
|
151
|
+
messageCount: messagesPayload.length,
|
|
152
|
+
model: 'claude-sonnet-4-5-20250929'
|
|
128
153
|
});
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
154
|
+
// Use AI SDK streamText - handles the entire LLM + tool loop automatically
|
|
155
|
+
const result = streamText({
|
|
156
|
+
model,
|
|
157
|
+
system: systemPrompt,
|
|
158
|
+
messages: messagesPayload,
|
|
159
|
+
tools: aiTools,
|
|
160
|
+
stopWhen: stepCountIs(5), // Max 5 tool-use rounds
|
|
161
|
+
temperature: 0.7,
|
|
162
|
+
maxOutputTokens: 8000,
|
|
138
163
|
});
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
statusText: response.statusText,
|
|
144
|
-
error: errorText,
|
|
145
|
-
model: requestBody.model
|
|
146
|
-
});
|
|
147
|
-
// Try to parse error for more detail
|
|
148
|
-
let errorDetail = "Failed to get AI response";
|
|
149
|
-
try {
|
|
150
|
-
const errorJson = JSON.parse(errorText);
|
|
151
|
-
errorDetail = errorJson.error?.message || errorJson.error || errorDetail;
|
|
152
|
-
}
|
|
153
|
-
catch {
|
|
154
|
-
// Use raw text if not JSON
|
|
155
|
-
if (errorText.length < 200) {
|
|
156
|
-
errorDetail = errorText || errorDetail;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
return c.json({ error: errorDetail, status: response.status }, response.status);
|
|
160
|
-
}
|
|
161
|
-
// Handle streaming response
|
|
162
|
-
if (stream === true && response.body) {
|
|
163
|
-
return c.newResponse(response.body, {
|
|
164
|
-
status: 200,
|
|
165
|
-
headers: {
|
|
166
|
-
'Content-Type': 'text/event-stream',
|
|
167
|
-
'Cache-Control': 'no-cache',
|
|
168
|
-
'Connection': 'keep-alive',
|
|
169
|
-
},
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
const data = await response.json();
|
|
173
|
-
const aiMessage = data.choices?.[0]?.message;
|
|
174
|
-
if (!aiMessage) {
|
|
175
|
-
console.error("No AI message in response:", {
|
|
176
|
-
model: requestBody.model,
|
|
177
|
-
choices: data.choices,
|
|
178
|
-
error: data.error
|
|
179
|
-
});
|
|
180
|
-
const errorDetail = data.error?.message || data.error || "No response from AI model";
|
|
181
|
-
return c.json({ error: errorDetail }, 500);
|
|
182
|
-
}
|
|
183
|
-
// Handle tool calls
|
|
184
|
-
if (aiMessage.tool_calls && aiMessage.tool_calls.length > 0) {
|
|
185
|
-
const toolResults = await executeToolCalls(aiMessage.tool_calls);
|
|
186
|
-
// Add tool results to the conversation and get final response
|
|
187
|
-
const messagesWithTools = [
|
|
188
|
-
...messagesPayload,
|
|
189
|
-
aiMessage,
|
|
190
|
-
...toolResults.map(result => ({
|
|
191
|
-
role: 'tool',
|
|
192
|
-
content: result.content,
|
|
193
|
-
tool_call_id: result.tool_call_id
|
|
194
|
-
}))
|
|
195
|
-
];
|
|
196
|
-
// Get final response from AI with tool results
|
|
197
|
-
// Use lower max_tokens for tool result synthesis (typically just needs to summarize)
|
|
198
|
-
const finalResponse = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
|
199
|
-
method: "POST",
|
|
200
|
-
headers: {
|
|
201
|
-
"Authorization": `Bearer ${openRouterKey}`,
|
|
202
|
-
"Content-Type": "application/json",
|
|
203
|
-
"HTTP-Referer": process.env.NEXT_PUBLIC_SITE_URL || "http://localhost:3001",
|
|
204
|
-
"X-Title": "Coconut AI Assistant",
|
|
205
|
-
},
|
|
206
|
-
body: JSON.stringify({
|
|
207
|
-
model: model || "openai/gpt-4o",
|
|
208
|
-
messages: messagesWithTools,
|
|
209
|
-
temperature: 0.5, // Lower temperature for more consistent tool result summaries
|
|
210
|
-
max_tokens: 2000, // Reduced from 8000 - tool summaries don't need to be as long
|
|
211
|
-
}),
|
|
212
|
-
});
|
|
213
|
-
if (!finalResponse.ok) {
|
|
214
|
-
const error = await finalResponse.text();
|
|
215
|
-
console.error("OpenRouter API error for final response:", error);
|
|
216
|
-
return c.json({ error: "Failed to get final AI response" }, 500);
|
|
217
|
-
}
|
|
218
|
-
const finalData = await finalResponse.json();
|
|
219
|
-
const finalMessage = finalData.choices?.[0]?.message;
|
|
220
|
-
return c.json({
|
|
221
|
-
response: finalMessage?.content || "Tool execution completed",
|
|
222
|
-
toolCalls: aiMessage.tool_calls,
|
|
223
|
-
toolResults: toolResults
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
return c.json({ response: aiMessage.content });
|
|
164
|
+
// Return a plain text stream response
|
|
165
|
+
// toTextStreamResponse() streams text-delta events as UTF-8 chunks,
|
|
166
|
+
// ignoring tool calls, tool results, and other non-text events
|
|
167
|
+
return result.toTextStreamResponse();
|
|
227
168
|
}
|
|
228
169
|
catch (error) {
|
|
229
170
|
console.error("AI API error:", error);
|
|
230
171
|
return c.json({ error: "Internal server error" }, 500);
|
|
231
172
|
}
|
|
232
173
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
try {
|
|
238
|
-
// Handle both function calling formats
|
|
239
|
-
const functionName = toolCall.function?.name || toolCall.name;
|
|
240
|
-
let functionArgs;
|
|
241
|
-
if (toolCall.function?.arguments) {
|
|
242
|
-
try {
|
|
243
|
-
// Try to sanitize common JSON issues before parsing
|
|
244
|
-
let argsStr = toolCall.function.arguments;
|
|
245
|
-
// Remove trailing commas before ] or }
|
|
246
|
-
argsStr = argsStr.replace(/,(\s*[}\]])/g, '$1');
|
|
247
|
-
// Fix common LLM JSON issues:
|
|
248
|
-
// Remove control characters (except \n, \r, \t which are valid in JSON strings)
|
|
249
|
-
argsStr = argsStr.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '');
|
|
250
|
-
functionArgs = JSON.parse(argsStr);
|
|
251
|
-
}
|
|
252
|
-
catch (parseError) {
|
|
253
|
-
// LLM may generate invalid JSON with unescaped characters in long content
|
|
254
|
-
const errorMsg = parseError instanceof Error ? parseError.message : 'Unknown parse error';
|
|
255
|
-
const hint = errorMsg.includes('position')
|
|
256
|
-
? 'The content may contain unescaped quotes or special characters. Try updating with shorter/simpler content, or update one field at a time.'
|
|
257
|
-
: 'Please retry with simpler content.';
|
|
258
|
-
return {
|
|
259
|
-
tool_call_id: toolCall.id,
|
|
260
|
-
content: JSON.stringify({
|
|
261
|
-
success: false,
|
|
262
|
-
error: `Invalid tool arguments: ${errorMsg}. ${hint}`
|
|
263
|
-
})
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
else {
|
|
268
|
-
functionArgs = toolCall.arguments;
|
|
269
|
-
}
|
|
270
|
-
let result = { success: false, error: 'Unknown tool' };
|
|
271
|
-
// Execute tools directly instead of making HTTP requests
|
|
272
|
-
if (functionName === 'change_proposals') {
|
|
273
|
-
result = await executeProposalsToolDirect(functionArgs, storage);
|
|
274
|
-
}
|
|
275
|
-
else if (functionName === 'knowledge_documents') {
|
|
276
|
-
result = await executeKnowledgeToolDirect(functionArgs);
|
|
277
|
-
}
|
|
278
|
-
else if (functionName === 'activity_events') {
|
|
279
|
-
result = await executeEventsToolDirect(functionArgs);
|
|
280
|
-
}
|
|
281
|
-
else if (functionName === 'project_context') {
|
|
282
|
-
result = await executeProjectContextToolDirect(functionArgs);
|
|
283
|
-
}
|
|
284
|
-
else if (functionName === 'architecture_context') {
|
|
285
|
-
result = await executeArchitectureContextToolDirect(functionArgs);
|
|
286
|
-
}
|
|
287
|
-
else if (functionName === 'role_context') {
|
|
288
|
-
result = await executeRoleContextToolDirect(functionArgs);
|
|
289
|
-
}
|
|
290
|
-
return {
|
|
291
|
-
tool_call_id: toolCall.id,
|
|
292
|
-
content: JSON.stringify({
|
|
293
|
-
success: result.success,
|
|
294
|
-
data: result.data,
|
|
295
|
-
message: result.message,
|
|
296
|
-
error: result.error
|
|
297
|
-
})
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
catch (error) {
|
|
301
|
-
return {
|
|
302
|
-
tool_call_id: toolCall.id,
|
|
303
|
-
content: JSON.stringify({
|
|
304
|
-
success: false,
|
|
305
|
-
error: error instanceof Error ? error.message : 'Unknown error'
|
|
306
|
-
})
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
});
|
|
310
|
-
return Promise.all(resultPromises);
|
|
311
|
-
}
|
|
312
|
-
// Proposals tool - full CRUD operations using @lovelybunch/core
|
|
174
|
+
// ─── Tool Execution Functions ────────────────────────────────────────────────
|
|
175
|
+
// These are unchanged from the previous implementation. They handle the actual
|
|
176
|
+
// business logic for each tool, called automatically by the AI SDK when the
|
|
177
|
+
// LLM makes tool calls.
|
|
313
178
|
async function executeProposalsToolDirect(args, _storage) {
|
|
314
179
|
const { operation, id, filters, proposal, updates } = args;
|
|
315
180
|
try {
|
|
@@ -381,7 +246,6 @@ async function executeProposalsToolDirect(args, _storage) {
|
|
|
381
246
|
}
|
|
382
247
|
}
|
|
383
248
|
catch (error) {
|
|
384
|
-
// Handle Zod validation errors specially
|
|
385
249
|
if (error instanceof ZodError) {
|
|
386
250
|
return {
|
|
387
251
|
success: false,
|
|
@@ -953,6 +817,7 @@ async function executeRoleContextToolDirect(args) {
|
|
|
953
817
|
return { success: false, error: error.message || 'Role context tool execution failed' };
|
|
954
818
|
}
|
|
955
819
|
}
|
|
820
|
+
// ─── Helper Functions ────────────────────────────────────────────────────────
|
|
956
821
|
function getContextBasePath() {
|
|
957
822
|
let basePath;
|
|
958
823
|
if (process.env.NODE_ENV === 'development' && process.env.GAIT_DEV_ROOT) {
|
|
@@ -1017,14 +882,14 @@ async function readKnowledgeDocument(basePath, filename) {
|
|
|
1017
882
|
fs.stat(filePath)
|
|
1018
883
|
]);
|
|
1019
884
|
const { data, content } = matter(raw);
|
|
1020
|
-
const
|
|
885
|
+
const normalizedMeta = normalizeKnowledgeMetadata(data);
|
|
1021
886
|
// Use file mtime for updated timestamp instead of frontmatter
|
|
1022
|
-
|
|
887
|
+
normalizedMeta.updated = stats.mtime.toISOString();
|
|
1023
888
|
const title = extractKnowledgeTitle(content, actualFilename);
|
|
1024
889
|
return {
|
|
1025
890
|
filename: actualFilename,
|
|
1026
891
|
title,
|
|
1027
|
-
metadata,
|
|
892
|
+
metadata: normalizedMeta,
|
|
1028
893
|
content
|
|
1029
894
|
};
|
|
1030
895
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './route.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './route.js';
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import matter from 'gray-matter';
|
|
5
|
+
const app = new Hono();
|
|
6
|
+
function generateSkillId(name) {
|
|
7
|
+
return name
|
|
8
|
+
.toLowerCase()
|
|
9
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
10
|
+
.replace(/\s+/g, '-')
|
|
11
|
+
.replace(/--+/g, '-')
|
|
12
|
+
.replace(/^-|-$/g, '');
|
|
13
|
+
}
|
|
14
|
+
function getSkillsPath() {
|
|
15
|
+
let basePath;
|
|
16
|
+
if (process.env.NODE_ENV === 'development' && process.env.GAIT_DEV_ROOT) {
|
|
17
|
+
basePath = process.env.GAIT_DEV_ROOT;
|
|
18
|
+
}
|
|
19
|
+
else if (process.env.GAIT_DATA_PATH) {
|
|
20
|
+
basePath = path.resolve(process.env.GAIT_DATA_PATH, '.nut');
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
basePath = path.resolve(process.cwd(), '.nut');
|
|
24
|
+
}
|
|
25
|
+
return path.join(basePath, 'skills');
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* GET /api/v1/skills/:id
|
|
29
|
+
* Load a specific skill document
|
|
30
|
+
*/
|
|
31
|
+
app.get('/', async (c) => {
|
|
32
|
+
try {
|
|
33
|
+
const id = c.req.param('id');
|
|
34
|
+
const skillsPath = getSkillsPath();
|
|
35
|
+
const skillFile = path.join(skillsPath, id, 'SKILL.md');
|
|
36
|
+
const fileContent = await fs.readFile(skillFile, 'utf-8');
|
|
37
|
+
const { data, content } = matter(fileContent);
|
|
38
|
+
const nestedMeta = typeof data.metadata === 'object' ? data.metadata : undefined;
|
|
39
|
+
const document = {
|
|
40
|
+
id,
|
|
41
|
+
name: data.name || id,
|
|
42
|
+
description: data.description || '',
|
|
43
|
+
license: data.license,
|
|
44
|
+
compatibility: data.compatibility,
|
|
45
|
+
allowedTools: data['allowed-tools'] || data.allowedTools,
|
|
46
|
+
metadata: {
|
|
47
|
+
name: data.name || id,
|
|
48
|
+
description: data.description || '',
|
|
49
|
+
license: data.license,
|
|
50
|
+
compatibility: data.compatibility,
|
|
51
|
+
allowedTools: data['allowed-tools'] || data.allowedTools,
|
|
52
|
+
...(nestedMeta || {}),
|
|
53
|
+
metadata: nestedMeta,
|
|
54
|
+
},
|
|
55
|
+
content,
|
|
56
|
+
};
|
|
57
|
+
return c.json({
|
|
58
|
+
success: true,
|
|
59
|
+
document
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (error.code === 'ENOENT') {
|
|
64
|
+
return c.json({ success: false, error: 'Skill document not found' }, 404);
|
|
65
|
+
}
|
|
66
|
+
console.error('Error loading skill document:', error);
|
|
67
|
+
return c.json({ success: false, error: 'Failed to load skill document' }, 500);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
/**
|
|
71
|
+
* PUT /api/v1/skills/:id
|
|
72
|
+
* Update a specific skill document
|
|
73
|
+
*/
|
|
74
|
+
app.put('/', async (c) => {
|
|
75
|
+
try {
|
|
76
|
+
const id = c.req.param('id');
|
|
77
|
+
const body = await c.req.json();
|
|
78
|
+
const skillsPath = getSkillsPath();
|
|
79
|
+
const skillDir = path.join(skillsPath, id);
|
|
80
|
+
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
81
|
+
// Check if skill exists
|
|
82
|
+
try {
|
|
83
|
+
await fs.access(skillFile);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return c.json({ success: false, error: 'Skill document not found' }, 404);
|
|
87
|
+
}
|
|
88
|
+
// Read current content
|
|
89
|
+
const currentContent = await fs.readFile(skillFile, 'utf-8');
|
|
90
|
+
const { data: currentData, content: currentMarkdown } = matter(currentContent);
|
|
91
|
+
// Merge updates
|
|
92
|
+
const updatedContent = body.content !== undefined ? body.content : currentMarkdown;
|
|
93
|
+
const updatedDescription = body.description !== undefined ? body.description : (currentData.description || '');
|
|
94
|
+
const updatedLicense = body.license !== undefined ? body.license : currentData.license;
|
|
95
|
+
const updatedCompatibility = body.compatibility !== undefined ? body.compatibility : currentData.compatibility;
|
|
96
|
+
const updatedAllowedTools = body.allowedTools !== undefined ? body.allowedTools : (currentData['allowed-tools'] || currentData.allowedTools);
|
|
97
|
+
// Merge metadata
|
|
98
|
+
const currentMeta = typeof currentData.metadata === 'object' && currentData.metadata !== null ? currentData.metadata : {};
|
|
99
|
+
const updatedMeta = body.metadata
|
|
100
|
+
? { ...currentMeta, ...body.metadata }
|
|
101
|
+
: currentMeta;
|
|
102
|
+
// Handle name change (directory rename)
|
|
103
|
+
let newId = id;
|
|
104
|
+
let newDir = skillDir;
|
|
105
|
+
if (body.name && body.name !== currentData.name) {
|
|
106
|
+
newId = generateSkillId(body.name);
|
|
107
|
+
newDir = path.join(skillsPath, newId);
|
|
108
|
+
if (newId !== id) {
|
|
109
|
+
try {
|
|
110
|
+
await fs.access(path.join(newDir, 'SKILL.md'));
|
|
111
|
+
return c.json({ success: false, error: 'A skill with this name already exists' }, 409);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// Doesn't exist, proceed with rename
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// Build updated frontmatter
|
|
119
|
+
const frontmatter = {
|
|
120
|
+
name: newId,
|
|
121
|
+
description: updatedDescription,
|
|
122
|
+
};
|
|
123
|
+
if (updatedLicense)
|
|
124
|
+
frontmatter.license = updatedLicense;
|
|
125
|
+
if (updatedCompatibility)
|
|
126
|
+
frontmatter.compatibility = updatedCompatibility;
|
|
127
|
+
if (updatedAllowedTools)
|
|
128
|
+
frontmatter['allowed-tools'] = updatedAllowedTools;
|
|
129
|
+
// Clean metadata
|
|
130
|
+
const cleanMeta = {};
|
|
131
|
+
for (const [key, value] of Object.entries(updatedMeta)) {
|
|
132
|
+
if (value !== undefined && value !== null) {
|
|
133
|
+
cleanMeta[key] = value;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (Object.keys(cleanMeta).length > 0) {
|
|
137
|
+
frontmatter.metadata = cleanMeta;
|
|
138
|
+
}
|
|
139
|
+
const fileContent = matter.stringify(updatedContent, frontmatter);
|
|
140
|
+
if (newId !== id) {
|
|
141
|
+
// Create new directory and write file
|
|
142
|
+
await fs.mkdir(newDir, { recursive: true });
|
|
143
|
+
await fs.writeFile(path.join(newDir, 'SKILL.md'), fileContent, 'utf-8');
|
|
144
|
+
// Remove old directory
|
|
145
|
+
await fs.rm(skillDir, { recursive: true, force: true });
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
await fs.writeFile(skillFile, fileContent, 'utf-8');
|
|
149
|
+
}
|
|
150
|
+
return c.json({
|
|
151
|
+
success: true,
|
|
152
|
+
document: {
|
|
153
|
+
id: newId,
|
|
154
|
+
name: newId,
|
|
155
|
+
description: updatedDescription,
|
|
156
|
+
metadata: {
|
|
157
|
+
name: newId,
|
|
158
|
+
description: updatedDescription,
|
|
159
|
+
...frontmatter,
|
|
160
|
+
...(typeof cleanMeta === 'object' ? cleanMeta : {}),
|
|
161
|
+
},
|
|
162
|
+
content: updatedContent
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
console.error('Error updating skill document:', error);
|
|
168
|
+
return c.json({ success: false, error: 'Failed to update skill document' }, 500);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
/**
|
|
172
|
+
* DELETE /api/v1/skills/:id
|
|
173
|
+
* Delete a specific skill document (removes entire skill directory)
|
|
174
|
+
*/
|
|
175
|
+
app.delete('/', async (c) => {
|
|
176
|
+
try {
|
|
177
|
+
const id = c.req.param('id');
|
|
178
|
+
const skillsPath = getSkillsPath();
|
|
179
|
+
const skillDir = path.join(skillsPath, id);
|
|
180
|
+
// Check if skill exists
|
|
181
|
+
try {
|
|
182
|
+
await fs.access(path.join(skillDir, 'SKILL.md'));
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return c.json({ success: false, error: 'Skill document not found' }, 404);
|
|
186
|
+
}
|
|
187
|
+
// Remove entire skill directory
|
|
188
|
+
await fs.rm(skillDir, { recursive: true, force: true });
|
|
189
|
+
return c.json({
|
|
190
|
+
success: true,
|
|
191
|
+
message: 'Skill document deleted successfully'
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
console.error('Error deleting skill document:', error);
|
|
196
|
+
return c.json({ success: false, error: 'Failed to delete skill document' }, 500);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
export default app;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './route.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './route.js';
|