@symbo.ls/mcp 1.0.13 → 1.0.17
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 +86 -32
- package/bin/symbols-mcp.js +846 -9
- package/package.json +2 -2
- package/symbols_mcp/skills/AUDIT.md +2 -0
- package/symbols_mcp/skills/CLI.md +374 -0
- package/symbols_mcp/skills/COMMON_MISTAKES.md +225 -0
- package/symbols_mcp/skills/DEFAULT_COMPONENTS.md +6 -22
- package/symbols_mcp/skills/LEARNINGS.md +1 -1
- package/symbols_mcp/skills/PATTERNS.md +14 -12
- package/symbols_mcp/skills/PROJECT_STRUCTURE.md +148 -37
- package/symbols_mcp/skills/RULES.md +685 -31
- package/symbols_mcp/skills/SDK.md +440 -0
- package/symbols_mcp/skills/SYNTAX.md +22 -19
package/bin/symbols-mcp.js
CHANGED
|
@@ -3,8 +3,15 @@
|
|
|
3
3
|
const fs = require('fs')
|
|
4
4
|
const path = require('path')
|
|
5
5
|
const readline = require('readline')
|
|
6
|
+
const https = require('https')
|
|
7
|
+
const http = require('http')
|
|
6
8
|
|
|
7
9
|
const SKILLS_DIR = path.join(__dirname, '..', 'symbols_mcp', 'skills')
|
|
10
|
+
const API_BASE = process.env.SYMBOLS_API_URL || 'https://api.symbols.app'
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Helpers — skills
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
8
15
|
|
|
9
16
|
function readSkill(filename) {
|
|
10
17
|
const p = path.join(SKILLS_DIR, filename)
|
|
@@ -37,6 +44,382 @@ function searchDocs(query, maxResults = 3) {
|
|
|
37
44
|
return results.length ? JSON.stringify(results, null, 2) : `No results found for '${query}'`
|
|
38
45
|
}
|
|
39
46
|
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Helpers — API
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
const AUTH_HELP = `To authenticate, provide one of:
|
|
52
|
+
- **token**: JWT from \`smbls login\` (stored in ~/.smblsrc) or env var SYMBOLS_TOKEN
|
|
53
|
+
- **api_key**: API key (sk_live_...) from your project's integration settings
|
|
54
|
+
|
|
55
|
+
To get a token:
|
|
56
|
+
1. Run \`smbls login\` in your terminal, or
|
|
57
|
+
2. Use the \`login\` tool with your email and password`
|
|
58
|
+
|
|
59
|
+
function apiRequest(method, urlPath, { token, apiKey, body } = {}) {
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
const url = new URL(API_BASE + urlPath)
|
|
62
|
+
const isHttps = url.protocol === 'https:'
|
|
63
|
+
const options = {
|
|
64
|
+
hostname: url.hostname,
|
|
65
|
+
port: url.port || (isHttps ? 443 : 80),
|
|
66
|
+
path: url.pathname + url.search,
|
|
67
|
+
method,
|
|
68
|
+
headers: { 'Content-Type': 'application/json' }
|
|
69
|
+
}
|
|
70
|
+
if (apiKey) options.headers['Authorization'] = `ApiKey ${apiKey}`
|
|
71
|
+
else if (token) options.headers['Authorization'] = `Bearer ${token}`
|
|
72
|
+
|
|
73
|
+
const payload = body ? JSON.stringify(body) : null
|
|
74
|
+
if (payload) options.headers['Content-Length'] = Buffer.byteLength(payload)
|
|
75
|
+
|
|
76
|
+
const req = (isHttps ? https : http).request(options, res => {
|
|
77
|
+
let data = ''
|
|
78
|
+
res.on('data', chunk => { data += chunk })
|
|
79
|
+
res.on('end', () => {
|
|
80
|
+
try { resolve(JSON.parse(data)) }
|
|
81
|
+
catch { resolve({ success: false, error: `HTTP ${res.statusCode}`, message: data }) }
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
req.on('error', err => reject(err))
|
|
85
|
+
req.setTimeout(30000, () => { req.destroy(); reject(new Error('Request timeout')) })
|
|
86
|
+
if (payload) req.write(payload)
|
|
87
|
+
req.end()
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function requireAuth(token, apiKey) {
|
|
92
|
+
if (!token && !apiKey) return `Authentication required.\n\n${AUTH_HELP}`
|
|
93
|
+
return null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function resolveProjectId(project, token, apiKey) {
|
|
97
|
+
if (!project) return { id: '', error: 'Project identifier is required.' }
|
|
98
|
+
const isKey = project.startsWith('pr_') || !/^[0-9a-f]+$/.test(project)
|
|
99
|
+
if (isKey) {
|
|
100
|
+
const result = await apiRequest('GET', `/core/projects/key/${project}`, { token, apiKey })
|
|
101
|
+
if (result.success) return { id: result.data?._id || '', error: null }
|
|
102
|
+
return { id: '', error: `Project '${project}' not found: ${result.error || 'unknown error'}` }
|
|
103
|
+
}
|
|
104
|
+
return { id: project, error: null }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Helpers — JS-to-JSON conversion (mirrors frank pipeline)
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
const DATA_KEYS = ['components', 'pages', 'snippets', 'functions', 'methods',
|
|
112
|
+
'designSystem', 'state', 'dependencies', 'files', 'config']
|
|
113
|
+
const CODE_SECTIONS = new Set(['components', 'pages', 'functions', 'methods', 'snippets'])
|
|
114
|
+
|
|
115
|
+
function findObjectEnd(code, start) {
|
|
116
|
+
if (start >= code.length) return -1
|
|
117
|
+
let i = start
|
|
118
|
+
while (i < code.length && ' \t\n\r'.includes(code[i])) i++
|
|
119
|
+
if (i >= code.length || !'{['.includes(code[i])) return -1
|
|
120
|
+
const opener = code[i]
|
|
121
|
+
const closer = opener === '{' ? '}' : ']'
|
|
122
|
+
let depth = 1
|
|
123
|
+
i++
|
|
124
|
+
let inString = null, inTemplate = false, escaped = false
|
|
125
|
+
while (i < code.length && depth > 0) {
|
|
126
|
+
const ch = code[i]
|
|
127
|
+
if (escaped) { escaped = false; i++; continue }
|
|
128
|
+
if (ch === '\\') { escaped = true; i++; continue }
|
|
129
|
+
if (inString) { if (ch === inString) inString = null; i++; continue }
|
|
130
|
+
if (inTemplate) { if (ch === '`') inTemplate = false; i++; continue }
|
|
131
|
+
if (ch === "'" || ch === '"') inString = ch
|
|
132
|
+
else if (ch === '`') inTemplate = true
|
|
133
|
+
else if (ch === opener) depth++
|
|
134
|
+
else if (ch === closer) depth--
|
|
135
|
+
i++
|
|
136
|
+
}
|
|
137
|
+
return depth === 0 ? i : -1
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function parseJsToJson(sourceCode) {
|
|
141
|
+
const result = {}
|
|
142
|
+
const code = sourceCode.replace(/^\s*import\s+.*$/gm, '')
|
|
143
|
+
const stripped = code.trim()
|
|
144
|
+
if (stripped.startsWith('{')) {
|
|
145
|
+
try { return JSON.parse(stripped) } catch {}
|
|
146
|
+
}
|
|
147
|
+
const exportRe = /export\s+const\s+(\w+)\s*=\s*/g
|
|
148
|
+
let m
|
|
149
|
+
const matches = []
|
|
150
|
+
while ((m = exportRe.exec(code)) !== null) matches.push(m)
|
|
151
|
+
for (const match of matches) {
|
|
152
|
+
const name = match[1]
|
|
153
|
+
const start = match.index + match[0].length
|
|
154
|
+
const end = findObjectEnd(code, start)
|
|
155
|
+
if (end === -1) continue
|
|
156
|
+
result[name] = code.slice(start, end).trim()
|
|
157
|
+
}
|
|
158
|
+
if (!matches.length) {
|
|
159
|
+
const defMatch = /export\s+default\s+/.exec(code)
|
|
160
|
+
if (defMatch) {
|
|
161
|
+
const start = defMatch.index + defMatch[0].length
|
|
162
|
+
const end = findObjectEnd(code, start)
|
|
163
|
+
if (end !== -1) result['__default__'] = code.slice(start, end).trim()
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return result
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function jsObjToJson(rawJs) {
|
|
170
|
+
let s = rawJs.trim()
|
|
171
|
+
// Stringify function values
|
|
172
|
+
s = stringifyFunctionsInJs(s)
|
|
173
|
+
// Normalize quotes
|
|
174
|
+
s = normalizeQuotes(s)
|
|
175
|
+
// Quote unquoted keys
|
|
176
|
+
s = s.replace(/(?<=[\{,\n])\s*([a-zA-Z_$][\w$]*)\s*:/g, ' "$1":')
|
|
177
|
+
s = s.replace(/(?<=[\{,\n])\s*(@[\w$]+)\s*:/g, ' "$1":')
|
|
178
|
+
// Remove trailing commas
|
|
179
|
+
s = s.replace(/,\s*([}\]])/g, '$1')
|
|
180
|
+
try { return JSON.parse(s) } catch { return s }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function stringifyFunctionsInJs(code) {
|
|
184
|
+
const result = []
|
|
185
|
+
let i = 0
|
|
186
|
+
while (i < code.length) {
|
|
187
|
+
const rest = code.slice(i)
|
|
188
|
+
const arrowMatch = rest.match(/^(\([^)]*\)\s*=>|\w+\s*=>)\s*/)
|
|
189
|
+
const funcMatch = rest.match(/^function\s*\w*\s*\(/)
|
|
190
|
+
if (arrowMatch && isValuePosition(code, i)) {
|
|
191
|
+
const fnEnd = findFunctionEnd(code, i, true)
|
|
192
|
+
if (fnEnd > i) {
|
|
193
|
+
result.push(JSON.stringify(code.slice(i, fnEnd).trim()))
|
|
194
|
+
i = fnEnd; continue
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (funcMatch && isValuePosition(code, i)) {
|
|
198
|
+
const fnEnd = findFunctionEnd(code, i, false)
|
|
199
|
+
if (fnEnd > i) {
|
|
200
|
+
result.push(JSON.stringify(code.slice(i, fnEnd).trim()))
|
|
201
|
+
i = fnEnd; continue
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
result.push(code[i]); i++
|
|
205
|
+
}
|
|
206
|
+
return result.join('')
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function isValuePosition(code, pos) {
|
|
210
|
+
let j = pos - 1
|
|
211
|
+
while (j >= 0 && ' \t\n\r'.includes(code[j])) j--
|
|
212
|
+
return j >= 0 && ':=,['.includes(code[j])
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function findFunctionEnd(code, start, isArrow) {
|
|
216
|
+
let i = start
|
|
217
|
+
if (isArrow) {
|
|
218
|
+
const arrowPos = code.indexOf('=>', i)
|
|
219
|
+
if (arrowPos === -1) return -1
|
|
220
|
+
i = arrowPos + 2
|
|
221
|
+
while (i < code.length && ' \t\n\r'.includes(code[i])) i++
|
|
222
|
+
if (i < code.length && code[i] === '{') return findObjectEnd(code, i)
|
|
223
|
+
let depth = 0
|
|
224
|
+
while (i < code.length) {
|
|
225
|
+
const ch = code[i]
|
|
226
|
+
if ('({['.includes(ch)) depth++
|
|
227
|
+
else if (')}]'.includes(ch)) { if (depth === 0) return i; depth-- }
|
|
228
|
+
else if (ch === ',' && depth === 0) return i
|
|
229
|
+
i++
|
|
230
|
+
}
|
|
231
|
+
return i
|
|
232
|
+
} else {
|
|
233
|
+
const brace = code.indexOf('{', i)
|
|
234
|
+
if (brace === -1) return -1
|
|
235
|
+
return findObjectEnd(code, brace)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function normalizeQuotes(s) {
|
|
240
|
+
const result = []
|
|
241
|
+
let i = 0
|
|
242
|
+
while (i < s.length) {
|
|
243
|
+
if (s[i] === "'") {
|
|
244
|
+
let j = i + 1
|
|
245
|
+
while (j < s.length) {
|
|
246
|
+
if (s[j] === '\\' && j + 1 < s.length) { j += 2; continue }
|
|
247
|
+
if (s[j] === "'") break
|
|
248
|
+
j++
|
|
249
|
+
}
|
|
250
|
+
const inner = s.slice(i + 1, j).replace(/"/g, '\\"')
|
|
251
|
+
result.push(`"${inner}"`); i = j + 1
|
|
252
|
+
} else if (s[i] === '"') {
|
|
253
|
+
let j = i + 1
|
|
254
|
+
while (j < s.length) {
|
|
255
|
+
if (s[j] === '\\' && j + 1 < s.length) { j += 2; continue }
|
|
256
|
+
if (s[j] === '"') break
|
|
257
|
+
j++
|
|
258
|
+
}
|
|
259
|
+
result.push(s.slice(i, j + 1)); i = j + 1
|
|
260
|
+
} else {
|
|
261
|
+
result.push(s[i]); i++
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return result.join('')
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function encodeSchemaCode(codeStr) {
|
|
268
|
+
return codeStr.replace(/\n/g, '/////n').replace(/`/g, '/////tilde')
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function buildSchemaItem(section, key, value) {
|
|
272
|
+
const codeStr = typeof value === 'string' ? value : JSON.stringify(value, null, 2)
|
|
273
|
+
const item = {
|
|
274
|
+
title: key, key, type: section,
|
|
275
|
+
code: encodeSchemaCode(`export default ${codeStr}`)
|
|
276
|
+
}
|
|
277
|
+
if (section === 'components' || section === 'pages') {
|
|
278
|
+
Object.assign(item, { settings: { gridOptions: {} }, props: {}, interactivity: [], dataTypes: [], error: null })
|
|
279
|
+
}
|
|
280
|
+
return item
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function buildChangesAndSchema(data) {
|
|
284
|
+
const changes = [], granular = [], orders = []
|
|
285
|
+
for (const [sectionKey, sectionData] of Object.entries(data)) {
|
|
286
|
+
if (!DATA_KEYS.includes(sectionKey)) continue
|
|
287
|
+
if (typeof sectionData !== 'object' || sectionData === null || Array.isArray(sectionData)) {
|
|
288
|
+
changes.push(['update', [sectionKey], sectionData])
|
|
289
|
+
granular.push(['update', [sectionKey], sectionData])
|
|
290
|
+
continue
|
|
291
|
+
}
|
|
292
|
+
const sectionItemKeys = []
|
|
293
|
+
for (const [itemKey, itemValue] of Object.entries(sectionData)) {
|
|
294
|
+
const itemPath = [sectionKey, itemKey]
|
|
295
|
+
changes.push(['update', itemPath, itemValue])
|
|
296
|
+
sectionItemKeys.push(itemKey)
|
|
297
|
+
if (typeof itemValue === 'object' && itemValue !== null && !Array.isArray(itemValue)) {
|
|
298
|
+
const itemKeys = []
|
|
299
|
+
for (const [propKey, propValue] of Object.entries(itemValue)) {
|
|
300
|
+
granular.push(['update', [...itemPath, propKey], propValue])
|
|
301
|
+
itemKeys.push(propKey)
|
|
302
|
+
}
|
|
303
|
+
if (itemKeys.length) orders.push({ path: itemPath, keys: itemKeys })
|
|
304
|
+
} else {
|
|
305
|
+
granular.push(['update', itemPath, itemValue])
|
|
306
|
+
}
|
|
307
|
+
if (CODE_SECTIONS.has(sectionKey)) {
|
|
308
|
+
const schemaItem = buildSchemaItem(sectionKey, itemKey, itemValue)
|
|
309
|
+
const schemaPath = ['schema', sectionKey, itemKey]
|
|
310
|
+
changes.push(['update', schemaPath, schemaItem])
|
|
311
|
+
granular.push(['delete', [...schemaPath, 'code']])
|
|
312
|
+
for (const [sk, sv] of Object.entries(schemaItem)) {
|
|
313
|
+
granular.push(['update', [...schemaPath, sk], sv])
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (sectionItemKeys.length) orders.push({ path: [sectionKey], keys: sectionItemKeys })
|
|
318
|
+
}
|
|
319
|
+
return { changes, granular, orders }
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ---------------------------------------------------------------------------
|
|
323
|
+
// Audit helpers (deterministic rule checking — mirrors server.py)
|
|
324
|
+
// ---------------------------------------------------------------------------
|
|
325
|
+
|
|
326
|
+
const V2_PATTERNS = [
|
|
327
|
+
[/\bextend\s*:/g, "v2 syntax: use 'extends' (plural) instead of 'extend'"],
|
|
328
|
+
[/\bchildExtend\s*:/g, "v2 syntax: use 'childExtends' (plural) instead of 'childExtend'"],
|
|
329
|
+
[/\bon\s*:\s*\{/g, "v2 syntax: flatten event handlers with onX prefix (e.g. onClick) instead of on: {} wrapper"],
|
|
330
|
+
[/\bprops\s*:\s*\{(?!\s*\})/g, "v2 syntax: flatten props directly on the component instead of props: {} wrapper"],
|
|
331
|
+
]
|
|
332
|
+
|
|
333
|
+
const RULE_CHECKS = [
|
|
334
|
+
[/\bimport\s+.*\bfrom\s+['"]\.\//, "FORBIDDEN: No imports between project files — reference components by PascalCase key name"],
|
|
335
|
+
[/\bexport\s+default\s+\{/, "Components should use named exports (export const Name = {}), not default exports"],
|
|
336
|
+
[/\bfunction\s+\w+\s*\(.*\)\s*\{[\s\S]*?return\s*\{/, "Components must be plain objects, not functions that return objects"],
|
|
337
|
+
[/\bextends\s*:\s*(?!['"])\w+/, "FORBIDDEN: extends must be a quoted string name (extends: 'Name'), not a variable reference — register in components/ and use string lookup (Rule 10)"],
|
|
338
|
+
[/extends\s*:\s*['"]Flex['"]/, "Replace extends: 'Flex' with flow: 'x' or flow: 'y' — do NOT just remove it, the element needs flow to stay flex (Rule 26)"],
|
|
339
|
+
[/extends\s*:\s*['"]Box['"]/, "Remove extends: 'Box' — every element is already a Box (Rule 26)"],
|
|
340
|
+
[/extends\s*:\s*['"]Text['"]/, "Remove extends: 'Text' — any element with text: is already Text (Rule 26)"],
|
|
341
|
+
[/\bchildExtends\s*:\s*\{/, "FORBIDDEN: childExtends must be a quoted string name, not an inline object — register as a named component (Rule 10)"],
|
|
342
|
+
[/(?:padding|margin|gap|width|height|fontSize|borderRadius|minWidth|maxWidth|minHeight|maxHeight|top|left|right|bottom|letterSpacing|lineHeight|borderWidth|outlineWidth)\s*:\s*['"]?\d+(?:\.\d+)?px/, "FORBIDDEN: No raw px values — use design system tokens (A, B, C, etc.) instead of hardcoded pixels (Rule 28)"],
|
|
343
|
+
[/(?:color|background|backgroundColor|borderColor|fill|stroke)\s*:\s*['"]#[0-9a-fA-F]/, "Use design system color tokens (primary, secondary, white, gray.5) instead of hardcoded hex colors (Rule 27)"],
|
|
344
|
+
[/(?:color|background|backgroundColor|borderColor|fill|stroke)\s*:\s*['"]rgb/, "Use design system color tokens instead of hardcoded rgb/rgba values (Rule 27)"],
|
|
345
|
+
[/(?:color|background|backgroundColor|borderColor|fill|stroke)\s*:\s*['"]hsl/, "Use design system color tokens instead of hardcoded hsl/hsla values (Rule 27)"],
|
|
346
|
+
[/<svg[\s>]/, "FORBIDDEN: Use the Icon component for SVG icons — store SVGs in designSystem/icons, never inline (Rule 29)"],
|
|
347
|
+
[/tag\s*:\s*['"]svg['"]/, "FORBIDDEN: Never use tag: 'svg' — store SVGs in designSystem/icons and use Icon component (Rule 29)"],
|
|
348
|
+
[/tag\s*:\s*['"]path['"]/, "FORBIDDEN: Never use tag: 'path' — store SVG paths in designSystem/icons and use Icon component (Rule 29)"],
|
|
349
|
+
[/extends\s*:\s*['"]Svg['"]/, "Use Icon component for icons, not Svg — Svg is only for decorative/structural SVGs (Rule 29)"],
|
|
350
|
+
[/\biconName\s*:/, "FORBIDDEN: Use icon: not iconName: — the prop is icon: 'name' matching a key in designSystem/icons (Rule 29)"],
|
|
351
|
+
[/document\.createElement\b/, "FORBIDDEN: No direct DOM manipulation — use DOMQL declarative object syntax instead (Rule 30)"],
|
|
352
|
+
[/\.querySelector\b/, "FORBIDDEN: No DOM queries — reference elements by key name in the DOMQL object tree (Rule 30)"],
|
|
353
|
+
[/\.appendChild\b/, "FORBIDDEN: No direct DOM manipulation — nest children as object keys or use children array (Rule 30)"],
|
|
354
|
+
[/\.removeChild\b/, "FORBIDDEN: No direct DOM manipulation — use if: (el, s) => condition to show/hide (Rule 30)"],
|
|
355
|
+
[/\.insertBefore\b/, "FORBIDDEN: No direct DOM manipulation — use children array ordering (Rule 30)"],
|
|
356
|
+
[/\.innerHTML\s*=/, "FORBIDDEN: No direct DOM manipulation — use text: or html: prop (Rule 30)"],
|
|
357
|
+
[/\.classList\./, "FORBIDDEN: No direct class manipulation — use isX + '.isX' pattern (Rule 19/30)"],
|
|
358
|
+
[/\.setAttribute\b/, "FORBIDDEN: No direct DOM manipulation — set attributes at root level in DOMQL (Rule 30)"],
|
|
359
|
+
[/\.addEventListener\b/, "FORBIDDEN: No direct event binding — use onX props: onClick, onInput, etc. (Rule 30)"],
|
|
360
|
+
[/\.style\.\w+\s*=/, "FORBIDDEN: No direct style manipulation — use DOMQL CSS-in-props (Rule 30)"],
|
|
361
|
+
[/html\s*:\s*\(?.*\)?\s*=>\s*/, "FORBIDDEN: Never use html: as a function returning markup — use DOMQL children, nesting, text:, and if: instead (Rule 31)"],
|
|
362
|
+
[/return\s*`<\w+/, "FORBIDDEN: Never return HTML template literals — use DOMQL declarative children and nesting (Rule 31)"],
|
|
363
|
+
[/style\s*=\s*['"`]/, "FORBIDDEN: No inline style= strings in html — use DOMQL CSS-in-props (Rule 31)"],
|
|
364
|
+
[/window\.innerWidth/, "FORBIDDEN: No window.innerWidth checks — use @mobileL, @tabletS responsive breakpoints (Rule 31)"],
|
|
365
|
+
[/\.parentNode\b/, "FORBIDDEN: No DOM traversal — use state and reactive props instead of walking the DOM tree (Rule 32)"],
|
|
366
|
+
[/\.childNodes\b/, "FORBIDDEN: No DOM traversal — use state-driven children with if: props (Rule 32)"],
|
|
367
|
+
[/\.textContent\b/, "FORBIDDEN: No DOM property access — use state and text: prop (Rule 32)"],
|
|
368
|
+
[/Array\.from\(\w+\.children\)/, "FORBIDDEN: No DOM child iteration — use state arrays with children/childExtends and if: filtering (Rule 32)"],
|
|
369
|
+
[/\.style\.display\s*=/, "FORBIDDEN: No style.display toggling — use show:/hide: to toggle visibility or if: to remove from DOM (Rule 32)"],
|
|
370
|
+
[/\.style\.cssText\s*=/, "FORBIDDEN: No direct cssText — use DOMQL CSS-in-props (Rule 32)"],
|
|
371
|
+
[/\.dataset\./, "FORBIDDEN: No dataset manipulation — use state and attr: for data-* attributes (Rule 32)"],
|
|
372
|
+
[/\.remove\(\)/, "FORBIDDEN: No DOM node removal — use if: (el, s) => condition to conditionally render (Rule 32)"],
|
|
373
|
+
[/el\.node\.\w+\s*=/, "FORBIDDEN: No direct el.node property assignment — use DOMQL props (placeholder:, value:, text:, etc.). Reading el.node is fine (Rule 39), writing is not (Rule 32)"],
|
|
374
|
+
[/document\.getElementById\b/, "FORBIDDEN: No document.getElementById — use el.lookdown('key') to find DOMQL elements (Rule 40)"],
|
|
375
|
+
[/document\.querySelectorAll\b/, "FORBIDDEN: No document.querySelectorAll — use el.lookdownAll('key') to find DOMQL elements (Rule 40)"],
|
|
376
|
+
[/el\.parent\.state\b/, "FORBIDDEN: Never use el.parent.state — with childrenAs: 'state', use s.field directly (Rule 36)"],
|
|
377
|
+
[/el\.context\.designSystem\b/, "FORBIDDEN: Never read designSystem from el.context in props — use token strings directly (Rule 38)"],
|
|
378
|
+
[/^const\s+\w+\s*=\s*(?:\(|function)/m, "FORBIDDEN: No module-level helper functions — move to functions/ and call via el.call('fnName') (Rule 33)"],
|
|
379
|
+
[/^let\s+\w+\s*=/m, "FORBIDDEN: No module-level variables — use el.scope for local state, functions/ for helpers (Rule 33)"],
|
|
380
|
+
[/^var\s+\w+\s*=/m, "FORBIDDEN: No module-level variables — use el.scope for local state, functions/ for helpers (Rule 33)"],
|
|
381
|
+
]
|
|
382
|
+
|
|
383
|
+
function auditCode(code) {
|
|
384
|
+
const violations = []
|
|
385
|
+
const warnings = []
|
|
386
|
+
|
|
387
|
+
for (const [pattern, message] of V2_PATTERNS) {
|
|
388
|
+
const re = new RegExp(pattern.source, pattern.flags)
|
|
389
|
+
let m
|
|
390
|
+
while ((m = re.exec(code)) !== null) {
|
|
391
|
+
const line = code.slice(0, m.index).split('\n').length
|
|
392
|
+
violations.push({ line, severity: 'error', message })
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
for (const [pattern, message] of RULE_CHECKS) {
|
|
397
|
+
const re = new RegExp(pattern.source, pattern.flags || 'g')
|
|
398
|
+
let m
|
|
399
|
+
while ((m = re.exec(code)) !== null) {
|
|
400
|
+
const line = code.slice(0, m.index).split('\n').length
|
|
401
|
+
const level = message.includes('FORBIDDEN') ? 'error' : 'warning'
|
|
402
|
+
const target = level === 'error' ? violations : warnings
|
|
403
|
+
target.push({ line, severity: level, message })
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const totalIssues = violations.length + warnings.length
|
|
408
|
+
const score = Math.max(1, 10 - totalIssues)
|
|
409
|
+
|
|
410
|
+
return {
|
|
411
|
+
passed: violations.length === 0,
|
|
412
|
+
score,
|
|
413
|
+
violations,
|
|
414
|
+
warnings,
|
|
415
|
+
summary: `${violations.length} errors, ${warnings.length} warnings — compliance score: ${score}/10`
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// ---------------------------------------------------------------------------
|
|
420
|
+
// Tool definitions
|
|
421
|
+
// ---------------------------------------------------------------------------
|
|
422
|
+
|
|
40
423
|
const TOOLS = [
|
|
41
424
|
{
|
|
42
425
|
name: 'get_project_rules',
|
|
@@ -45,23 +428,479 @@ const TOOLS = [
|
|
|
45
428
|
},
|
|
46
429
|
{
|
|
47
430
|
name: 'search_symbols_docs',
|
|
48
|
-
description: 'Search the Symbols documentation knowledge base for relevant information.',
|
|
431
|
+
description: 'Search the Symbols documentation knowledge base for relevant information including CLI commands, SDK services, syntax, components, and more.',
|
|
49
432
|
inputSchema: {
|
|
50
433
|
type: 'object',
|
|
51
434
|
properties: {
|
|
52
|
-
query: { type: 'string', description: 'Natural language search query about Symbols/DOMQL' },
|
|
435
|
+
query: { type: 'string', description: 'Natural language search query about Symbols/DOMQL/CLI/SDK' },
|
|
53
436
|
max_results: { type: 'number', description: 'Maximum number of results (1-5)', default: 3 }
|
|
54
437
|
},
|
|
55
438
|
required: ['query']
|
|
56
439
|
}
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
name: 'get_cli_reference',
|
|
443
|
+
description: 'Returns the complete Symbols CLI (@symbo.ls/cli) command reference — all smbls commands, options, and workflows.',
|
|
444
|
+
inputSchema: { type: 'object', properties: {} }
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
name: 'get_sdk_reference',
|
|
448
|
+
description: 'Returns the complete Symbols SDK (@symbo.ls/sdk) API reference — all services, methods, and usage examples.',
|
|
449
|
+
inputSchema: { type: 'object', properties: {} }
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
name: 'generate_component',
|
|
453
|
+
description: 'Generate a Symbols.app DOMQL v3 component from a description with full context (rules, syntax, cookbook, default library).',
|
|
454
|
+
inputSchema: {
|
|
455
|
+
type: 'object',
|
|
456
|
+
properties: {
|
|
457
|
+
description: { type: 'string', description: 'What the component should do and look like' },
|
|
458
|
+
component_name: { type: 'string', description: 'PascalCase name for the component', default: 'MyComponent' }
|
|
459
|
+
},
|
|
460
|
+
required: ['description']
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
name: 'generate_page',
|
|
465
|
+
description: 'Generate a Symbols.app page with routing integration and full context.',
|
|
466
|
+
inputSchema: {
|
|
467
|
+
type: 'object',
|
|
468
|
+
properties: {
|
|
469
|
+
description: { type: 'string', description: 'What the page should contain and do' },
|
|
470
|
+
page_name: { type: 'string', description: 'camelCase name for the page', default: 'home' }
|
|
471
|
+
},
|
|
472
|
+
required: ['description']
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
name: 'convert_react',
|
|
477
|
+
description: 'Convert React/JSX code to Symbols.app DOMQL v3 with migration context.',
|
|
478
|
+
inputSchema: {
|
|
479
|
+
type: 'object',
|
|
480
|
+
properties: {
|
|
481
|
+
source_code: { type: 'string', description: 'The React/JSX source code to convert' }
|
|
482
|
+
},
|
|
483
|
+
required: ['source_code']
|
|
484
|
+
}
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
name: 'convert_html',
|
|
488
|
+
description: 'Convert raw HTML/CSS to Symbols.app DOMQL v3 components with full context.',
|
|
489
|
+
inputSchema: {
|
|
490
|
+
type: 'object',
|
|
491
|
+
properties: {
|
|
492
|
+
source_code: { type: 'string', description: 'The HTML/CSS source code to convert' }
|
|
493
|
+
},
|
|
494
|
+
required: ['source_code']
|
|
495
|
+
}
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
name: 'detect_environment',
|
|
499
|
+
description: 'Detect project type (local, CDN, JSON runtime, or remote server) based on project indicators.',
|
|
500
|
+
inputSchema: {
|
|
501
|
+
type: 'object',
|
|
502
|
+
properties: {
|
|
503
|
+
has_symbols_json: { type: 'boolean', default: false },
|
|
504
|
+
has_symbols_dir: { type: 'boolean', default: false },
|
|
505
|
+
has_package_json: { type: 'boolean', default: false },
|
|
506
|
+
has_cdn_import: { type: 'boolean', default: false },
|
|
507
|
+
has_iife_script: { type: 'boolean', default: false },
|
|
508
|
+
has_json_data: { type: 'boolean', default: false },
|
|
509
|
+
has_mermaid_config: { type: 'boolean', default: false },
|
|
510
|
+
file_list: { type: 'string', description: 'Comma-separated list of key files', default: '' }
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
name: 'audit_component',
|
|
516
|
+
description: 'Audit a Symbols/DOMQL component for v3 compliance — checks for v2 syntax, raw px values, hardcoded colors, direct DOM manipulation, and more. Returns violations, warnings, and a score.',
|
|
517
|
+
inputSchema: {
|
|
518
|
+
type: 'object',
|
|
519
|
+
properties: {
|
|
520
|
+
component_code: { type: 'string', description: 'The JavaScript component code to audit' }
|
|
521
|
+
},
|
|
522
|
+
required: ['component_code']
|
|
523
|
+
}
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
name: 'convert_to_json',
|
|
527
|
+
description: 'Convert DOMQL v3 JavaScript source code to platform JSON format. Parses export statements, stringifies functions, outputs JSON ready for save_to_project.',
|
|
528
|
+
inputSchema: {
|
|
529
|
+
type: 'object',
|
|
530
|
+
properties: {
|
|
531
|
+
source_code: { type: 'string', description: 'JavaScript source code with export const/default statements' },
|
|
532
|
+
section: { type: 'string', description: 'Target section: components, pages, functions, snippets, designSystem, state', default: 'components' }
|
|
533
|
+
},
|
|
534
|
+
required: ['source_code']
|
|
535
|
+
}
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
name: 'login',
|
|
539
|
+
description: 'Log in to the Symbols platform and get an access token for project operations.',
|
|
540
|
+
inputSchema: {
|
|
541
|
+
type: 'object',
|
|
542
|
+
properties: {
|
|
543
|
+
email: { type: 'string', description: 'Symbols account email address' },
|
|
544
|
+
password: { type: 'string', description: 'Symbols account password' }
|
|
545
|
+
},
|
|
546
|
+
required: ['email', 'password']
|
|
547
|
+
}
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
name: 'list_projects',
|
|
551
|
+
description: 'List the user\'s Symbols projects (names, keys, IDs) to choose from.',
|
|
552
|
+
inputSchema: {
|
|
553
|
+
type: 'object',
|
|
554
|
+
properties: {
|
|
555
|
+
token: { type: 'string', description: 'JWT access token from login' },
|
|
556
|
+
api_key: { type: 'string', description: 'API key (sk_live_...)' }
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
name: 'create_project',
|
|
562
|
+
description: 'Create a new Symbols project on the platform.',
|
|
563
|
+
inputSchema: {
|
|
564
|
+
type: 'object',
|
|
565
|
+
properties: {
|
|
566
|
+
name: { type: 'string', description: 'Project display name' },
|
|
567
|
+
key: { type: 'string', description: 'Project key (pr_xxxx format). Auto-generated if empty' },
|
|
568
|
+
token: { type: 'string', description: 'JWT access token from login' },
|
|
569
|
+
api_key: { type: 'string', description: 'API key (sk_live_...)' },
|
|
570
|
+
visibility: { type: 'string', description: 'private, public, or password-protected', default: 'private' }
|
|
571
|
+
},
|
|
572
|
+
required: ['name']
|
|
573
|
+
}
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
name: 'get_project',
|
|
577
|
+
description: 'Get a project\'s current data (components, pages, design system, state).',
|
|
578
|
+
inputSchema: {
|
|
579
|
+
type: 'object',
|
|
580
|
+
properties: {
|
|
581
|
+
project: { type: 'string', description: 'Project key (pr_xxxx) or project ID' },
|
|
582
|
+
token: { type: 'string', description: 'JWT access token from login' },
|
|
583
|
+
api_key: { type: 'string', description: 'API key (sk_live_...)' },
|
|
584
|
+
branch: { type: 'string', description: 'Branch to read from', default: 'main' }
|
|
585
|
+
},
|
|
586
|
+
required: ['project']
|
|
587
|
+
}
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
name: 'save_to_project',
|
|
591
|
+
description: 'Save components/pages/data to a Symbols project. Creates a new version with change tuples, granular changes, orders, and auto-generated schema entries (mirrors CLI push pipeline).',
|
|
592
|
+
inputSchema: {
|
|
593
|
+
type: 'object',
|
|
594
|
+
properties: {
|
|
595
|
+
project: { type: 'string', description: 'Project key (pr_xxxx) or project ID' },
|
|
596
|
+
changes: { type: 'string', description: 'JSON string with project data: { components: {...}, pages: {...}, designSystem: {...}, state: {...}, functions: {...} }' },
|
|
597
|
+
token: { type: 'string', description: 'JWT access token from login' },
|
|
598
|
+
api_key: { type: 'string', description: 'API key (sk_live_...)' },
|
|
599
|
+
message: { type: 'string', description: 'Version commit message' },
|
|
600
|
+
branch: { type: 'string', description: 'Branch to save to', default: 'main' }
|
|
601
|
+
},
|
|
602
|
+
required: ['project', 'changes']
|
|
603
|
+
}
|
|
604
|
+
},
|
|
605
|
+
{
|
|
606
|
+
name: 'publish',
|
|
607
|
+
description: 'Publish a version of a Symbols project. Makes the specified version (or latest) the published/live version.',
|
|
608
|
+
inputSchema: {
|
|
609
|
+
type: 'object',
|
|
610
|
+
properties: {
|
|
611
|
+
project: { type: 'string', description: 'Project key (pr_xxxx) or project ID' },
|
|
612
|
+
token: { type: 'string', description: 'JWT access token from login' },
|
|
613
|
+
api_key: { type: 'string', description: 'API key (sk_live_...)' },
|
|
614
|
+
version: { type: 'string', description: 'Version string or ID. Empty for latest' },
|
|
615
|
+
branch: { type: 'string', description: 'Branch to publish from', default: 'main' }
|
|
616
|
+
},
|
|
617
|
+
required: ['project']
|
|
618
|
+
}
|
|
619
|
+
},
|
|
620
|
+
{
|
|
621
|
+
name: 'push',
|
|
622
|
+
description: 'Deploy a Symbols project to an environment (production, staging, dev).',
|
|
623
|
+
inputSchema: {
|
|
624
|
+
type: 'object',
|
|
625
|
+
properties: {
|
|
626
|
+
project: { type: 'string', description: 'Project key (pr_xxxx) or project ID' },
|
|
627
|
+
token: { type: 'string', description: 'JWT access token from login' },
|
|
628
|
+
api_key: { type: 'string', description: 'API key (sk_live_...)' },
|
|
629
|
+
environment: { type: 'string', description: 'Target environment key', default: 'production' },
|
|
630
|
+
mode: { type: 'string', description: 'Deploy mode: latest, published, version, or branch', default: 'published' },
|
|
631
|
+
version: { type: 'string', description: 'Version string when mode is "version"' },
|
|
632
|
+
branch: { type: 'string', description: 'Branch when mode is "latest" or "branch"', default: 'main' }
|
|
633
|
+
},
|
|
634
|
+
required: ['project']
|
|
635
|
+
}
|
|
57
636
|
}
|
|
58
637
|
]
|
|
59
638
|
|
|
639
|
+
// ---------------------------------------------------------------------------
|
|
640
|
+
// Tool handlers
|
|
641
|
+
// ---------------------------------------------------------------------------
|
|
642
|
+
|
|
643
|
+
async function handleTool(name, args) {
|
|
644
|
+
// Documentation tools (sync)
|
|
645
|
+
if (name === 'get_project_rules') return loadAgentInstructions()
|
|
646
|
+
if (name === 'search_symbols_docs') return searchDocs(args.query, args.max_results || 3)
|
|
647
|
+
if (name === 'get_cli_reference') return readSkill('CLI.md')
|
|
648
|
+
if (name === 'get_sdk_reference') return readSkill('SDK.md')
|
|
649
|
+
|
|
650
|
+
// Helper to read and concatenate multiple skill files
|
|
651
|
+
function readSkills(...filenames) {
|
|
652
|
+
return filenames.map(f => readSkill(f)).filter(c => !c.startsWith('Skill ')).join('\n\n---\n\n')
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// generate_component
|
|
656
|
+
if (name === 'generate_component') {
|
|
657
|
+
const componentName = args.component_name || 'MyComponent'
|
|
658
|
+
const context = readSkills('RULES.md', 'COMMON_MISTAKES.md', 'COMPONENTS.md', 'SYNTAX.md', 'COOKBOOK.md', 'DEFAULT_LIBRARY.md')
|
|
659
|
+
return `# Generate Component: ${componentName}\n\n## Description\n${args.description}\n\n## Requirements\n- Named export: \`export const ${componentName} = { ... }\`\n- DOMQL v3 syntax only (extends, childExtends, flattened props, onX events)\n- **MANDATORY: ALL values MUST use design system tokens** — spacing (A, B, C, D), colors (primary, surface, white, gray.5), typography (fontSize: 'B'). ZERO px values, ZERO hex colors, ZERO rgb/hsl.\n- NO imports between files — PascalCase keys auto-extend registered components\n- Include responsive breakpoints where appropriate (@tabletS, @mobileL)\n- Use the default library components (Button, Avatar, Icon, Field, etc.) via extends\n- Use Icon component for SVGs — store icons in designSystem/icons\n- NO direct DOM manipulation — all structure via DOMQL declarative syntax\n- Follow modern UI/UX: visual hierarchy, confident typography, minimal cognitive load\n\n## Context — Rules, Syntax & Examples\n\n${context}`
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// generate_page
|
|
663
|
+
if (name === 'generate_page') {
|
|
664
|
+
const pageName = args.page_name || 'home'
|
|
665
|
+
const context = readSkills('RULES.md', 'COMMON_MISTAKES.md', 'PROJECT_STRUCTURE.md', 'PATTERNS.md', 'SNIPPETS.md', 'DEFAULT_LIBRARY.md', 'COMPONENTS.md')
|
|
666
|
+
return `# Generate Page: ${pageName}\n\n## Description\n${args.description}\n\n## Requirements\n- Export as: \`export const ${pageName} = { ... }\`\n- Page is a plain object composing components\n- Add to pages/index.js route map: \`'/${pageName}': ${pageName}\`\n- Use components by PascalCase key (Header, Footer, Hero, etc.)\n- **MANDATORY: ALL values MUST use design system tokens** — spacing (A, B, C, D), colors (primary, surface, white, gray.5), typography (fontSize: 'B'). ZERO px values, ZERO hex colors, ZERO rgb/hsl.\n- Use Icon component for SVGs — store icons in designSystem/icons\n- NO direct DOM manipulation — all structure via DOMQL declarative syntax\n- Include responsive layout adjustments\n\n## Context — Rules, Structure, Patterns & Snippets\n\n${context}`
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// convert_react
|
|
670
|
+
if (name === 'convert_react') {
|
|
671
|
+
const context = readSkills('RULES.md', 'MIGRATION.md', 'SYNTAX.md', 'COMPONENTS.md', 'LEARNINGS.md')
|
|
672
|
+
return `# Convert React → Symbols DOMQL v3\n\n## Source Code to Convert\n\`\`\`jsx\n${args.source_code}\n\`\`\`\n\n## Conversion Rules\n- Function/class components → plain object exports\n- JSX → nested object children (PascalCase keys auto-extend)\n- import/export between files → REMOVE (reference by key name)\n- useState → state: { key: val } + s.update({ key: newVal })\n- useEffect → onRender (mount), onStateUpdate (deps)\n- props → flattened directly on component (no props wrapper)\n- onClick={handler} → onClick: (event, el, state) => {}\n- className → use design tokens and theme directly\n- map() → children: (el, s) => s.items, childExtends, childProps\n- conditional rendering → if: (el, s) => boolean\n- CSS modules/styled → CSS-in-props with design tokens\n- React.Fragment → not needed, just nest children\n\n## Context — Migration Guide, Syntax & Rules\n\n${context}`
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// convert_html
|
|
676
|
+
if (name === 'convert_html') {
|
|
677
|
+
const context = readSkills('RULES.md', 'SYNTAX.md', 'COMPONENTS.md', 'DESIGN_SYSTEM.md', 'SNIPPETS.md', 'LEARNINGS.md')
|
|
678
|
+
return `# Convert HTML → Symbols DOMQL v3\n\n## Source Code to Convert\n\`\`\`html\n${args.source_code}\n\`\`\`\n\n## Conversion Rules\n- <div> → Box, Flex, or Grid (based on layout purpose)\n- <span>, <p>, <h1>-<h6> → Text, P, H with tag property\n- <a> → Link (has built-in SPA router)\n- <button> → Button (has icon/text support)\n- <input> → Input, Radio, Checkbox (based on type)\n- <img> → Img\n- <form> → Form (extends Box with tag: 'form')\n- <ul>/<ol> + <li> → children array with childExtends\n- CSS classes → flatten as CSS-in-props on the component\n- CSS px values → design tokens (16px → 'A', 26px → 'B', 42px → 'C')\n- CSS colors → theme color tokens\n- media queries → @tabletS, @mobileL, @screenS breakpoints\n- id/class attributes → not needed (use key names and themes)\n- inline styles → flatten as component properties\n- <style> blocks → distribute to component-level properties\n\n## Context — Syntax, Components & Design System\n\n${context}`
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// detect_environment
|
|
682
|
+
if (name === 'detect_environment') {
|
|
683
|
+
let envType = 'unknown', confidence = 'low'
|
|
684
|
+
if (args.has_mermaid_config) { envType = 'remote_server'; confidence = 'high' }
|
|
685
|
+
else if (args.has_json_data) { envType = 'json_runtime'; confidence = 'high' }
|
|
686
|
+
else if (args.has_symbols_json && args.has_symbols_dir) { envType = 'local_project'; confidence = 'high' }
|
|
687
|
+
else if (args.has_symbols_dir || (args.has_package_json && args.has_symbols_json)) { envType = 'local_project'; confidence = 'medium' }
|
|
688
|
+
else if (args.has_cdn_import || args.has_iife_script) { envType = 'cdn'; confidence = 'high' }
|
|
689
|
+
else if (args.has_package_json) { envType = 'local_project'; confidence = 'low' }
|
|
690
|
+
else if (args.file_list) {
|
|
691
|
+
const files = args.file_list.toLowerCase()
|
|
692
|
+
if (files.includes('index.html') && !files.includes('package.json') && !files.includes('symbols.json')) { envType = 'cdn'; confidence = 'medium' }
|
|
693
|
+
}
|
|
694
|
+
const guide = readSkill('RUNNING_APPS.md')
|
|
695
|
+
return `# Environment Detection\n\n**Detected: ${envType}** (confidence: ${confidence})\n\n${guide}`
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// audit_component (sync)
|
|
699
|
+
if (name === 'audit_component') {
|
|
700
|
+
const result = auditCode(args.component_code)
|
|
701
|
+
const rulesContext = readSkill('AUDIT.md')
|
|
702
|
+
let output = `# Audit Report\n\n## Summary\n${result.summary}\nPassed: ${result.passed ? 'Yes' : 'No'}\n\n## Violations (Errors)\n`
|
|
703
|
+
if (result.violations.length) {
|
|
704
|
+
for (const v of result.violations) output += `- **Line ${v.line}**: ${v.message}\n`
|
|
705
|
+
} else {
|
|
706
|
+
output += 'No violations found.\n'
|
|
707
|
+
}
|
|
708
|
+
output += '\n## Warnings\n'
|
|
709
|
+
if (result.warnings.length) {
|
|
710
|
+
for (const w of result.warnings) output += `- **Line ${w.line}**: ${w.message}\n`
|
|
711
|
+
} else {
|
|
712
|
+
output += 'No warnings.\n'
|
|
713
|
+
}
|
|
714
|
+
if (result.violations.length) {
|
|
715
|
+
output += '\n## MANDATORY ACTION\n\n**Every violation above MUST be fixed. There is NO concept of "known debt", "accepted violations", or "95% fixed" in Symbols. ALL violations must reach 100% resolution. Do NOT label any violation as "known debt" or defer it. Rewrite the code using proper DOMQL syntax.**\n\n'
|
|
716
|
+
}
|
|
717
|
+
output += `\n## Detailed Rules Reference\n\n${rulesContext}`
|
|
718
|
+
return output
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// convert_to_json (sync)
|
|
722
|
+
if (name === 'convert_to_json') {
|
|
723
|
+
const section = args.section || 'components'
|
|
724
|
+
const parsed = parseJsToJson(args.source_code)
|
|
725
|
+
if (!Object.keys(parsed).length) {
|
|
726
|
+
return 'Could not parse any exports from the source code. Make sure it contains `export const Name = { ... }` or `export default { ... }`.'
|
|
727
|
+
}
|
|
728
|
+
const result = {}
|
|
729
|
+
for (const [exportName, rawValue] of Object.entries(parsed)) {
|
|
730
|
+
const converted = typeof rawValue === 'string' ? jsObjToJson(rawValue) : rawValue
|
|
731
|
+
if (exportName === '__default__') {
|
|
732
|
+
if (['designSystem', 'state', 'dependencies', 'config'].includes(section)) {
|
|
733
|
+
result[section] = converted
|
|
734
|
+
} else {
|
|
735
|
+
if (!result[section]) result[section] = {}
|
|
736
|
+
result[section]['default'] = converted
|
|
737
|
+
}
|
|
738
|
+
} else {
|
|
739
|
+
if (!result[section]) result[section] = {}
|
|
740
|
+
result[section][exportName] = converted
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
const output = JSON.stringify(result, null, 2)
|
|
744
|
+
const sections = Object.keys(result)
|
|
745
|
+
const items = []
|
|
746
|
+
for (const sec of sections) {
|
|
747
|
+
if (typeof result[sec] === 'object' && result[sec]) items.push(...Object.keys(result[sec]))
|
|
748
|
+
}
|
|
749
|
+
return `# Converted to Platform JSON\n\n**Section:** ${sections.join(', ')}\n**Items:** ${items.join(', ') || 'default export'}\n\n\`\`\`json\n${output}\n\`\`\`\n\nThis JSON is ready to use with \`save_to_project\`. Pass the JSON object above as the \`changes\` parameter.\n\n**Full flow:**\n1. \`convert_to_json\` (done) → structured JSON\n2. \`save_to_project\` → push to platform (creates new version)\n3. \`publish\` → make version live\n4. \`push\` → deploy to environment (production/staging/dev)`
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// login
|
|
753
|
+
if (name === 'login') {
|
|
754
|
+
const result = await apiRequest('POST', '/core/auth/login', {
|
|
755
|
+
body: { email: args.email, password: args.password }
|
|
756
|
+
})
|
|
757
|
+
if (result.success) {
|
|
758
|
+
const { tokens = {}, user = {} } = result.data || {}
|
|
759
|
+
const token = tokens.accessToken || ''
|
|
760
|
+
return `Logged in as ${user.name || user.email || 'unknown'}.\nToken: ${token}\nExpires: ${tokens.accessTokenExp?.expiresAt || 'unknown'}\n\nUse this token with project, save, publish and push tools.`
|
|
761
|
+
}
|
|
762
|
+
return `Login failed: ${result.error || result.message || 'Unknown error'}`
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// list_projects
|
|
766
|
+
if (name === 'list_projects') {
|
|
767
|
+
const authErr = requireAuth(args.token, args.api_key)
|
|
768
|
+
if (authErr) return authErr
|
|
769
|
+
const result = await apiRequest('GET', '/core/projects', { token: args.token, apiKey: args.api_key })
|
|
770
|
+
if (result.success) {
|
|
771
|
+
const projects = result.data || []
|
|
772
|
+
if (!projects.length) return 'No projects found. Use `create_project` to create one.'
|
|
773
|
+
const lines = ['# Your Projects\n']
|
|
774
|
+
for (const p of projects) {
|
|
775
|
+
lines.push(`- **${p.name || 'Untitled'}** — key: \`${p.key || '—'}\`, id: \`${p._id || ''}\`, visibility: ${p.visibility || 'private'}`)
|
|
776
|
+
}
|
|
777
|
+
return lines.join('\n')
|
|
778
|
+
}
|
|
779
|
+
return `Failed to list projects: ${result.error || 'Unknown error'}`
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// create_project
|
|
783
|
+
if (name === 'create_project') {
|
|
784
|
+
const authErr = requireAuth(args.token, args.api_key)
|
|
785
|
+
if (authErr) return authErr
|
|
786
|
+
const body = { name: args.name, visibility: args.visibility || 'private', language: 'javascript' }
|
|
787
|
+
if (args.key) body.key = args.key
|
|
788
|
+
const result = await apiRequest('POST', '/core/projects', { token: args.token, apiKey: args.api_key, body })
|
|
789
|
+
if (result.success) {
|
|
790
|
+
const d = result.data || {}
|
|
791
|
+
return `Project created successfully.\nName: ${d.name || args.name}\nKey: \`${d.key || 'unknown'}\`\nID: \`${d._id || 'unknown'}\`\n\nUse this project key/ID with \`save_to_project\` to push your components.`
|
|
792
|
+
}
|
|
793
|
+
return `Create failed: ${result.error || 'Unknown error'}\n${result.message || ''}`
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// get_project
|
|
797
|
+
if (name === 'get_project') {
|
|
798
|
+
const authErr = requireAuth(args.token, args.api_key)
|
|
799
|
+
if (authErr) return authErr
|
|
800
|
+
const branch = args.branch || 'main'
|
|
801
|
+
const project = args.project
|
|
802
|
+
const isKey = project.startsWith('pr_') || !/^[0-9a-f]+$/.test(project)
|
|
803
|
+
const urlPath = isKey
|
|
804
|
+
? `/core/projects/key/${project}/data?branch=${branch}&version=latest`
|
|
805
|
+
: `/core/projects/${project}/data?branch=${branch}&version=latest`
|
|
806
|
+
const result = await apiRequest('GET', urlPath, { token: args.token, apiKey: args.api_key })
|
|
807
|
+
if (result.success) {
|
|
808
|
+
const data = result.data || {}
|
|
809
|
+
const components = data.components || {}
|
|
810
|
+
const pages = data.pages || {}
|
|
811
|
+
const ds = data.designSystem || {}
|
|
812
|
+
const state = data.state || {}
|
|
813
|
+
const functions = data.functions || {}
|
|
814
|
+
const lines = [`# Project Data (branch: ${branch})\n`]
|
|
815
|
+
lines.push(`**Components (${Object.keys(components).length}):** ${Object.keys(components).slice(0, 20).join(', ') || 'none'}`)
|
|
816
|
+
lines.push(`**Pages (${Object.keys(pages).length}):** ${Object.keys(pages).slice(0, 20).join(', ') || 'none'}`)
|
|
817
|
+
lines.push(`**Design System keys:** ${Object.keys(ds).slice(0, 15).join(', ') || 'none'}`)
|
|
818
|
+
lines.push(`**State keys:** ${Object.keys(state).slice(0, 15).join(', ') || 'none'}`)
|
|
819
|
+
lines.push(`**Functions (${Object.keys(functions).length}):** ${Object.keys(functions).slice(0, 15).join(', ') || 'none'}`)
|
|
820
|
+
lines.push(`\n---\n\nFull data:\n\`\`\`json\n${JSON.stringify(data, null, 2).slice(0, 8000)}\n\`\`\``)
|
|
821
|
+
return lines.join('\n')
|
|
822
|
+
}
|
|
823
|
+
return `Failed to get project data: ${result.error || 'Unknown error'}`
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// save_to_project
|
|
827
|
+
if (name === 'save_to_project') {
|
|
828
|
+
const authErr = requireAuth(args.token, args.api_key)
|
|
829
|
+
if (authErr) return authErr
|
|
830
|
+
let changesData
|
|
831
|
+
try { changesData = JSON.parse(args.changes) }
|
|
832
|
+
catch (e) { return `Invalid JSON in changes: ${e.message}` }
|
|
833
|
+
if (typeof changesData !== 'object' || changesData === null) {
|
|
834
|
+
return 'Changes must be a JSON object with keys like components, pages, designSystem, state, functions.'
|
|
835
|
+
}
|
|
836
|
+
const { id: projectId, error: resolveErr } = await resolveProjectId(args.project, args.token, args.api_key)
|
|
837
|
+
if (resolveErr) return resolveErr
|
|
838
|
+
const { changes, granular: granularChanges, orders } = buildChangesAndSchema(changesData)
|
|
839
|
+
if (!changes.length) return 'No valid changes found. Include at least one data section.'
|
|
840
|
+
const branch = args.branch || 'main'
|
|
841
|
+
const body = {
|
|
842
|
+
changes, granularChanges, orders,
|
|
843
|
+
message: args.message || 'Updated via Symbols MCP',
|
|
844
|
+
branch, type: 'patch'
|
|
845
|
+
}
|
|
846
|
+
const result = await apiRequest('POST', `/core/projects/${projectId}/changes`, { token: args.token, apiKey: args.api_key, body })
|
|
847
|
+
if (result.success) {
|
|
848
|
+
const d = result.data || {}
|
|
849
|
+
const version = d.value || d.version || d.id || 'unknown'
|
|
850
|
+
const savedSections = Object.keys(changesData)
|
|
851
|
+
return `Saved to project \`${args.project}\` successfully.\nVersion: ${version}\nBranch: ${branch}\nSections updated: ${savedSections.join(', ')}\n\nUse \`publish\` to make this version live, or \`push\` to deploy to an environment.`
|
|
852
|
+
}
|
|
853
|
+
return `Save failed: ${result.error || 'Unknown error'}\n${result.message || ''}`
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// publish
|
|
857
|
+
if (name === 'publish') {
|
|
858
|
+
const authErr = requireAuth(args.token, args.api_key)
|
|
859
|
+
if (authErr) return authErr
|
|
860
|
+
const { id: projectId, error: resolveErr } = await resolveProjectId(args.project, args.token, args.api_key)
|
|
861
|
+
if (resolveErr) return resolveErr
|
|
862
|
+
const body = { branch: args.branch || 'main' }
|
|
863
|
+
if (args.version) body.version = args.version
|
|
864
|
+
const result = await apiRequest('POST', `/core/projects/${projectId}/publish`, { token: args.token, apiKey: args.api_key, body })
|
|
865
|
+
if (result.success) {
|
|
866
|
+
const d = result.data || {}
|
|
867
|
+
return `Published successfully.\nVersion: ${d.value || d.id || 'unknown'}`
|
|
868
|
+
}
|
|
869
|
+
return `Publish failed: ${result.error || 'Unknown error'}\n${result.message || ''}`
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// push
|
|
873
|
+
if (name === 'push') {
|
|
874
|
+
const authErr = requireAuth(args.token, args.api_key)
|
|
875
|
+
if (authErr) return authErr
|
|
876
|
+
const { id: projectId, error: resolveErr } = await resolveProjectId(args.project, args.token, args.api_key)
|
|
877
|
+
if (resolveErr) return resolveErr
|
|
878
|
+
const environment = args.environment || 'production'
|
|
879
|
+
const mode = args.mode || 'published'
|
|
880
|
+
const branch = args.branch || 'main'
|
|
881
|
+
const body = { mode, branch }
|
|
882
|
+
if (args.version) body.version = args.version
|
|
883
|
+
const result = await apiRequest('POST', `/core/projects/${projectId}/environments/${environment}/publish`, { token: args.token, apiKey: args.api_key, body })
|
|
884
|
+
if (result.success) {
|
|
885
|
+
const d = result.data || {}
|
|
886
|
+
const config = d.config || {}
|
|
887
|
+
return `Pushed to ${d.key || environment} successfully.\nMode: ${config.mode || mode}\nVersion: ${config.version || 'latest'}\nBranch: ${config.branch || branch}`
|
|
888
|
+
}
|
|
889
|
+
return `Push failed: ${result.error || 'Unknown error'}\n${result.message || ''}`
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
throw new Error(`Unknown tool: ${name}`)
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// ---------------------------------------------------------------------------
|
|
896
|
+
// JSON-RPC server
|
|
897
|
+
// ---------------------------------------------------------------------------
|
|
898
|
+
|
|
60
899
|
function send(obj) {
|
|
61
900
|
process.stdout.write(JSON.stringify(obj) + '\n')
|
|
62
901
|
}
|
|
63
902
|
|
|
64
|
-
function handle(req) {
|
|
903
|
+
async function handle(req) {
|
|
65
904
|
if (!req.method) return
|
|
66
905
|
if (req.method === 'initialize') {
|
|
67
906
|
return send({
|
|
@@ -69,7 +908,7 @@ function handle(req) {
|
|
|
69
908
|
result: {
|
|
70
909
|
protocolVersion: req.params?.protocolVersion ?? '2025-03-26',
|
|
71
910
|
capabilities: { tools: {} },
|
|
72
|
-
serverInfo: { name: 'Symbols MCP', version: '1.0.
|
|
911
|
+
serverInfo: { name: 'Symbols MCP', version: '1.0.15' }
|
|
73
912
|
}
|
|
74
913
|
})
|
|
75
914
|
}
|
|
@@ -79,10 +918,7 @@ function handle(req) {
|
|
|
79
918
|
if (req.method === 'tools/call') {
|
|
80
919
|
const { name, arguments: args = {} } = req.params
|
|
81
920
|
try {
|
|
82
|
-
|
|
83
|
-
if (name === 'get_project_rules') text = loadAgentInstructions()
|
|
84
|
-
else if (name === 'search_symbols_docs') text = searchDocs(args.query, args.max_results || 3)
|
|
85
|
-
else throw new Error(`Unknown tool: ${name}`)
|
|
921
|
+
const text = await handleTool(name, args)
|
|
86
922
|
return send({ jsonrpc: '2.0', id: req.id, result: { content: [{ type: 'text', text }] } })
|
|
87
923
|
} catch (e) {
|
|
88
924
|
return send({ jsonrpc: '2.0', id: req.id, result: { content: [{ type: 'text', text: e.message }], isError: true } })
|
|
@@ -96,6 +932,7 @@ function handle(req) {
|
|
|
96
932
|
const rl = readline.createInterface({ input: process.stdin, terminal: false })
|
|
97
933
|
rl.on('line', line => {
|
|
98
934
|
if (!line.trim()) return
|
|
99
|
-
try { handle(JSON.parse(line))
|
|
935
|
+
try { handle(JSON.parse(line)).catch(e => process.stderr.write(`Handler error: ${e.message}\n`)) }
|
|
936
|
+
catch (e) { process.stderr.write(`Parse error: ${e.message}\n`) }
|
|
100
937
|
})
|
|
101
938
|
rl.on('close', () => process.exit(0))
|