@mindfiredigital/ignix-lite-mcp 1.2.0 → 1.4.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 +108 -0
- package/dist/server.js +187 -1954
- package/dist/server.js.map +1 -1
- package/dist/utils/check-api.js +2 -0
- package/dist/utils/check-api.js.map +1 -1
- package/package.json +14 -6
- package/.turbo/turbo-build.log +0 -25
- package/CHANGELOG.md +0 -7
- package/dist/manifests/accordion.json +0 -61
- package/dist/manifests/alert.json +0 -69
- package/dist/manifests/avatar.json +0 -75
- package/dist/manifests/badge.json +0 -74
- package/dist/manifests/breadcrumb.json +0 -87
- package/dist/manifests/button.json +0 -85
- package/dist/manifests/card.json +0 -91
- package/dist/manifests/checkbox.json +0 -122
- package/dist/manifests/codeblock.json +0 -63
- package/dist/manifests/combobox.json +0 -33
- package/dist/manifests/dialog.json +0 -64
- package/dist/manifests/divider.json +0 -47
- package/dist/manifests/dropdown.json +0 -105
- package/dist/manifests/form.json +0 -81
- package/dist/manifests/grid.json +0 -143
- package/dist/manifests/input.json +0 -99
- package/dist/manifests/meter.json +0 -103
- package/dist/manifests/navigation.json +0 -70
- package/dist/manifests/progress.json +0 -88
- package/dist/manifests/radio.json +0 -121
- package/dist/manifests/select.json +0 -109
- package/dist/manifests/skeleton.json +0 -101
- package/dist/manifests/tab.json +0 -88
- package/dist/manifests/table.json +0 -92
- package/dist/manifests/textarea.json +0 -117
- package/dist/manifests/toast.json +0 -157
- package/dist/manifests/tooltip.json +0 -115
- package/dist/vector-index.json +0 -14015
- package/src/context/api-context.ts +0 -14
- package/src/global.d.ts +0 -15
- package/src/manifests/accordion.json +0 -61
- package/src/manifests/alert.json +0 -69
- package/src/manifests/avatar.json +0 -75
- package/src/manifests/badge.json +0 -74
- package/src/manifests/breadcrumb.json +0 -87
- package/src/manifests/button.json +0 -85
- package/src/manifests/card.json +0 -91
- package/src/manifests/checkbox.json +0 -122
- package/src/manifests/codeblock.json +0 -63
- package/src/manifests/combobox.json +0 -33
- package/src/manifests/dialog.json +0 -64
- package/src/manifests/divider.json +0 -47
- package/src/manifests/dropdown.json +0 -105
- package/src/manifests/form.json +0 -81
- package/src/manifests/grid.json +0 -143
- package/src/manifests/index.ts +0 -45
- package/src/manifests/input.json +0 -99
- package/src/manifests/meter.json +0 -103
- package/src/manifests/navigation.json +0 -70
- package/src/manifests/progress.json +0 -88
- package/src/manifests/radio.json +0 -121
- package/src/manifests/select.json +0 -109
- package/src/manifests/skeleton.json +0 -101
- package/src/manifests/tab.json +0 -88
- package/src/manifests/table.json +0 -92
- package/src/manifests/textarea.json +0 -117
- package/src/manifests/toast.json +0 -157
- package/src/manifests/tooltip.json +0 -115
- package/src/server.ts +0 -201
- package/src/tools/build-index.ts +0 -55
- package/src/tools/check-a11y.ts +0 -106
- package/src/tools/embedder.ts +0 -18
- package/src/tools/generate-theme.ts +0 -42
- package/src/tools/get-emmet.ts +0 -64
- package/src/tools/get-manifests.ts +0 -55
- package/src/tools/intent-engine.ts +0 -197
- package/src/tools/list-components.ts +0 -20
- package/src/tools/search-index.ts +0 -66
- package/src/tools/theme-palette.ts +0 -65
- package/src/tools/theme-tokens.ts +0 -176
- package/src/tools/validator.ts +0 -367
- package/src/types.ts +0 -63
- package/src/utils/a11y-rules.ts +0 -873
- package/src/utils/a11y-types.ts +0 -15
- package/src/utils/check-api.ts +0 -13
- package/src/utils/cosine.ts +0 -15
- package/src/utils/emmet-helpers.ts +0 -171
- package/src/utils/intent-helpers.ts +0 -66
- package/src/utils/intent-parser.ts +0 -186
- package/src/utils/tokenizer.ts +0 -7
- package/tsconfig.json +0 -14
- package/tsup.config.ts +0 -13
package/src/utils/a11y-types.ts
DELETED
|
@@ -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
|
-
}
|
package/src/utils/check-api.ts
DELETED
|
@@ -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
|
-
}
|
package/src/utils/cosine.ts
DELETED
|
@@ -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
|
-
}
|
package/src/utils/tokenizer.ts
DELETED
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
|
-
})
|