@mindfiredigital/ignix-lite-mcp 1.2.0 → 1.3.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.
Files changed (90) hide show
  1. package/README.md +108 -0
  2. package/dist/server.js +187 -1954
  3. package/dist/server.js.map +1 -1
  4. package/dist/utils/check-api.js +2 -0
  5. package/dist/utils/check-api.js.map +1 -1
  6. package/package.json +14 -6
  7. package/.turbo/turbo-build.log +0 -25
  8. package/CHANGELOG.md +0 -7
  9. package/dist/manifests/accordion.json +0 -61
  10. package/dist/manifests/alert.json +0 -69
  11. package/dist/manifests/avatar.json +0 -75
  12. package/dist/manifests/badge.json +0 -74
  13. package/dist/manifests/breadcrumb.json +0 -87
  14. package/dist/manifests/button.json +0 -85
  15. package/dist/manifests/card.json +0 -91
  16. package/dist/manifests/checkbox.json +0 -122
  17. package/dist/manifests/codeblock.json +0 -63
  18. package/dist/manifests/combobox.json +0 -33
  19. package/dist/manifests/dialog.json +0 -64
  20. package/dist/manifests/divider.json +0 -47
  21. package/dist/manifests/dropdown.json +0 -105
  22. package/dist/manifests/form.json +0 -81
  23. package/dist/manifests/grid.json +0 -143
  24. package/dist/manifests/input.json +0 -99
  25. package/dist/manifests/meter.json +0 -103
  26. package/dist/manifests/navigation.json +0 -70
  27. package/dist/manifests/progress.json +0 -88
  28. package/dist/manifests/radio.json +0 -121
  29. package/dist/manifests/select.json +0 -109
  30. package/dist/manifests/skeleton.json +0 -101
  31. package/dist/manifests/tab.json +0 -88
  32. package/dist/manifests/table.json +0 -92
  33. package/dist/manifests/textarea.json +0 -117
  34. package/dist/manifests/toast.json +0 -157
  35. package/dist/manifests/tooltip.json +0 -115
  36. package/dist/vector-index.json +0 -14015
  37. package/src/context/api-context.ts +0 -14
  38. package/src/global.d.ts +0 -15
  39. package/src/manifests/accordion.json +0 -61
  40. package/src/manifests/alert.json +0 -69
  41. package/src/manifests/avatar.json +0 -75
  42. package/src/manifests/badge.json +0 -74
  43. package/src/manifests/breadcrumb.json +0 -87
  44. package/src/manifests/button.json +0 -85
  45. package/src/manifests/card.json +0 -91
  46. package/src/manifests/checkbox.json +0 -122
  47. package/src/manifests/codeblock.json +0 -63
  48. package/src/manifests/combobox.json +0 -33
  49. package/src/manifests/dialog.json +0 -64
  50. package/src/manifests/divider.json +0 -47
  51. package/src/manifests/dropdown.json +0 -105
  52. package/src/manifests/form.json +0 -81
  53. package/src/manifests/grid.json +0 -143
  54. package/src/manifests/index.ts +0 -45
  55. package/src/manifests/input.json +0 -99
  56. package/src/manifests/meter.json +0 -103
  57. package/src/manifests/navigation.json +0 -70
  58. package/src/manifests/progress.json +0 -88
  59. package/src/manifests/radio.json +0 -121
  60. package/src/manifests/select.json +0 -109
  61. package/src/manifests/skeleton.json +0 -101
  62. package/src/manifests/tab.json +0 -88
  63. package/src/manifests/table.json +0 -92
  64. package/src/manifests/textarea.json +0 -117
  65. package/src/manifests/toast.json +0 -157
  66. package/src/manifests/tooltip.json +0 -115
  67. package/src/server.ts +0 -201
  68. package/src/tools/build-index.ts +0 -55
  69. package/src/tools/check-a11y.ts +0 -106
  70. package/src/tools/embedder.ts +0 -18
  71. package/src/tools/generate-theme.ts +0 -42
  72. package/src/tools/get-emmet.ts +0 -64
  73. package/src/tools/get-manifests.ts +0 -55
  74. package/src/tools/intent-engine.ts +0 -197
  75. package/src/tools/list-components.ts +0 -20
  76. package/src/tools/search-index.ts +0 -66
  77. package/src/tools/theme-palette.ts +0 -65
  78. package/src/tools/theme-tokens.ts +0 -176
  79. package/src/tools/validator.ts +0 -367
  80. package/src/types.ts +0 -63
  81. package/src/utils/a11y-rules.ts +0 -873
  82. package/src/utils/a11y-types.ts +0 -15
  83. package/src/utils/check-api.ts +0 -13
  84. package/src/utils/cosine.ts +0 -15
  85. package/src/utils/emmet-helpers.ts +0 -171
  86. package/src/utils/intent-helpers.ts +0 -66
  87. package/src/utils/intent-parser.ts +0 -186
  88. package/src/utils/tokenizer.ts +0 -7
  89. package/tsconfig.json +0 -14
  90. package/tsup.config.ts +0 -13
