@usesidekick/react 0.1.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/README.md +246 -0
- package/dist/index.d.mts +358 -0
- package/dist/index.d.ts +358 -0
- package/dist/index.js +2470 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2403 -0
- package/dist/index.mjs.map +1 -0
- package/dist/jsx-dev-runtime.d.mts +21 -0
- package/dist/jsx-dev-runtime.d.ts +21 -0
- package/dist/jsx-dev-runtime.js +160 -0
- package/dist/jsx-dev-runtime.js.map +1 -0
- package/dist/jsx-dev-runtime.mjs +122 -0
- package/dist/jsx-dev-runtime.mjs.map +1 -0
- package/dist/jsx-runtime.d.mts +26 -0
- package/dist/jsx-runtime.d.ts +26 -0
- package/dist/jsx-runtime.js +150 -0
- package/dist/jsx-runtime.js.map +1 -0
- package/dist/jsx-runtime.mjs +109 -0
- package/dist/jsx-runtime.mjs.map +1 -0
- package/dist/server/index.d.mts +235 -0
- package/dist/server/index.d.ts +235 -0
- package/dist/server/index.js +642 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/index.mjs +597 -0
- package/dist/server/index.mjs.map +1 -0
- package/package.json +64 -0
- package/src/components/SidekickPanel.tsx +868 -0
- package/src/components/index.ts +1 -0
- package/src/context.tsx +157 -0
- package/src/flags.ts +47 -0
- package/src/index.ts +71 -0
- package/src/jsx-dev-runtime.ts +138 -0
- package/src/jsx-runtime.ts +159 -0
- package/src/loader.ts +35 -0
- package/src/primitives/behavior.ts +70 -0
- package/src/primitives/data.ts +91 -0
- package/src/primitives/index.ts +3 -0
- package/src/primitives/ui.ts +268 -0
- package/src/provider.tsx +1264 -0
- package/src/runtime-loader.ts +106 -0
- package/src/server/drizzle-adapter.ts +53 -0
- package/src/server/drizzle-schema.ts +16 -0
- package/src/server/generate.ts +578 -0
- package/src/server/handler.ts +343 -0
- package/src/server/index.ts +20 -0
- package/src/server/storage.ts +1 -0
- package/src/server/types.ts +49 -0
- package/src/types.ts +295 -0
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
// src/server/handler.ts
|
|
2
|
+
import * as fs from "fs/promises";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { NextResponse } from "next/server";
|
|
5
|
+
|
|
6
|
+
// src/server/generate.ts
|
|
7
|
+
var ALLOWED_IMPORTS = ["@usesidekick/react", "react"];
|
|
8
|
+
var FORBIDDEN_PATTERNS = [
|
|
9
|
+
/\bfetch\s*\(/,
|
|
10
|
+
/\beval\s*\(/,
|
|
11
|
+
/\bFunction\s*\(/,
|
|
12
|
+
/\bdocument\./,
|
|
13
|
+
/\bwindow\./,
|
|
14
|
+
/\blocalStorage\./,
|
|
15
|
+
/\bsessionStorage\./,
|
|
16
|
+
/\bXMLHttpRequest/,
|
|
17
|
+
/\bWebSocket/,
|
|
18
|
+
/\bimport\s*\(/,
|
|
19
|
+
/require\s*\(/
|
|
20
|
+
];
|
|
21
|
+
function formatDesignSystem(schema) {
|
|
22
|
+
const ds = schema.designSystem;
|
|
23
|
+
if (!ds) {
|
|
24
|
+
return "No design system information available. Use clean, minimal inline styles with neutral colors and standard spacing.";
|
|
25
|
+
}
|
|
26
|
+
const parts = [];
|
|
27
|
+
parts.push("All generated UI MUST look like it was built by the same team that built the host app. Study the style samples below and match the exact same patterns \u2014 colors, spacing, typography, border radii, hover states, and dark mode support.");
|
|
28
|
+
if (ds.framework) {
|
|
29
|
+
parts.push(`
|
|
30
|
+
**CSS Framework:** ${ds.framework}`);
|
|
31
|
+
}
|
|
32
|
+
if (ds.fontFamily) {
|
|
33
|
+
parts.push(`**Font:** ${ds.fontFamily}`);
|
|
34
|
+
}
|
|
35
|
+
if (ds.globalCss) {
|
|
36
|
+
parts.push("**Global CSS:**\n```css\n" + ds.globalCss + "\n```");
|
|
37
|
+
}
|
|
38
|
+
if (ds.componentStyles && ds.componentStyles.length > 0) {
|
|
39
|
+
parts.push("\n**Component Style Samples (actual className strings from the app \u2014 use these as your reference):**");
|
|
40
|
+
for (const cs of ds.componentStyles) {
|
|
41
|
+
parts.push(`
|
|
42
|
+
${cs.component} (${cs.file}):`);
|
|
43
|
+
for (const cn of cs.classNames) {
|
|
44
|
+
parts.push(` "${cn}"`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
parts.push("\nWhen adding new UI elements to an existing component, copy the exact className patterns from that component above. When creating standalone elements, pick the closest matching component pattern and follow it.");
|
|
48
|
+
}
|
|
49
|
+
return parts.join("\n");
|
|
50
|
+
}
|
|
51
|
+
function formatComponents(schema) {
|
|
52
|
+
const components = schema.components || [];
|
|
53
|
+
return components.map((c) => {
|
|
54
|
+
let entry = "- " + c.name + " (props: " + (c.props.join(", ") || "none") + ")";
|
|
55
|
+
if (c.renderStructure) {
|
|
56
|
+
entry += "\n Renders: " + c.renderStructure;
|
|
57
|
+
}
|
|
58
|
+
return entry;
|
|
59
|
+
}).join("\n");
|
|
60
|
+
}
|
|
61
|
+
function buildSystemPrompt(schema) {
|
|
62
|
+
const tick = "`";
|
|
63
|
+
const ticks = "```";
|
|
64
|
+
const dollar = "$";
|
|
65
|
+
return `You are a code generator for Sidekick, an SDK that allows users to customize web applications.
|
|
66
|
+
Your task is to generate override modules based on user requests.
|
|
67
|
+
|
|
68
|
+
## SDK Primitives Available
|
|
69
|
+
|
|
70
|
+
UI Primitives:
|
|
71
|
+
- sdk.ui.wrap(componentName, wrapperFn, options?) - Wrap any component by name to add behavior around it. options: { priority?, where?: (props) => boolean }
|
|
72
|
+
- sdk.ui.replace(componentName, Component) - Replace any component entirely by name
|
|
73
|
+
- sdk.ui.addColumn(tableId, config) - Add a column to a table (config: { header, accessor, render? })
|
|
74
|
+
- sdk.ui.renameColumn(tableId, originalHeader, newHeader) - Rename a column header
|
|
75
|
+
- sdk.ui.reorderColumns(tableId, headerOrder) - Reorder columns by header name (array of header strings)
|
|
76
|
+
- sdk.ui.hideColumn(tableId, header) - Hide a column by header name
|
|
77
|
+
- sdk.ui.filterRows(tableId, filterFn) - Filter table rows (filterFn receives a row object, returns true to keep)
|
|
78
|
+
- sdk.ui.addStyles(css) - Add CSS styles globally
|
|
79
|
+
- sdk.ui.setText(selector, text) - Change text content of elements matching CSS selector
|
|
80
|
+
- sdk.ui.setAttribute(selector, attr, value) - Set an attribute on matching elements
|
|
81
|
+
- sdk.ui.setStyle(selector, styles) - Apply inline styles to matching elements (styles is an object like { color: 'red' })
|
|
82
|
+
- sdk.ui.addClass(selector, className) - Add a CSS class to matching elements
|
|
83
|
+
- sdk.ui.removeClass(selector, className) - Remove a CSS class from matching elements
|
|
84
|
+
- sdk.ui.inject(selector, Component, position?) - Inject a React component before/after/prepend/append an element (default: 'after')
|
|
85
|
+
|
|
86
|
+
Data Primitives:
|
|
87
|
+
- sdk.data.computed(fieldName, computeFn) - Add a computed field
|
|
88
|
+
- sdk.data.addFilter(name, filterFn) - Add a filter preset
|
|
89
|
+
- sdk.data.transform(dataKey, transformFn) - Transform data
|
|
90
|
+
- sdk.data.intercept(pathPattern, handler) - Intercept and modify API responses (handler receives response JSON and { path, method })
|
|
91
|
+
|
|
92
|
+
Behavior Primitives:
|
|
93
|
+
- sdk.behavior.addKeyboardShortcut(keys, action, description?) - Register a keyboard shortcut (e.g., "ctrl+k", "shift+?")
|
|
94
|
+
- sdk.behavior.onDOMEvent(selector, eventType, handler) - Listen for DOM events on elements matching a CSS selector
|
|
95
|
+
- sdk.behavior.modifyRoute(pathPattern, handler) - Intercept navigation and optionally redirect
|
|
96
|
+
|
|
97
|
+
## App Design System
|
|
98
|
+
|
|
99
|
+
` + formatDesignSystem(schema) + "\n\n## App Schema\n\nComponents (targetable by name with wrap/replace):\n" + formatComponents(schema) + "\n\nData Models: " + JSON.stringify(schema.dataModels || []) + "\n\n## Example Overrides\n\n### Example 1: Wrap a component to add a badge (RECOMMENDED APPROACH)\n" + ticks + `tsx
|
|
100
|
+
import { createOverride, SDK } from '@usesidekick/react';
|
|
101
|
+
import { ComponentType } from 'react';
|
|
102
|
+
|
|
103
|
+
export default createOverride(
|
|
104
|
+
{
|
|
105
|
+
id: 'task-card-badge',
|
|
106
|
+
name: 'Task Card Badge',
|
|
107
|
+
description: 'Adds a "New" badge to task cards',
|
|
108
|
+
version: '1.0.0',
|
|
109
|
+
primitives: ['ui.wrap'],
|
|
110
|
+
},
|
|
111
|
+
(sdk: SDK) => {
|
|
112
|
+
sdk.ui.wrap('TaskCard', (Original: ComponentType<Record<string, unknown>>) => {
|
|
113
|
+
return function WrappedTaskCard(props: Record<string, unknown>) {
|
|
114
|
+
return (
|
|
115
|
+
<div style={{ position: 'relative' }}>
|
|
116
|
+
<span style={{
|
|
117
|
+
position: 'absolute',
|
|
118
|
+
top: '-8px',
|
|
119
|
+
right: '-8px',
|
|
120
|
+
backgroundColor: '#EF4444',
|
|
121
|
+
color: 'white',
|
|
122
|
+
fontSize: '10px',
|
|
123
|
+
fontWeight: 'bold',
|
|
124
|
+
padding: '2px 6px',
|
|
125
|
+
borderRadius: '9999px',
|
|
126
|
+
}}>
|
|
127
|
+
NEW
|
|
128
|
+
</span>
|
|
129
|
+
<Original {...props} />
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
);
|
|
136
|
+
` + ticks + "\n\n### Example 2: Replace a component entirely\n" + ticks + "tsx\nimport { createOverride, SDK } from '@usesidekick/react';\n\nconst CustomTaskModal = (props: Record<string, unknown>) => {\n const task = props.task as { title?: string; description?: string } | undefined;\n const onClose = props.onClose as () => void;\n\n return (\n <div style={{\n position: 'fixed',\n inset: 0,\n backgroundColor: 'rgba(0,0,0,0.5)',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n }}>\n <div style={{\n backgroundColor: 'white',\n borderRadius: '16px',\n padding: '24px',\n maxWidth: '500px',\n width: '100%',\n }}>\n <h2 style={{ fontSize: '24px', fontWeight: 'bold', marginBottom: '16px' }}>\n {task?.title || 'Task Details'}\n </h2>\n <p>{task?.description || 'No description'}</p>\n <button\n onClick={onClose}\n style={{\n marginTop: '16px',\n backgroundColor: '#3B82F6',\n color: 'white',\n padding: '8px 16px',\n borderRadius: '8px',\n border: 'none',\n cursor: 'pointer',\n }}\n >\n Close\n </button>\n </div>\n </div>\n );\n};\n\nexport default createOverride(\n {\n id: 'custom-task-modal',\n name: 'Custom Task Modal',\n description: 'Replaces the task modal with a custom design',\n version: '1.0.0',\n primitives: ['ui.replace'],\n },\n (sdk: SDK) => {\n sdk.ui.replace('TaskModal', CustomTaskModal);\n }\n);\n" + ticks + "\n\n### Example 3: Add a table column\n" + ticks + "tsx\nimport { createOverride, SDK } from '@usesidekick/react';\n\nexport default createOverride(\n {\n id: 'days-left-column',\n name: 'Days Left Column',\n description: 'Shows days until due date',\n version: '1.0.0',\n primitives: ['ui.addColumn'],\n },\n (sdk: SDK) => {\n sdk.ui.addColumn('task-table', {\n header: 'Days Left',\n accessor: (row: Record<string, unknown>) => {\n const dueDate = row.dueDate as string | undefined;\n if (!dueDate) return null;\n const days = Math.ceil((new Date(dueDate).getTime() - Date.now()) / (1000 * 60 * 60 * 24));\n return days;\n },\n render: (value: unknown) => {\n if (value === null) return <span>-</span>;\n const days = value as number;\n const color = days < 0 ? 'red' : days <= 3 ? 'orange' : 'green';\n return <span style={{ color }}>{days}d</span>;\n },\n });\n }\n);\n" + ticks + "\n\n### Example 4: Add global styles\n" + ticks + "tsx\nimport { createOverride, SDK } from '@usesidekick/react';\n\nexport default createOverride(\n {\n id: 'custom-theme',\n name: 'Custom Theme',\n description: 'Adds custom styling to the app',\n version: '1.0.0',\n primitives: ['ui.addStyles'],\n },\n (sdk: SDK) => {\n sdk.ui.addStyles(" + tick + "\n .task-card {\n border-radius: 12px;\n box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);\n }\n .task-card:hover {\n transform: translateY(-2px);\n transition: transform 0.2s ease;\n }\n " + tick + ");\n }\n);\n" + ticks + "\n\n### Example 5: Column operations (rename, reorder, hide)\n" + ticks + "tsx\nimport { createOverride, SDK } from '@usesidekick/react';\n\nexport default createOverride(\n {\n id: 'reorder-columns',\n name: 'Reorder Table Columns',\n description: 'Reorders Status and Importance columns and hides Assignee',\n version: '1.0.0',\n primitives: ['ui.reorderColumns', 'ui.hideColumn'],\n },\n (sdk: SDK) => {\n sdk.ui.reorderColumns('task-table', ['Title', 'Importance', 'Status', 'Project', 'Due Date']);\n sdk.ui.hideColumn('task-table', 'Assignee');\n }\n);\n" + ticks + "\n\n### Example 6: Interactive filter with sidebar nav button\n" + ticks + "tsx\nimport { createOverride, SDK } from '@usesidekick/react';\nimport { ComponentType, useState, useEffect } from 'react';\n\nlet _filterActive = false;\nconst _listeners = new Set<(active: boolean) => void>();\n\nfunction setFilterActive(active: boolean) {\n _filterActive = active;\n _listeners.forEach(fn => fn(active));\n}\n\nfunction useFilterActive(): [boolean, (active: boolean) => void] {\n const [active, setActive] = useState(_filterActive);\n useEffect(() => {\n const handler = (val: boolean) => setActive(val);\n _listeners.add(handler);\n return () => { _listeners.delete(handler); };\n }, []);\n return [active, setFilterActive];\n}\n\nexport default createOverride(\n {\n id: 'high-importance-filter',\n name: 'High Importance Filter',\n description: 'Adds a sidebar button to filter high importance tasks',\n version: '1.0.0',\n primitives: ['ui.wrap'],\n },\n (sdk: SDK) => {\n sdk.ui.wrap('Sidebar', (Original: ComponentType<Record<string, unknown>>) => {\n return function SidebarWithFilter(props: Record<string, unknown>) {\n const [isActive, setActive] = useFilterActive();\n return (\n <div style={{ display: 'contents' }}>\n <Original {...props} />\n <div style={{ padding: '0 16px', marginTop: '8px' }}>\n <button\n onClick={() => setActive(!isActive)}\n style={{\n backgroundColor: isActive ? '#EFF6FF' : 'transparent',\n color: isActive ? '#1D4ED8' : '#374151',\n }}\n >\n High Importance\n </button>\n </div>\n </div>\n );\n };\n });\n\n sdk.ui.wrap('TaskTable', (Original: ComponentType<Record<string, unknown>>) => {\n return function FilteredTaskTable(props: Record<string, unknown>) {\n const [isActive] = useFilterActive();\n if (!isActive) return <Original {...props} />;\n const tasks = (props.tasks || []) as Array<Record<string, unknown>>;\n const filtered = tasks.filter(t => t.importance === 'high');\n return <Original {...props} tasks={filtered} />;\n };\n });\n }\n);\n" + ticks + "\n\n### Example 7: Permanent row filter (always active when override is ON)\n" + ticks + "tsx\nimport { createOverride, SDK } from '@usesidekick/react';\n\nexport default createOverride(\n {\n id: 'hide-done-tasks',\n name: 'Hide Done Tasks',\n description: 'Hides completed tasks from the table',\n version: '1.0.0',\n primitives: ['ui.filterRows'],\n },\n (sdk: SDK) => {\n sdk.ui.filterRows('task-table', (row: Record<string, unknown>) => {\n return row.status !== 'done';\n });\n }\n);\n" + ticks + "\n\n### Example 8: Keyboard shortcut\n" + ticks + "tsx\nimport { createOverride, SDK } from '@usesidekick/react';\n\nexport default createOverride(\n {\n id: 'quick-search-shortcut',\n name: 'Quick Search Shortcut',\n description: 'Adds Ctrl+K shortcut to focus search',\n version: '1.0.0',\n primitives: ['behavior.addKeyboardShortcut'],\n },\n (sdk: SDK) => {\n sdk.behavior.addKeyboardShortcut('ctrl+k', () => {\n // Toggle search or custom action\n }, 'Open quick search');\n }\n);\n" + ticks + "\n\n### Example 9: DOM modification\n" + ticks + "tsx\nimport { createOverride, SDK } from '@usesidekick/react';\n\nexport default createOverride(\n {\n id: 'custom-dom-mods',\n name: 'Custom DOM Modifications',\n description: 'Changes sidebar title and styles task cards',\n version: '1.0.0',\n primitives: ['ui.setText', 'ui.setStyle'],\n },\n (sdk: SDK) => {\n sdk.ui.setText('.sidebar-title', 'My Custom App');\n sdk.ui.setStyle('.task-card', { borderLeft: '4px solid #3B82F6' });\n }\n);\n" + ticks + `
|
|
137
|
+
|
|
138
|
+
## Rules
|
|
139
|
+
|
|
140
|
+
1. ONLY import from '@usesidekick/react' or 'react' - NO other imports allowed
|
|
141
|
+
2. DO NOT use fetch, eval, document.*, window.*, localStorage, etc.
|
|
142
|
+
3. Use the createOverride function from @usesidekick/react
|
|
143
|
+
4. The activate function receives an SDK object with ui and data primitives
|
|
144
|
+
5. Generate React components using JSX with inline styles
|
|
145
|
+
6. Keep code simple and focused on the user's request
|
|
146
|
+
7. Use sdk.ui.wrap() to add behavior around existing components, use sdk.ui.replace() to fully swap components
|
|
147
|
+
8. When wrapping, always pass through all props to the Original component
|
|
148
|
+
9. ALWAYS prefer SDK primitives over raw wrapping for table operations
|
|
149
|
+
10. When using sdk.ui.wrap(), ALWAYS render <Original {...props} /> as-is
|
|
150
|
+
11. A wrapper's ONLY job is to add content BEFORE or AFTER <Original {...props} />, or to modify the props passed to it
|
|
151
|
+
12. For permanent row filtering, use sdk.ui.filterRows(tableId, filterFn)
|
|
152
|
+
13. Overrides MUST always act on the UI
|
|
153
|
+
14. For interactive/togglable behavior, use module-level shared state with a listener pattern
|
|
154
|
+
15. VISUAL INTEGRATION IS MANDATORY
|
|
155
|
+
16. Study the component style samples in the design system carefully
|
|
156
|
+
17. NEVER use absolute/fixed positioning to place elements inside existing components
|
|
157
|
+
18. New elements MUST be visually indistinguishable from existing elements
|
|
158
|
+
19. All interactive elements MUST have hover states
|
|
159
|
+
20. BEFORE writing any wrap() code, read the target component's "Renders:" tree
|
|
160
|
+
21. Structure wrappers as: optional-new-content-above + <Original {...props} /> + optional-new-content-below
|
|
161
|
+
22. CSS selectors MUST be valid CSS selectors supported by document.querySelectorAll()
|
|
162
|
+
|
|
163
|
+
## Output Format
|
|
164
|
+
|
|
165
|
+
Respond with ONLY a JSON object (no markdown, no explanation):
|
|
166
|
+
{
|
|
167
|
+
"manifest": {
|
|
168
|
+
"id": "override-id-kebab-case",
|
|
169
|
+
"name": "Human Readable Name",
|
|
170
|
+
"description": "What this override does",
|
|
171
|
+
"version": "1.0.0",
|
|
172
|
+
"primitives": ["list", "of", "primitives", "used"]
|
|
173
|
+
},
|
|
174
|
+
"code": "// Full TypeScript/TSX code here"
|
|
175
|
+
}`;
|
|
176
|
+
}
|
|
177
|
+
function buildUserPrompt(request, previousErrors, existingCode) {
|
|
178
|
+
let prompt;
|
|
179
|
+
if (existingCode) {
|
|
180
|
+
prompt = "You are MODIFYING an existing override. Here is the current code:\n```tsx\n" + existingCode + '\n```\n\nApply this change: "' + request + '"\n\nIMPORTANT: Keep the same override ID. Preserve all existing functionality unless the user explicitly asks to change it. Return the complete modified code.';
|
|
181
|
+
} else {
|
|
182
|
+
prompt = 'Generate an override module for this request: "' + request + '"';
|
|
183
|
+
}
|
|
184
|
+
if (previousErrors && previousErrors.length > 0) {
|
|
185
|
+
prompt += "\n\nIMPORTANT: Previous generation attempt failed validation with these errors:\n";
|
|
186
|
+
prompt += previousErrors.map((e) => "- " + e).join("\n");
|
|
187
|
+
prompt += "\n\nPlease fix these issues in your response.";
|
|
188
|
+
}
|
|
189
|
+
return prompt;
|
|
190
|
+
}
|
|
191
|
+
async function callAI(request, schema, apiKey, previousErrors, existingCode) {
|
|
192
|
+
const systemPrompt = buildSystemPrompt(schema);
|
|
193
|
+
const userPrompt = buildUserPrompt(request, previousErrors, existingCode);
|
|
194
|
+
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
195
|
+
method: "POST",
|
|
196
|
+
headers: {
|
|
197
|
+
"Content-Type": "application/json",
|
|
198
|
+
"x-api-key": apiKey,
|
|
199
|
+
"anthropic-version": "2023-06-01"
|
|
200
|
+
},
|
|
201
|
+
body: JSON.stringify({
|
|
202
|
+
model: "claude-sonnet-4-20250514",
|
|
203
|
+
max_tokens: 4096,
|
|
204
|
+
system: systemPrompt,
|
|
205
|
+
messages: [{ role: "user", content: userPrompt }]
|
|
206
|
+
})
|
|
207
|
+
});
|
|
208
|
+
if (!response.ok) {
|
|
209
|
+
const error = await response.text();
|
|
210
|
+
throw new Error("API request failed: " + response.status + " " + error);
|
|
211
|
+
}
|
|
212
|
+
const result = await response.json();
|
|
213
|
+
const textContent = result.content.find((c) => c.type === "text");
|
|
214
|
+
if (!textContent || !textContent.text) {
|
|
215
|
+
throw new Error("No text content in API response");
|
|
216
|
+
}
|
|
217
|
+
return parseAIResponse(textContent.text);
|
|
218
|
+
}
|
|
219
|
+
function parseAIResponse(text2) {
|
|
220
|
+
const jsonMatch = text2.match(/\{[\s\S]*\}/);
|
|
221
|
+
if (!jsonMatch) {
|
|
222
|
+
throw new Error("No JSON found in AI response");
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
226
|
+
if (!parsed.manifest || !parsed.code) {
|
|
227
|
+
throw new Error("Invalid response structure");
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
manifest: parsed.manifest,
|
|
231
|
+
code: parsed.code
|
|
232
|
+
};
|
|
233
|
+
} catch (e) {
|
|
234
|
+
throw new Error("Failed to parse AI response: " + (e instanceof Error ? e.message : String(e)));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function validateCode(code) {
|
|
238
|
+
const errors = [];
|
|
239
|
+
const importMatches = code.matchAll(/import\s+.*?\s+from\s+['"]([^'"]+)['"]/g);
|
|
240
|
+
for (const match of importMatches) {
|
|
241
|
+
const moduleName = match[1];
|
|
242
|
+
const isAllowed = ALLOWED_IMPORTS.some(
|
|
243
|
+
(allowed) => moduleName === allowed || moduleName.startsWith(allowed + "/")
|
|
244
|
+
);
|
|
245
|
+
if (!isAllowed) {
|
|
246
|
+
errors.push("Forbidden import: " + moduleName);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
for (const pattern of FORBIDDEN_PATTERNS) {
|
|
250
|
+
if (pattern.test(code)) {
|
|
251
|
+
errors.push("Forbidden pattern: " + pattern.source);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return { valid: errors.length === 0, errors };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// src/server/handler.ts
|
|
258
|
+
function getAction(req) {
|
|
259
|
+
const url = new URL(req.url);
|
|
260
|
+
const segments = url.pathname.split("/").filter(Boolean);
|
|
261
|
+
return segments[segments.length - 1] || "";
|
|
262
|
+
}
|
|
263
|
+
async function readSchema(schemaPath) {
|
|
264
|
+
const candidates = schemaPath ? [schemaPath] : [
|
|
265
|
+
path.join(process.cwd(), "src/sidekick/schema.json"),
|
|
266
|
+
path.join(process.cwd(), "sidekick/schema.json")
|
|
267
|
+
];
|
|
268
|
+
for (const candidate of candidates) {
|
|
269
|
+
try {
|
|
270
|
+
const content = await fs.readFile(candidate, "utf-8");
|
|
271
|
+
return JSON.parse(content);
|
|
272
|
+
} catch {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
throw new Error("Failed to read schema.json");
|
|
277
|
+
}
|
|
278
|
+
function safeParsePrimitives(primitives) {
|
|
279
|
+
try {
|
|
280
|
+
const parsed = JSON.parse(primitives);
|
|
281
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
282
|
+
} catch {
|
|
283
|
+
return [];
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
async function handleListOverrides(storage) {
|
|
287
|
+
const overrides = await storage.listOverrides();
|
|
288
|
+
return NextResponse.json(
|
|
289
|
+
{
|
|
290
|
+
success: true,
|
|
291
|
+
overrides: overrides.map((o) => ({
|
|
292
|
+
id: o.id,
|
|
293
|
+
name: o.name,
|
|
294
|
+
description: o.description,
|
|
295
|
+
version: o.version,
|
|
296
|
+
primitives: safeParsePrimitives(o.primitives),
|
|
297
|
+
code: o.code,
|
|
298
|
+
enabled: o.enabled,
|
|
299
|
+
createdAt: o.createdAt,
|
|
300
|
+
updatedAt: o.updatedAt
|
|
301
|
+
}))
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
headers: {
|
|
305
|
+
"Cache-Control": "no-store, no-cache, must-revalidate"
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
async function safeParseBody(req) {
|
|
311
|
+
try {
|
|
312
|
+
return await req.json();
|
|
313
|
+
} catch {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
async function handleGenerate(req, storage, schemaPath) {
|
|
318
|
+
const body = await safeParseBody(req);
|
|
319
|
+
if (!body) {
|
|
320
|
+
return NextResponse.json(
|
|
321
|
+
{ success: false, error: "Invalid JSON in request body" },
|
|
322
|
+
{ status: 400 }
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
const { request, overrideId } = body;
|
|
326
|
+
if (!request || typeof request !== "string") {
|
|
327
|
+
return NextResponse.json(
|
|
328
|
+
{ success: false, error: "Missing request parameter" },
|
|
329
|
+
{ status: 400 }
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
let existingCode;
|
|
333
|
+
if (overrideId) {
|
|
334
|
+
const existing = await storage.getOverride(overrideId);
|
|
335
|
+
if (!existing) {
|
|
336
|
+
return NextResponse.json(
|
|
337
|
+
{ success: false, error: `Override "${overrideId}" not found` },
|
|
338
|
+
{ status: 404 }
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
existingCode = existing.code;
|
|
342
|
+
}
|
|
343
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
344
|
+
if (!apiKey) {
|
|
345
|
+
return NextResponse.json(
|
|
346
|
+
{ success: false, error: "ANTHROPIC_API_KEY not configured" },
|
|
347
|
+
{ status: 500 }
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
let schema;
|
|
351
|
+
try {
|
|
352
|
+
schema = await readSchema(schemaPath);
|
|
353
|
+
} catch {
|
|
354
|
+
return NextResponse.json(
|
|
355
|
+
{ success: false, error: "Failed to read schema.json" },
|
|
356
|
+
{ status: 500 }
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
const MAX_RETRIES = 3;
|
|
360
|
+
let lastError;
|
|
361
|
+
let lastValidationErrors;
|
|
362
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
363
|
+
try {
|
|
364
|
+
const generated = await callAI(request, schema, apiKey, lastValidationErrors, existingCode);
|
|
365
|
+
if (overrideId) {
|
|
366
|
+
generated.manifest.id = overrideId;
|
|
367
|
+
}
|
|
368
|
+
const validation = validateCode(generated.code);
|
|
369
|
+
if (!validation.valid) {
|
|
370
|
+
lastValidationErrors = validation.errors;
|
|
371
|
+
lastError = `Validation failed: ${validation.errors.join(", ")}`;
|
|
372
|
+
if (attempt < MAX_RETRIES) {
|
|
373
|
+
console.log(`[Sidekick] Attempt ${attempt} failed validation, retrying...`);
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
return NextResponse.json({
|
|
377
|
+
success: false,
|
|
378
|
+
error: lastError,
|
|
379
|
+
validationErrors: lastValidationErrors
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
const existing = await storage.getOverride(generated.manifest.id);
|
|
383
|
+
if (existing) {
|
|
384
|
+
await storage.updateOverride(generated.manifest.id, {
|
|
385
|
+
name: generated.manifest.name,
|
|
386
|
+
description: generated.manifest.description,
|
|
387
|
+
version: generated.manifest.version,
|
|
388
|
+
primitives: JSON.stringify(generated.manifest.primitives),
|
|
389
|
+
code: generated.code
|
|
390
|
+
});
|
|
391
|
+
} else {
|
|
392
|
+
await storage.createOverride({
|
|
393
|
+
id: generated.manifest.id,
|
|
394
|
+
name: generated.manifest.name,
|
|
395
|
+
description: generated.manifest.description,
|
|
396
|
+
version: generated.manifest.version,
|
|
397
|
+
primitives: JSON.stringify(generated.manifest.primitives),
|
|
398
|
+
code: generated.code,
|
|
399
|
+
enabled: true,
|
|
400
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
401
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
return NextResponse.json({
|
|
405
|
+
success: true,
|
|
406
|
+
overrideId: generated.manifest.id,
|
|
407
|
+
name: generated.manifest.name,
|
|
408
|
+
description: generated.manifest.description
|
|
409
|
+
});
|
|
410
|
+
} catch (e) {
|
|
411
|
+
lastError = e instanceof Error ? e.message : String(e);
|
|
412
|
+
if (attempt === MAX_RETRIES) {
|
|
413
|
+
return NextResponse.json({
|
|
414
|
+
success: false,
|
|
415
|
+
error: `Failed after ${MAX_RETRIES} attempts: ${lastError}`
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return NextResponse.json({
|
|
421
|
+
success: false,
|
|
422
|
+
error: lastError || "Unknown error"
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
async function handleToggle(req, storage) {
|
|
426
|
+
const body = await safeParseBody(req);
|
|
427
|
+
if (!body) {
|
|
428
|
+
return NextResponse.json(
|
|
429
|
+
{ success: false, error: "Invalid JSON in request body" },
|
|
430
|
+
{ status: 400 }
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
const { overrideId, enabled } = body;
|
|
434
|
+
if (!overrideId || typeof overrideId !== "string") {
|
|
435
|
+
return NextResponse.json(
|
|
436
|
+
{ success: false, error: "Missing overrideId parameter" },
|
|
437
|
+
{ status: 400 }
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
if (typeof enabled !== "boolean") {
|
|
441
|
+
return NextResponse.json(
|
|
442
|
+
{ success: false, error: "Missing or invalid enabled parameter" },
|
|
443
|
+
{ status: 400 }
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
const existing = await storage.getOverride(overrideId);
|
|
447
|
+
if (!existing) {
|
|
448
|
+
return NextResponse.json(
|
|
449
|
+
{ success: false, error: "Override not found" },
|
|
450
|
+
{ status: 404 }
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
await storage.updateOverride(overrideId, { enabled });
|
|
454
|
+
const updated = await storage.getOverride(overrideId);
|
|
455
|
+
return NextResponse.json({
|
|
456
|
+
success: true,
|
|
457
|
+
overrideId,
|
|
458
|
+
enabled: updated?.enabled ?? enabled
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
async function handleDelete(req, storage) {
|
|
462
|
+
const body = await safeParseBody(req);
|
|
463
|
+
if (!body) {
|
|
464
|
+
return NextResponse.json(
|
|
465
|
+
{ success: false, error: "Invalid JSON in request body" },
|
|
466
|
+
{ status: 400 }
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
const { overrideId } = body;
|
|
470
|
+
if (!overrideId || typeof overrideId !== "string") {
|
|
471
|
+
return NextResponse.json(
|
|
472
|
+
{ success: false, error: "Missing overrideId parameter" },
|
|
473
|
+
{ status: 400 }
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
const existing = await storage.getOverride(overrideId);
|
|
477
|
+
if (!existing) {
|
|
478
|
+
return NextResponse.json(
|
|
479
|
+
{ success: false, error: "Override not found" },
|
|
480
|
+
{ status: 404 }
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
await storage.deleteOverride(overrideId);
|
|
484
|
+
return NextResponse.json({
|
|
485
|
+
success: true,
|
|
486
|
+
message: `Override "${overrideId}" deleted successfully`
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
function createSidekickHandler(options) {
|
|
490
|
+
const { storage, schemaPath } = options;
|
|
491
|
+
return {
|
|
492
|
+
async GET(req) {
|
|
493
|
+
try {
|
|
494
|
+
const action = getAction(req);
|
|
495
|
+
if (action === "overrides") {
|
|
496
|
+
return handleListOverrides(storage);
|
|
497
|
+
}
|
|
498
|
+
return NextResponse.json(
|
|
499
|
+
{ success: false, error: `Unknown GET action: ${action}` },
|
|
500
|
+
{ status: 404 }
|
|
501
|
+
);
|
|
502
|
+
} catch (e) {
|
|
503
|
+
console.error("[Sidekick] Handler error:", e);
|
|
504
|
+
return NextResponse.json(
|
|
505
|
+
{ success: false, error: e instanceof Error ? e.message : "Unknown error" },
|
|
506
|
+
{ status: 500 }
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
},
|
|
510
|
+
async POST(req) {
|
|
511
|
+
try {
|
|
512
|
+
const action = getAction(req);
|
|
513
|
+
switch (action) {
|
|
514
|
+
case "generate":
|
|
515
|
+
return handleGenerate(req, storage, schemaPath);
|
|
516
|
+
case "toggle":
|
|
517
|
+
return handleToggle(req, storage);
|
|
518
|
+
case "delete":
|
|
519
|
+
return handleDelete(req, storage);
|
|
520
|
+
default:
|
|
521
|
+
return NextResponse.json(
|
|
522
|
+
{ success: false, error: `Unknown POST action: ${action}` },
|
|
523
|
+
{ status: 404 }
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
} catch (e) {
|
|
527
|
+
console.error("[Sidekick] Handler error:", e);
|
|
528
|
+
return NextResponse.json(
|
|
529
|
+
{ success: false, error: e instanceof Error ? e.message : "Unknown error" },
|
|
530
|
+
{ status: 500 }
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// src/server/drizzle-adapter.ts
|
|
538
|
+
import { eq } from "drizzle-orm";
|
|
539
|
+
function createDrizzleStorage(db, overridesTable) {
|
|
540
|
+
return {
|
|
541
|
+
async listOverrides() {
|
|
542
|
+
const rows = await db.select().from(overridesTable).orderBy(overridesTable.createdAt);
|
|
543
|
+
return rows;
|
|
544
|
+
},
|
|
545
|
+
async getOverride(id) {
|
|
546
|
+
const rows = await db.select().from(overridesTable).where(eq(overridesTable.id, id));
|
|
547
|
+
return rows[0] ?? null;
|
|
548
|
+
},
|
|
549
|
+
async createOverride(data) {
|
|
550
|
+
await db.insert(overridesTable).values({
|
|
551
|
+
id: data.id,
|
|
552
|
+
name: data.name,
|
|
553
|
+
description: data.description,
|
|
554
|
+
version: data.version,
|
|
555
|
+
primitives: data.primitives,
|
|
556
|
+
code: data.code,
|
|
557
|
+
enabled: data.enabled ?? true,
|
|
558
|
+
createdAt: data.createdAt ?? /* @__PURE__ */ new Date(),
|
|
559
|
+
updatedAt: data.updatedAt ?? /* @__PURE__ */ new Date()
|
|
560
|
+
});
|
|
561
|
+
},
|
|
562
|
+
async updateOverride(id, data) {
|
|
563
|
+
const { id: _id, createdAt: _ca, ...updateData } = data;
|
|
564
|
+
await db.update(overridesTable).set({ ...updateData, updatedAt: /* @__PURE__ */ new Date() }).where(eq(overridesTable.id, id));
|
|
565
|
+
},
|
|
566
|
+
async deleteOverride(id) {
|
|
567
|
+
await db.delete(overridesTable).where(eq(overridesTable.id, id));
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// src/server/drizzle-schema.ts
|
|
573
|
+
import { pgTable, text, timestamp, boolean } from "drizzle-orm/pg-core";
|
|
574
|
+
var sidekickOverrides = pgTable("overrides", {
|
|
575
|
+
id: text("id").primaryKey(),
|
|
576
|
+
name: text("name").notNull(),
|
|
577
|
+
description: text("description").notNull(),
|
|
578
|
+
version: text("version").notNull().default("1.0.0"),
|
|
579
|
+
primitives: text("primitives").notNull(),
|
|
580
|
+
// JSON array stored as text
|
|
581
|
+
code: text("code").notNull(),
|
|
582
|
+
enabled: boolean("enabled").notNull().default(true),
|
|
583
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
584
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull()
|
|
585
|
+
});
|
|
586
|
+
export {
|
|
587
|
+
buildSystemPrompt,
|
|
588
|
+
buildUserPrompt,
|
|
589
|
+
callAI,
|
|
590
|
+
createDrizzleStorage,
|
|
591
|
+
createSidekickHandler,
|
|
592
|
+
formatDesignSystem,
|
|
593
|
+
parseAIResponse,
|
|
594
|
+
sidekickOverrides,
|
|
595
|
+
validateCode
|
|
596
|
+
};
|
|
597
|
+
//# sourceMappingURL=index.mjs.map
|