@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.
Files changed (118) hide show
  1. package/dist/routes/api/v1/ai/index.js +0 -2
  2. package/dist/routes/api/v1/ai/route.js +108 -243
  3. package/dist/routes/api/v1/skills/[id]/index.d.ts +1 -0
  4. package/dist/routes/api/v1/skills/[id]/index.js +1 -0
  5. package/dist/routes/api/v1/skills/[id]/route.d.ts +3 -0
  6. package/dist/routes/api/v1/skills/[id]/route.js +199 -0
  7. package/dist/routes/api/v1/skills/index.d.ts +1 -0
  8. package/dist/routes/api/v1/skills/index.js +1 -0
  9. package/dist/routes/api/v1/skills/route.d.ts +3 -0
  10. package/dist/routes/api/v1/skills/route.js +329 -0
  11. package/dist/server-with-static.js +4 -4
  12. package/dist/server.js +4 -4
  13. package/package.json +8 -4
  14. package/static/assets/{ActivityPage-BI-4IFaP.js → ActivityPage-QmXUVEPm.js} +1 -1
  15. package/static/assets/{ApiKeysSettingsPage-CTPK48rU.js → ApiKeysSettingsPage-qie8F6gb.js} +2 -2
  16. package/static/assets/{ArchitectureEditPage-Cn4zVbPK.js → ArchitectureEditPage-CPQnvHk4.js} +1 -1
  17. package/static/assets/{ArchitecturePage-CCocwST0.js → ArchitecturePage-CJNygJU2.js} +1 -1
  18. package/static/assets/{AuthSettingsPage-C-nDwiO4.js → AuthSettingsPage-B204F8bT.js} +2 -2
  19. package/static/assets/{CallbackPage-ZcrfbMj2.js → CallbackPage-MGCRI7r_.js} +1 -1
  20. package/static/assets/CodePage-BxEsUJP-.js +2 -0
  21. package/static/assets/{CollapsibleSection-DreaKHR1.js → CollapsibleSection-CtALgJpu.js} +1 -1
  22. package/static/assets/{DashboardPage-Da0zfmye.js → DashboardPage-DbvVqN24.js} +2 -2
  23. package/static/assets/{GitPage-DE0oU3cj.js → GitPage-D2bnwXyJ.js} +1 -1
  24. package/static/assets/{GitSettingsPage-DndQUnzg.js → GitSettingsPage-ZDdKMZl1.js} +2 -2
  25. package/static/assets/{IdentityPage-DD-9upeG.js → IdentityPage-Bu-duoN6.js} +3 -3
  26. package/static/assets/{ImplementationStepsEditor-C3vh3Hc0.js → ImplementationStepsEditor-PDSNPMod.js} +1 -1
  27. package/static/assets/IntegrationsSettingsPage-DsM2vBgQ.js +1 -0
  28. package/static/assets/JobDetailPage-BBCASAAb.js +1 -0
  29. package/static/assets/KnowledgeDetailPage-D1EptY9f.js +1 -0
  30. package/static/assets/KnowledgeEditPage-Dd4B0Zol.js +1 -0
  31. package/static/assets/{KnowledgePage-cOsVzN4A.js → KnowledgePage-ibFc3y1K.js} +2 -2
  32. package/static/assets/{LoginPage-CvVZ8le4.js → LoginPage-v8StIMKr.js} +1 -1
  33. package/static/assets/{McpSettingsPage-_q1tdgv0.js → McpSettingsPage-m5Wzim08.js} +1 -1
  34. package/static/assets/NewKnowledgePage-rmXqT_ay.js +9 -0
  35. package/static/assets/{NewProposalPage-h91-EksD.js → NewProposalPage-CmMTQLo5.js} +2 -2
  36. package/static/assets/NewSkillPage-D3Fffhkj.js +1 -0
  37. package/static/assets/{ProjectEditPage-CcwGPvlc.js → ProjectEditPage-C37nAWFP.js} +1 -1
  38. package/static/assets/ProjectPage--_Q_QLC1.js +1 -0
  39. package/static/assets/{PromptsSettingsPage-BTOf3Xzc.js → PromptsSettingsPage-CGXj9CAs.js} +1 -1
  40. package/static/assets/ProposalDetailPage-BUHhxZ3l.js +1 -0
  41. package/static/assets/ProposalEditPage-C0eEdxJc.js +1 -0
  42. package/static/assets/{ProposalsPage-ekrWnqoo.js → ProposalsPage-DyQC5FFd.js} +1 -1
  43. package/static/assets/{ResourcesPage-90Wdfshd.js → ResourcesPage-IrMHiK53.js} +4 -4
  44. package/static/assets/{RoleEditPage-DY75XHIO.js → RoleEditPage-Bw35Wz-d.js} +1 -1
  45. package/static/assets/{RolePage-CXas69QY.js → RolePage-BOjMgEYI.js} +1 -1
  46. package/static/assets/{RulesSettingsPage-p6AjPCI0.js → RulesSettingsPage-B0eW6OmX.js} +2 -2
  47. package/static/assets/SchedulePage-CrfWjM5k.js +4 -0
  48. package/static/assets/SkillDetailPage-CKDNJYXm.js +1 -0
  49. package/static/assets/SkillEditPage-DnBv-wu9.js +1 -0
  50. package/static/assets/SkillsPage-C5VD2DNt.js +8 -0
  51. package/static/assets/SkillsSettingsPage-D3M6mo3a.js +1 -0
  52. package/static/assets/SourceInput-BN8ZVkqs.js +1 -0
  53. package/static/assets/{TagInput-Dfue2spU.js → TagInput-CICgcKUW.js} +1 -1
  54. package/static/assets/TerminalPage-PJ6jkMN5.js +1 -0
  55. package/static/assets/TerminalSessionPage-DuJ2n3mT.js +13 -0
  56. package/static/assets/{UserPreferencesPage-C9yXdIdZ.js → UserPreferencesPage-CyNuAJru.js} +1 -1
  57. package/static/assets/{UserSettingsPage-ba7UUKyG.js → UserSettingsPage-Bq2TlR-8.js} +1 -1
  58. package/static/assets/{UtilitiesPage-5Zot5Svs.js → UtilitiesPage-mZb2CtxF.js} +1 -1
  59. package/static/assets/{alert-BhJUdRgG.js → alert-CrqaDLRU.js} +1 -1
  60. package/static/assets/{arrow-down-BYk0k85B.js → arrow-down-CfqDgiIx.js} +1 -1
  61. package/static/assets/{arrow-left-TQT21N4S.js → arrow-left-CIkDScAO.js} +1 -1
  62. package/static/assets/{arrow-up-BkSi1XTr.js → arrow-up-DoOW0Uek.js} +1 -1
  63. package/static/assets/{badge-DS8SPg0q.js → badge-ikmjXCY_.js} +1 -1
  64. package/static/assets/{browser-modal-CgvW-bbQ.js → browser-modal-BJ16icY9.js} +2 -2
  65. package/static/assets/{calendar-AzmVUvxT.js → calendar-B3EFURS-.js} +1 -1
  66. package/static/assets/{card-C4vq6AMz.js → card-C0a11V0T.js} +1 -1
  67. package/static/assets/{chevron-left-CeRwwCqx.js → chevron-left-Cbq2vuK_.js} +1 -1
  68. package/static/assets/{chevrons-up-BdEukQhU.js → chevrons-up-C3zGaueJ.js} +1 -1
  69. package/static/assets/{circle-alert-C1JRie1k.js → circle-alert-BZyc7kNR.js} +1 -1
  70. package/static/assets/{circle-check-DxiLNIIS.js → circle-check-CC67C1b-.js} +1 -1
  71. package/static/assets/{circle-check-big-CQBGgL4s.js → circle-check-big-tNM4uoO8.js} +1 -1
  72. package/static/assets/{circle-play-qq_sLPT0.js → circle-play-DgDKRctX.js} +1 -1
  73. package/static/assets/{circle-x-kpttvnBF.js → circle-x-CbIXbCmI.js} +1 -1
  74. package/static/assets/{clipboard-CBBHo2mi.js → clipboard-Bhg2iYmV.js} +1 -1
  75. package/static/assets/{clock-Ck6whvmg.js → clock-DUwjJehj.js} +1 -1
  76. package/static/assets/{download-Cx4OcU33.js → download-CAr6POG8.js} +1 -1
  77. package/static/assets/{droid-BWtMgfPD.js → droid-DqWsM2dp.js} +5 -5
  78. package/static/assets/external-link-C552Tasx.js +6 -0
  79. package/static/assets/{eye-2CnfcAd3.js → eye-F-BxEo2B.js} +1 -1
  80. package/static/assets/{folder-git-2-CvwpDtwI.js → folder-git-2-BCO-gmji.js} +1 -1
  81. package/static/assets/index-Cb4mP03_.js +462 -0
  82. package/static/assets/index-DVTgTsDa.css +2 -0
  83. package/static/assets/{info-BO6_vv66.js → info-DZ6rfcGz.js} +1 -1
  84. package/static/assets/{label-B-1O5hdX.js → label-VXwOLYGm.js} +1 -1
  85. package/static/assets/{markdown-editor-H-rT6Hat.js → markdown-editor-DKBU3Sgf.js} +1 -1
  86. package/static/assets/{pause-CW39bpDf.js → pause-CJhh_zgy.js} +1 -1
  87. package/static/assets/{play-Bqd-yHZN.js → play-DqF_TESp.js} +1 -1
  88. package/static/assets/{plus-BWhZkuI2.js → plus-B-nP-2Bc.js} +1 -1
  89. package/static/assets/{radio-group-CHbb2Tdt.js → radio-group-CkXoX9re.js} +1 -1
  90. package/static/assets/{refresh-cw-CaOTs34G.js → refresh-cw-DNCHo2vd.js} +1 -1
  91. package/static/assets/{search-D2duvyuf.js → search-pT3zGN1p.js} +1 -1
  92. package/static/assets/{switch-BRsOxt7T.js → switch-DKgMJKx9.js} +1 -1
  93. package/static/assets/{tabs--rTULKlo.js → tabs-LRqYZP6q.js} +1 -1
  94. package/static/assets/{tag-BQ5pJD32.js → tag-CNJNVCjj.js} +1 -1
  95. package/static/assets/{terminal-preview-ewZC6O1I.js → terminal-preview-C_hF2vLs.js} +1 -1
  96. package/static/assets/{use-terminal-Cya6EYny.js → use-terminal-B3kcgG1Q.js} +1 -1
  97. package/static/assets/{zap-DQNov92q.js → zap-DeMKwLsi.js} +1 -1
  98. package/static/index.html +2 -2
  99. package/static/assets/AgentDetailPage-eVEOya9x.js +0 -1
  100. package/static/assets/AgentEditPage-Ds5XXaak.js +0 -1
  101. package/static/assets/AgentsPage-CE59G63x.js +0 -3
  102. package/static/assets/AgentsSettingsPage-uVKYxuov.js +0 -6
  103. package/static/assets/CodePage-DuHcwCCu.js +0 -2
  104. package/static/assets/IntegrationsSettingsPage-B4J4wDXt.js +0 -1
  105. package/static/assets/JobDetailPage-tHRLIpQI.js +0 -1
  106. package/static/assets/KnowledgeDetailPage-Dh315TpW.js +0 -1
  107. package/static/assets/KnowledgeEditPage-B5bNMMvh.js +0 -1
  108. package/static/assets/NewAgentPage-DIzdUSPb.js +0 -1
  109. package/static/assets/NewKnowledgePage-weR174CB.js +0 -9
  110. package/static/assets/ProjectPage-D9APD9HG.js +0 -1
  111. package/static/assets/ProposalDetailPage-DbUTHokj.js +0 -1
  112. package/static/assets/ProposalEditPage-97pQXtDD.js +0 -1
  113. package/static/assets/SchedulePage-C-Yevtfw.js +0 -4
  114. package/static/assets/SourceInput-CLzuDG61.js +0 -1
  115. package/static/assets/TerminalPage-BbA46X-G.js +0 -1
  116. package/static/assets/TerminalSessionPage-FueIeaIL.js +0 -13
  117. package/static/assets/index-C8_b70Ej.css +0 -2
  118. package/static/assets/index-Df8kpDhM.js +0 -468