@@ -1,15 +0,0 @@
1
- export type IssueType = 'error' | 'warning'
2
-
3
- export interface A11yIssue {
4
- type: IssueType
5
- rule: string
6
- element: string
7
- message: string
8
- fix: string
9
- confidence?: number
10
- }
11
-
12
- export interface RuleResult {
13
- ruleName: string
14
- issues: A11yIssue[]
15
- }
@@ -1,13 +0,0 @@
1
- import { readFileSync } from 'fs'
2
- import { encoding_for_model } from 'tiktoken'
3
- import path from 'path'
4
-
5
- const encoder = encoding_for_model('gpt-4')
6
-
7
- const files = ['../../api-full.txt']
8
-
9
- for (const file of files) {
10
- const text = readFileSync(path.resolve(file), 'utf-8')
11
-
12
- console.log(file, '=', encoder.encode(text).length, 'tokens')
13
- }
@@ -1,15 +0,0 @@
1
- export function cosineSimilarity(a: number[], b: number[]): number {
2
- let dot = 0
3
- let magA = 0
4
- let magB = 0
5
-
6
- for (let i = 0; i < a.length; i++) {
7
- dot += a[i] * b[i]
8
- magA += a[i] * a[i]
9
- magB += b[i] * b[i]
10
- }
11
-
12
- if (magA === 0 || magB === 0) return 0
13
-
14
- return dot / (Math.sqrt(magA) * Math.sqrt(magB))
15
- }
@@ -1,171 +0,0 @@
1
- import emmet from 'emmet'
2
-
3
- // Expand Emmet shorthand to full HTML using the emmet npm package
4
- export function expandEmmet(emmetStr: string): string {
5
- try {
6
- return emmet(emmetStr)
7
- } catch {
8
- return `<!-- expand: ${emmetStr} -->`
9
- }
10
- }
11
-
12
- // Maps HTML element names / web-component tag names → ignix-lite component names
13
- export const ELEMENT_TO_COMPONENT: Record<string, string> = {
14
- // Native elements
15
- button: 'button',
16
- input: 'input',
17
- textarea: 'textarea',
18
- select: 'select',
19
- form: 'form',
20
- dialog: 'dialog',
21
- details: 'accordion',
22
- summary: 'accordion',
23
- progress: 'progress',
24
- meter: 'meter',
25
- aside: 'alert',
26
- mark: 'badge',
27
- img: 'avatar',
28
- article: 'card',
29
- nav: 'navigation',
30
- table: 'table',
31
- pre: 'codeblock',
32
- code: 'codeblock',
33
- hr: 'divider',
34
- label: 'input', // label wraps input/checkbox/radio
35
- // Web components
36
- 'ix-tabs': 'tab',
37
- 'ix-dropdown': 'dropdown',
38
- 'ix-combobox': 'combobox',
39
- 'ix-tooltip': 'tooltip',
40
- 'ix-toast': 'toast'
41
- }
42
-
43
- // Skeleton heuristic: span with aria-busy or data-shape
44
- export const SKELETON_PATTERN = /span\[[^\]]*(?:aria-busy|data-shape)[^\]]*\]/i
45
-
46
- /**
47
- * Given an emmet string, return the unique set of ignix-lite component names
48
- * that the pattern uses. E.g.:
49
- * "article>(img[slot=avatar])+(button{View})" → ["card", "avatar", "button"]
50
- */
51
- export function extractComponents(emmetStr: string): string[] {
52
- const found = new Set<string>()
53
-
54
- // Special case: skeleton uses span[aria-busy] or span[data-shape]
55
- if (SKELETON_PATTERN.test(emmetStr)) {
56
- found.add('skeleton')
57
- }
58
-
59
- // Special case: nav[aria-label=Breadcrumb] → breadcrumb
60
- if (/nav\[[^\]]*breadcrumb/i.test(emmetStr)) {
61
- found.add('breadcrumb')
62
- } else if (/\bnav\b/.test(emmetStr)) {
63
- found.add('navigation')
64
- }
65
-
66
- // Special case: label > input[type=checkbox] → checkbox
67
- if (/input\[[^\]]*type=checkbox/i.test(emmetStr)) {
68
- found.add('checkbox')
69
- }
70
-
71
- // Special case: label > input[type=radio] → radio
72
- if (/input\[[^\]]*type=radio/i.test(emmetStr)) {
73
- found.add('radio')
74
- }
75
-
76
- // Special case: section[data-grid] → grid (only when data-grid attr is present)
77
- if (/section\[[^\]]*data-grid/i.test(emmetStr)) {
78
- found.add('grid')
79
- }
80
-
81
- // General: extract all tag names from the emmet string
82
- const tagPattern = /(?:^|[>+(])([a-z][a-z0-9-]*)/gi
83
- let m: RegExpExecArray | null
84
- while ((m = tagPattern.exec(emmetStr)) !== null) {
85
- const tag = m[1].toLowerCase()
86
- const component = ELEMENT_TO_COMPONENT[tag]
87
- if (component) found.add(component)
88
- }
89
-
90
- return Array.from(found)
91
- }
92
-
93
- /**
94
- * Customizes standard Emmet shorthand by dynamically replacing text strings
95
- * and visual intent attributes based on the user prompt description.
96
- */
97
- export function interpolateEmmet(
98
- emmetStr: string,
99
- description: string
100
- ): string {
101
- let result = emmetStr
102
-
103
- // 1. Color/Intent overrides based on keywords in description
104
- const descLower = description.toLowerCase()
105
- if (/\b(red|danger|delete|remove|destroy)\b/.test(descLower)) {
106
- // replace any data-intent of primary/neutral/ghost with danger
107
- result = result.replace(
108
- /data-intent=(?:primary|neutral|ghost)/g,
109
- 'data-intent=danger'
110
- )
111
- } else if (/\b(green|success|work|done)\b/.test(descLower)) {
112
- result = result.replace(
113
- /data-intent=(?:primary|neutral|ghost)/g,
114
- 'data-intent=success'
115
- )
116
- } else if (/\b(yellow|orange|warning|risky)\b/.test(descLower)) {
117
- result = result.replace(
118
- /data-intent=(?:primary|neutral|ghost)/g,
119
- 'data-intent=warning'
120
- )
121
- }
122
-
123
- // 2. Extract quoted strings for label/text overrides
124
- const matches = [...description.matchAll(/['"]([^'"]+)['"]/g)]
125
- const quotedStrings = matches
126
- .map((m) => m[1].trim())
127
- .filter((s) => s.length > 0)
128
-
129
- if (quotedStrings.length > 0) {
130
- const hasButtonMention = /\b(button|says|labeled|action|click)\b/i.test(
131
- description
132
- )
133
-
134
- // We only replace the button text if:
135
- // - There are multiple quoted strings (so the last one is assumed to be the button), OR
136
- // - There is only one quoted string, but the prompt explicitly mentions a button/action
137
- const shouldReplaceButton =
138
- quotedStrings.length >= 2 ||
139
- (quotedStrings.length === 1 && hasButtonMention)
140
-
141
- if (shouldReplaceButton) {
142
- const buttonRegex = /(button[^}]*)\{([^}]+)\}/g
143
- const buttonMatches = [...result.matchAll(buttonRegex)]
144
-
145
- if (buttonMatches.length > 0) {
146
- const buttonLabel = quotedStrings[quotedStrings.length - 1]
147
- result = result.replace(buttonRegex, (match, p1) => {
148
- return `${p1}{${buttonLabel}}`
149
- })
150
- quotedStrings.pop()
151
- }
152
- }
153
-
154
- // Replace label text in order with the remaining quoted strings
155
- const labelRegex = /label\{([^}]+)\}/g
156
- let quoteIndex = 0
157
- result = result.replace(labelRegex, (match) => {
158
- if (quoteIndex < quotedStrings.length) {
159
- return `label{${quotedStrings[quoteIndex++]}}`
160
- }
161
- return match
162
- })
163
-
164
- // Auto-adapt input type: if label contains "username", change type=email to type=text
165
- if (/label\{username\}/i.test(result)) {
166
- result = result.replace(/type=email/g, 'type=text')
167
- }
168
- }
169
-
170
- return result
171
- }
@@ -1,66 +0,0 @@
1
- export const STOP_WORDS = new Set([
2
- 'with',
3
- 'for',
4
- 'and',
5
- 'the',
6
- 'you',
7
- 'can',
8
- 'from',
9
- 'this',
10
- 'that',
11
- 'your',
12
- 'want',
13
- 'need',
14
- 'show',
15
- 'give',
16
- 'nice',
17
- 'page',
18
- 'here',
19
- 'please',
20
- 'make',
21
- 'create',
22
- 'build',
23
- 'about',
24
- 'using',
25
- 'what',
26
- 'should',
27
- 'how'
28
- ])
29
-
30
- export function tokenise(text: string): string[] {
31
- return text
32
- .toLowerCase()
33
- .replace(/[^a-z0-9\s/]/g, ' ')
34
- .split(/[\s/]+/)
35
- .filter((w) => w.length > 1 && !STOP_WORDS.has(w))
36
- }
37
-
38
- export function editDistance(s1: string, s2: string): number {
39
- const costs: number[] = []
40
- for (let i = 0; i <= s1.length; i++) {
41
- let lastValue = i
42
- for (let j = 0; j <= s2.length; j++) {
43
- if (i === 0) {
44
- costs[j] = j
45
- } else {
46
- if (j > 0) {
47
- let newValue = costs[j - 1]
48
- if (s1.charAt(i - 1) !== s2.charAt(j - 1)) {
49
- newValue = Math.min(Math.min(newValue, lastValue), costs[j]) + 1
50
- }
51
- costs[j - 1] = lastValue
52
- lastValue = newValue
53
- }
54
- }
55
- }
56
- if (i > 0) costs[s2.length] = lastValue
57
- }
58
- return costs[s2.length]
59
- }
60
-
61
- export function isSimilar(w1: string, w2: string): boolean {
62
- if (w1.length < 3 || w2.length < 3) return w1 === w2
63
- // Typo tolerance: 1 edit for 3-5 chars, 2 edits for 6+ chars
64
- const maxDistance = w1.length >= 6 && w2.length >= 6 ? 2 : 1
65
- return editDistance(w1, w2) <= maxDistance
66
- }
@@ -1,186 +0,0 @@
1
- import { readFileSync, existsSync } from 'fs'
2
- import path from 'path'
3
- import { fileURLToPath } from 'url'
4
- import { tokenise, isSimilar } from './intent-helpers.js'
5
-
6
- export type IntentEntry = {
7
- name: string // the caveman phrase (first line)
8
- phrases: string[] // all tokenised words from the phrase
9
- emmet: string // the emmet pattern from the "- ..." line
10
- category: string // section header e.g. "DESTROY / DANGER"
11
- }
12
-
13
- /**
14
- * Parse the INTENTS block out of api-full.txt.
15
- *
16
- * Format expected:
17
- * --- CATEGORY NAME ---
18
- * caveman phrase description
19
- * - emmet[pattern]{here}
20
- * (blank line)
21
- * ...
22
- */
23
- function parseIntents(raw: string): IntentEntry[] {
24
- const entries: IntentEntry[] = []
25
-
26
- // Find where the INTENTS block starts
27
- const intentStart = raw.indexOf('INTENTS:')
28
- if (intentStart === -1) return entries
29
-
30
- const block = raw.slice(intentStart)
31
- const lines = block.split(/\r?\n/)
32
-
33
- let currentCategory = 'GENERAL'
34
- let pendingPhrase: string | null = null
35
-
36
- for (const raw_line of lines) {
37
- const line = raw_line.trim()
38
-
39
- // Category header: --- DESTROY / DANGER ---
40
- if (line.startsWith('---') && line.endsWith('---')) {
41
- currentCategory = line
42
- .replace(/^-+\s*/, '')
43
- .replace(/\s*-+$/, '')
44
- .trim()
45
- pendingPhrase = null
46
- continue
47
- }
48
-
49
- // Skip meta comment line [caveman prompt → emmet...]
50
- if (line.startsWith('[') && line.endsWith(']')) continue
51
-
52
- // Skip NEVER DO section (starts with "never use")
53
- if (line.startsWith('never ')) continue
54
-
55
- // Skip blank lines and "→" alias lines (these are in NEVER DO)
56
- if (line === '' || line.startsWith('→')) continue
57
-
58
- // Emmet line: starts with "- "
59
- if (line.startsWith('- ') && pendingPhrase !== null) {
60
- const emmet = line.slice(2).trim()
61
- entries.push({
62
- name: pendingPhrase,
63
- phrases: tokenise(pendingPhrase),
64
- emmet,
65
- category: currentCategory
66
- })
67
- pendingPhrase = null
68
- continue
69
- }
70
-
71
- // Anything else is a caveman phrase (intent description)
72
- if (!line.startsWith('-')) {
73
- pendingPhrase = line
74
- }
75
- }
76
-
77
- return entries
78
- }
79
-
80
- // ---- Resolve the api-full.txt path relative to this package root ----
81
- function loadIntents(): IntentEntry[] {
82
- try {
83
- const currentDir = path.dirname(fileURLToPath(import.meta.url))
84
-
85
- // Try relative paths from current file structure (packages/mcp/src/utils/ or dist/utils/)
86
- const path1 = path.resolve(currentDir, '../../../api-full.txt')
87
- const path2 = path.resolve(currentDir, '../../api-full.txt')
88
- const path3 = path.resolve(currentDir, '../../../../api-full.txt')
89
-
90
- let txtPath = path1
91
- if (existsSync(path1)) {
92
- txtPath = path1
93
- } else if (existsSync(path2)) {
94
- txtPath = path2
95
- } else if (existsSync(path3)) {
96
- txtPath = path3
97
- } else {
98
- txtPath = path.resolve(process.cwd(), '../../api-full.txt')
99
- }
100
-
101
- const raw = readFileSync(txtPath, 'utf8')
102
- return parseIntents(raw)
103
- } catch {
104
- return []
105
- }
106
- }
107
-
108
- // Singleton - parsed once at server startup, stays in-memory (DC-13)
109
- let _cache: IntentEntry[] | null = null
110
-
111
- export function getIntentEntries(): IntentEntry[] {
112
- if (!_cache) {
113
- _cache = loadIntents()
114
- }
115
- return _cache
116
- }
117
-
118
- export type ScoreResult = {
119
- score: number
120
- density: number
121
- }
122
-
123
- /**
124
- * Score a single IntentEntry against the query, tolerating minor typos.
125
- * Returns { score, density } for tie-breaking.
126
- */
127
- export function scoreEntry(
128
- entry: IntentEntry,
129
- queryWords: string[]
130
- ): ScoreResult {
131
- let score = 0
132
- const entryWords = new Set(entry.phrases)
133
- let matchedCount = 0
134
-
135
- for (const qw of queryWords) {
136
- let matched = false
137
- // 1. Exact match (+10)
138
- if (entryWords.has(qw)) {
139
- score += 10
140
- matched = true
141
- } else {
142
- // 2. Fuzzy spelling similarity match (+8)
143
- for (const ew of entryWords) {
144
- if (isSimilar(qw, ew)) {
145
- score += 8
146
- matched = true
147
- break
148
- }
149
- }
150
- // 3. Substring match fallback (+4)
151
- if (!matched) {
152
- for (const ew of entryWords) {
153
- if (ew.includes(qw) || qw.includes(ew)) {
154
- score += 4
155
- matched = true
156
- break
157
- }
158
- }
159
- }
160
- }
161
- if (matched) {
162
- matchedCount++
163
- }
164
- }
165
-
166
- // Bonus: category keyword overlap (supports fuzzy spelling matches)
167
- const catWords = tokenise(entry.category)
168
- for (const qw of queryWords) {
169
- if (catWords.includes(qw)) {
170
- score += 3
171
- } else {
172
- for (const cw of catWords) {
173
- if (isSimilar(qw, cw)) {
174
- score += 2
175
- break
176
- }
177
- }
178
- }
179
- }
180
-
181
- // Density = portion of the entry's phrases that were matched by the query
182
- const density =
183
- entry.phrases.length > 0 ? matchedCount / entry.phrases.length : 0
184
-
185
- return { score, density }
186
- }
@@ -1,7 +0,0 @@
1
- import { encoding_for_model } from 'tiktoken'
2
-
3
- const encoder = encoding_for_model('gpt-4')
4
-
5
- export function getTokenCount(input: string): number {
6
- return encoder.encode(input).length
7
- }
package/tsconfig.json DELETED
@@ -1,14 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "compilerOptions": {
4
- "target": "ES2020",
5
- "module": "NodeNext",
6
- "moduleResolution": "NodeNext",
7
- "resolveJsonModule": true,
8
- "esModuleInterop": true,
9
- "strict": true,
10
- "outDir": "./dist",
11
- "composite": false
12
- },
13
- "include": ["src/**/*", "src/**/*.json"]
14
- }
package/tsup.config.ts DELETED
@@ -1,13 +0,0 @@
1
- import { defineConfig } from 'tsup'
2
-
3
- export default defineConfig({
4
- entry: ['src/server.ts', 'src/utils/check-api.ts'],
5
-
6
- format: ['esm'],
7
- target: 'es2020',
8
- dts: true,
9
- clean: true,
10
- sourcemap: true,
11
- bundle: true,
12
- external: ['tiktoken']
13
- })