@@ -1,7 +1,5 @@
1
1
  import { Hono } from 'hono';
2
2
  import { POST } from './route.js';
3
- import { POST as toolsPost } from './tools.js';
4
3
  const app = new Hono();
5
4
  app.post('/', POST);
6
- app.post('/tools', toolsPost);
7
5
  export default app;
@@ -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, model, context, contextContent, agentPersona, systemPrompt: systemOverride, maxTokens, enableTools, attachedContextFiles, stream } = await c.req.json();
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
- let openRouterKey = getGlobalApiKey('openrouter') || process.env.OPENROUTER_API_KEY;
46
- if (!openRouterKey) {
47
+ const apiKey = getGlobalApiKey('anthropic') || process.env.ANTHROPIC_API_KEY;
48
+ if (!apiKey) {
47
49
  return c.json({
48
- error: "OpenRouter API key not configured",
49
- hint: "Set via 'coconut config set-key -p openrouter -k <KEY>' (or 'nut config set-key -p openrouter -k <KEY>'), or set OPENROUTER_API_KEY env var"
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 systemPrompt = agentPersona
54
- ? `${baseSystem}\n\nThe following persona is authoritative and overrides general guidance above. You must strictly follow it.\n\n${agentPersona}`
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
- // Prepare tools for function calling
57
- // Note: proposals/knowledge/project/architecture are read+write, events is read-only
58
- const tools = enableTools ? [
59
- {
60
- type: "function",
61
- function: proposalsFullTool
62
- },
63
- {
64
- type: "function",
65
- function: knowledgeTool
66
- },
67
- {
68
- type: "function",
69
- function: eventsTool
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
- ] : null;
84
- const contextMessages = Array.isArray(attachedContextFiles)
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
- // sanitize roles to user/assistant only, preserve order
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
- const requestBody = {
111
- model: model || "openai/gpt-4o",
112
- messages: messagesPayload,
113
- temperature: 0.7,
114
- max_tokens: typeof maxTokens === 'number' ? Math.max(256, Math.min(8000, Math.floor(maxTokens))) : 8000,
115
- stream: stream === true,
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
- enableTools,
125
- tools: tools ? tools.length : 'null',
126
- hasToolChoice: !!requestBody.tool_choice,
127
- model: requestBody.model
150
+ toolCount: Object.keys(aiTools).length,
151
+ messageCount: messagesPayload.length,
152
+ model: 'claude-sonnet-4-5-20250929'
128
153
  });
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(requestBody),
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
- if (!response.ok) {
140
- const errorText = await response.text();
141
- console.error("OpenRouter API error:", {
142
- status: response.status,
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
- async function executeToolCalls(toolCalls) {
234
- const storage = new FileStorageAdapter();
235
- // Execute all tool calls in parallel for better performance
236
- const resultPromises = toolCalls.map(async (toolCall) => {
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 metadata = normalizeKnowledgeMetadata(data);
885
+ const normalizedMeta = normalizeKnowledgeMetadata(data);
1021
886
  // Use file mtime for updated timestamp instead of frontmatter
1022
- metadata.updated = stats.mtime.toISOString();
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,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,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';
@@ -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;