@astralkit/mcp 1.5.0 → 1.7.1
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 +58 -0
- package/dist/api.js +2 -2
- package/dist/auth.js +4 -2
- package/dist/auth.js.map +1 -1
- package/dist/data/build-standards.d.ts +5 -0
- package/dist/data/build-standards.d.ts.map +1 -0
- package/dist/data/build-standards.js +190 -0
- package/dist/data/build-standards.js.map +1 -0
- package/dist/data/screens.d.ts +5 -0
- package/dist/data/screens.d.ts.map +1 -0
- package/dist/data/screens.js +161 -0
- package/dist/data/screens.js.map +1 -0
- package/dist/review.d.ts +19 -0
- package/dist/review.d.ts.map +1 -0
- package/dist/review.js +182 -0
- package/dist/review.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +210 -31
- package/dist/server.js.map +1 -1
- package/package.json +5 -3
package/dist/review.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// review_app — a STRUCTURAL audit (the architectural sibling of validate_code's
|
|
2
|
+
// visual audit). It flags high-signal "messy app" smells: a missing four-state
|
|
3
|
+
// contract, swallowed errors, atomic-design/separation-of-concerns violations,
|
|
4
|
+
// missing mobile navigation, and Radix a11y gaps. Heuristic, not a compiler —
|
|
5
|
+
// deliberately conservative (only flags what it is confident about) so agents
|
|
6
|
+
// trust it. Rules mirror the named anti-patterns in docs/empty-states-guide.md,
|
|
7
|
+
// docs/error-handing.md, and docs/CODE_ARCHITECTURE_GUIDE.md.
|
|
8
|
+
const FILE_SIZE_LIMIT = 300;
|
|
9
|
+
// Strip line/block comments so we don't flag commented-out code.
|
|
10
|
+
function decomment(code) {
|
|
11
|
+
return code.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:])\/\/[^\n]*/g, '$1');
|
|
12
|
+
}
|
|
13
|
+
function countLines(s) {
|
|
14
|
+
return s.split('\n').length;
|
|
15
|
+
}
|
|
16
|
+
// ── Checks that work on any code snippet ──────────────────────────────────────
|
|
17
|
+
function snippetChecks(raw, where) {
|
|
18
|
+
const issues = [];
|
|
19
|
+
const code = decomment(raw);
|
|
20
|
+
const at = where ? ` (${where})` : '';
|
|
21
|
+
// --- State contract: the "Ghost" (render nothing when empty) ---
|
|
22
|
+
// `{items.length > 0 && <JSX>}` or `{items.length && <JSX>}` with no empty branch.
|
|
23
|
+
const ghost = /\{\s*[\w.]+\.length\s*(?:>\s*0\s*)?&&\s*[(<]/.test(code);
|
|
24
|
+
const hasEmptyHandling = /length\s*===\s*0|length\s*<\s*1|!\w+\??\.length|\bEmptyState\b|isEmpty|no[\s-]?results/i.test(code);
|
|
25
|
+
if (ghost && !hasEmptyHandling) {
|
|
26
|
+
issues.push({
|
|
27
|
+
rule: 'ghost-render',
|
|
28
|
+
severity: 'high',
|
|
29
|
+
detail: `Renders a list only when it has items, with no empty branch${at} — users see blank space and assume it's broken (the "Ghost" anti-pattern).`,
|
|
30
|
+
fix: 'Add an explicit empty branch: `if (!data?.length) return <EmptyState .../>` before rendering the list. See get_build_standards topic "state-lifecycle".',
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
// --- State contract: the "Naked Empty" ---
|
|
34
|
+
if (/return\s*\(?\s*<\w+[^>]*>\s*(No data|Nothing here|No results|Empty|None)\b/i.test(code)) {
|
|
35
|
+
issues.push({
|
|
36
|
+
rule: 'naked-empty-state',
|
|
37
|
+
severity: 'medium',
|
|
38
|
+
detail: `A bare "No data"-style empty state${at} with no icon, explanation, or action.`,
|
|
39
|
+
fix: 'Use a real empty state (icon + title + description + CTA) — reuse atoms/empty-state.tsx. See get_build_standards topic "empty-states".',
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
// --- State contract: a data view with no loading/empty handling ---
|
|
43
|
+
const isDataView = /\buseQuery\s*\(|\buseSWR\s*\(/.test(code);
|
|
44
|
+
const handlesLoadingOrEmpty = /isLoading|isPending|isFetching|Skeleton|EmptyState|length\s*===\s*0|!\w+\??\.length/.test(code);
|
|
45
|
+
if (isDataView && !handlesLoadingOrEmpty) {
|
|
46
|
+
issues.push({
|
|
47
|
+
rule: 'incomplete-states',
|
|
48
|
+
severity: 'medium',
|
|
49
|
+
detail: `Fetches data${at} but appears to render no loading skeleton or empty state.`,
|
|
50
|
+
fix: 'Handle all four states: loading (skeleton) -> empty -> error -> data. See get_build_standards topic "state-lifecycle".',
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
// --- Error handling: empty catch ---
|
|
54
|
+
if (/catch\s*\([^)]*\)\s*\{\s*\}/.test(code)) {
|
|
55
|
+
issues.push({
|
|
56
|
+
rule: 'empty-catch',
|
|
57
|
+
severity: 'high',
|
|
58
|
+
detail: `An empty catch block${at} silently swallows errors.`,
|
|
59
|
+
fix: 'Every catch must log AND update the UI (set an error state / show a recovery toast). See get_build_standards topic "error-handling".',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
// --- Error handling: alert() ---
|
|
63
|
+
if (/(?:^|[^.\w])alert\s*\(/.test(code)) {
|
|
64
|
+
issues.push({
|
|
65
|
+
rule: 'alert-used',
|
|
66
|
+
severity: 'high',
|
|
67
|
+
detail: `Uses alert()${at} for user feedback.`,
|
|
68
|
+
fix: 'Replace alert() with a toast (react-hot-toast / Sonner / Radix Toast). Error toasts should not auto-dismiss.',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
// --- Error handling: raw error rendered to the UI (the "Confuser") ---
|
|
72
|
+
if (/\{\s*\w*[eE]rr\w*\.message\s*\}/.test(code) || /Error\s*\d{3}\b/.test(code) || /ECONNREFUSED|ETIMEDOUT/.test(code)) {
|
|
73
|
+
issues.push({
|
|
74
|
+
rule: 'raw-error-shown',
|
|
75
|
+
severity: 'medium',
|
|
76
|
+
detail: `A raw error message or error code is shown to the user${at}.`,
|
|
77
|
+
fix: 'Show a plain-language title + recovery action; log the technical detail. Never surface stack traces / status codes / errno to users. See get_build_standards topic "error-handling".',
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
// --- Responsive: a nav with no mobile menu ---
|
|
81
|
+
const looksLikeNav = /<nav[\s>]|role=["']navigation["']|<header[\s>]/.test(code);
|
|
82
|
+
const hasDesktopLinks = /hidden\s+md:|md:flex|md:items-center/.test(code);
|
|
83
|
+
const hasMobileMenu = /md:hidden|Sheet|Drawer|Dialog|hamburger|mobile[-\s]?menu|MobileMenu|\bList\b.*weight|aria-label=["'][^"']*menu/i.test(code);
|
|
84
|
+
if (looksLikeNav && hasDesktopLinks && !hasMobileMenu) {
|
|
85
|
+
issues.push({
|
|
86
|
+
rule: 'missing-mobile-menu',
|
|
87
|
+
severity: 'high',
|
|
88
|
+
detail: `Navigation${at} has desktop links but appears to have no mobile menu (no hamburger / Dialog sheet / md:hidden toggle).`,
|
|
89
|
+
fix: 'Add a hamburger (md:hidden) that opens a Radix Dialog used as a side sheet (w-[85vw] max-w-sm). See get_build_standards topic "responsive" and get_screen_blueprint("nav").',
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
// --- Radix: Dialog/AlertDialog content without a Title (a11y) ---
|
|
93
|
+
const hasDialogContent = /DialogContent|Dialog\.Content|AlertDialogContent|AlertDialog\.Content/.test(code);
|
|
94
|
+
const hasDialogTitle = /DialogTitle|Dialog\.Title|AlertDialogTitle|AlertDialog\.Title/.test(code);
|
|
95
|
+
if (hasDialogContent && !hasDialogTitle) {
|
|
96
|
+
issues.push({
|
|
97
|
+
rule: 'dialog-without-title',
|
|
98
|
+
severity: 'medium',
|
|
99
|
+
detail: `A Radix Dialog/AlertDialog${at} has content but no Title — screen readers announce nothing and Radix warns.`,
|
|
100
|
+
fix: 'Add a Title (wrap in a visually-hidden span if there is no visible heading) and a description / aria-describedby. See get_build_standards topic "radix".',
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return issues;
|
|
104
|
+
}
|
|
105
|
+
// ── Path-aware checks (only available when `files` is provided) ───────────────
|
|
106
|
+
function fileChecks(path, content) {
|
|
107
|
+
const issues = [];
|
|
108
|
+
const lines = countLines(content);
|
|
109
|
+
const lower = path.toLowerCase();
|
|
110
|
+
if (lines > FILE_SIZE_LIMIT) {
|
|
111
|
+
issues.push({
|
|
112
|
+
rule: 'file-too-large',
|
|
113
|
+
severity: 'medium',
|
|
114
|
+
detail: `${path} is ${lines} lines (limit ${FILE_SIZE_LIMIT}).`,
|
|
115
|
+
fix: 'Split it: extract sub-components, a custom hook, or utilities. See get_build_standards topic "architecture" (file-size limits) and "refactoring".',
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
const code = decomment(content);
|
|
119
|
+
const isAtomOrMolecule = /[\\/](atoms|molecules)[\\/]/.test(lower);
|
|
120
|
+
const hasSideEffects = /\bfetch\s*\(|\buseQuery\s*\(|\buseSWR\s*\(|await\s+prisma|['"]use server['"]/.test(code);
|
|
121
|
+
if (isAtomOrMolecule && hasSideEffects) {
|
|
122
|
+
issues.push({
|
|
123
|
+
rule: 'atom-with-side-effects',
|
|
124
|
+
severity: 'high',
|
|
125
|
+
detail: `${path} is an atom/molecule but fetches data or runs a server action — it should be presentational (data via props).`,
|
|
126
|
+
fix: 'Move data fetching to a hook/container; keep this component dumb. See get_build_standards topic "architecture" (atomic design + presentational vs container).',
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
const isRouteFile = /[\\/]page\.tsx$/.test(lower);
|
|
130
|
+
const hasBusinessLogic = /\buseState\s*\(|\buseEffect\s*\(|\buseReducer\s*\(/.test(code);
|
|
131
|
+
if (isRouteFile && hasBusinessLogic && lines > 60) {
|
|
132
|
+
issues.push({
|
|
133
|
+
rule: 'fat-page',
|
|
134
|
+
severity: 'medium',
|
|
135
|
+
detail: `${path} (a route file) holds component state/logic and is ${lines} lines.`,
|
|
136
|
+
fix: 'Keep page.tsx thin (wire data + compose). Move state/logic into a hook and the UI into an organism. See get_build_standards topic "architecture".',
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return issues;
|
|
140
|
+
}
|
|
141
|
+
export function reviewApp(input) {
|
|
142
|
+
const issues = [];
|
|
143
|
+
if (input.files && Object.keys(input.files).length > 0) {
|
|
144
|
+
const routePaths = new Set();
|
|
145
|
+
for (const [path, content] of Object.entries(input.files)) {
|
|
146
|
+
issues.push(...fileChecks(path, content));
|
|
147
|
+
issues.push(...snippetChecks(content, path));
|
|
148
|
+
const lower = path.toLowerCase();
|
|
149
|
+
const m = lower.match(/(.*[\\/])(page|layout)\.tsx$/);
|
|
150
|
+
if (m)
|
|
151
|
+
routePaths.add(m[1]);
|
|
152
|
+
}
|
|
153
|
+
// Route hygiene: a route folder with page.tsx should ship error.tsx + loading.tsx.
|
|
154
|
+
for (const dir of routePaths) {
|
|
155
|
+
const has = (name) => Object.keys(input.files).some(p => p.toLowerCase() === `${dir}${name}`);
|
|
156
|
+
const missing = ['error.tsx', 'loading.tsx'].filter(n => !has(n));
|
|
157
|
+
if (missing.length) {
|
|
158
|
+
issues.push({
|
|
159
|
+
rule: 'route-missing-boundaries',
|
|
160
|
+
severity: 'medium',
|
|
161
|
+
detail: `Route ${dir} is missing ${missing.join(' + ')}.`,
|
|
162
|
+
fix: 'Every route should ship error.tsx (recovery UI) and loading.tsx (skeleton). See get_build_standards topic "error-handling". (Note: only the files you passed were checked.)',
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
else if (input.code) {
|
|
168
|
+
issues.push(...snippetChecks(input.code, ''));
|
|
169
|
+
}
|
|
170
|
+
const screenNote = input.screenType
|
|
171
|
+
? ` For screen-specific must-haves, also see get_screen_blueprint("${input.screenType}").`
|
|
172
|
+
: '';
|
|
173
|
+
return {
|
|
174
|
+
valid: issues.length === 0,
|
|
175
|
+
issueCount: issues.length,
|
|
176
|
+
issues,
|
|
177
|
+
summary: issues.length === 0
|
|
178
|
+
? `No structural issues detected. NOTE: review_app is a heuristic structural audit, not a compiler or a substitute for review — it checks for common "messy app" smells (missing states, swallowed errors, layering, mobile nav, Radix a11y), not correctness or compilation.${screenNote}`
|
|
179
|
+
: `Found ${issues.length} structural issue${issues.length === 1 ? '' : 's'}. Apply each fix; pair with validate_code for visual/token issues. review_app is heuristic — confirm each finding in context.${screenNote}`,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=review.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"review.js","sourceRoot":"","sources":["../src/review.ts"],"names":[],"mappings":"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"}
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AA4kCA,wBAAsB,WAAW,kBA2BhC"}
|
package/dist/server.js
CHANGED
|
@@ -9,7 +9,10 @@ import { PHOSPHOR_ICONS, PHOSPHOR_ICON_NAMES } from './data/icons.js';
|
|
|
9
9
|
import { SETUP } from './data/setup.js';
|
|
10
10
|
import { CROSSWALK, nearestToken } from './data/crosswalk.js';
|
|
11
11
|
import { buildPolishGuide, ARCHETYPE_KEYS } from './data/polish.js';
|
|
12
|
-
|
|
12
|
+
import { buildStandardsGuide, BUILD_STANDARD_TOPICS } from './data/build-standards.js';
|
|
13
|
+
import { buildScreenBlueprint, SCREEN_TYPES } from './data/screens.js';
|
|
14
|
+
import { reviewApp } from './review.js';
|
|
15
|
+
const VERSION = '1.7.1';
|
|
13
16
|
/** Build "raw → ak" suggestions for a list of flagged raw classes (for validate_code). */
|
|
14
17
|
function tokenSuggestions(classes) {
|
|
15
18
|
const seen = new Set();
|
|
@@ -26,24 +29,29 @@ function tokenSuggestions(classes) {
|
|
|
26
29
|
}
|
|
27
30
|
// The golden path — surfaced as server `instructions` so even a naive agent
|
|
28
31
|
// (one that just gets "build X") self-guides to design-system-quality output.
|
|
29
|
-
const SERVER_INSTRUCTIONS = `AstralKit builds polished, accessible, on-brand UI
|
|
32
|
+
const SERVER_INSTRUCTIONS = `AstralKit builds polished, accessible, on-brand UI. The AstralKit COMPONENT LIBRARY is the source of truth — build FROM real library components; do NOT freestyle UI. Hand-writing a component the library already provides is a failure, even if it is token-compliant.
|
|
30
33
|
|
|
31
|
-
GOLDEN PATH —
|
|
34
|
+
GOLDEN PATH — for ANY UI request (build, redesign, or improve a page/section/component):
|
|
32
35
|
1. get_coding_standards — the mandatory rules. Read first.
|
|
33
36
|
2. get_design_tokens — use ONLY ak-* tokens. NEVER raw Tailwind colors (bg-gray-*), NEVER arbitrary values (p-[1rem]).
|
|
34
|
-
3. search_components —
|
|
35
|
-
4. get_preview — SEE
|
|
36
|
-
5. get_component (mode:"recipe") —
|
|
37
|
-
6.
|
|
38
|
-
7.
|
|
39
|
-
8. validate_code — run
|
|
40
|
-
9. BUILD IT — validate_code checks conventions, NOT that the code compiles. Always run a real build/typecheck (tsc / next build) before declaring done; it will not catch syntax errors (e.g. a stray */ inside a block comment, unbalanced braces).
|
|
37
|
+
3. search_components — ALWAYS do this before writing any UI. Find the closest real component(s) for what you are building; never skip straight to JSX.
|
|
38
|
+
4. get_preview — SEE candidates (returns an image) and pick the best-looking fit.
|
|
39
|
+
5. get_component (mode:"recipe") — get the real component and REUSE it: keep its structure, layout, and polish. "Adapt" = RE-CONTENT, not rebuild — swap its placeholder copy, nav items, logo, and sample data for the app's real content, and match the app's theme. Do NOT regenerate the layout into something generic, and do NOT freestyle a replacement.
|
|
40
|
+
6. install_component — install the chosen component(s) into the project (e.g. components/ui) and import them. The library piece is what ships; your job is to wire it in + re-content it.
|
|
41
|
+
7. get_icons — confirm Phosphor names exist (never guess). get_setup — the CSS imports + font-load step.
|
|
42
|
+
8. validate_code + review_app — fix every issue, then BUILD (run tsc / next build — validate_code is NOT a compiler; it won't catch syntax errors).
|
|
41
43
|
|
|
42
|
-
|
|
44
|
+
IF NOTHING FITS EXACTLY: still start from the CLOSEST component's recipe and modify it (adjust layout/content as needed) — NEVER from a blank file. The reference sets the quality bar; match it.
|
|
43
45
|
|
|
44
|
-
|
|
46
|
+
REDESIGN / IMPROVE AN EXISTING APP: for each bespoke piece (sidebar, top bar, cards, forms, hero…) search the AstralKit equivalent, install it, and REPLACE the bespoke one — then RE-CONTENT it with the app's real nav, logo, copy, and data. PRESERVE the app's existing theme: if the app is dark, keep it dark (set the matching data-ak-theme — never flip dark↔light). Never invent colors. ("*-light" showcase components are LIGHT demos — match them to the app's theme, don't drag the app to light.)
|
|
45
47
|
|
|
46
|
-
|
|
48
|
+
TOKENIZE (source already well-designed, only needs ak-* tokens): get_coding_standards → get_design_tokens (raw-Tailwind→ak CROSSWALK) → map every raw class → swap icons to Phosphor → validate_code → build. PRESERVE the source theme — the crosswalk assumes a LIGHT source, so for a dark app map to dark surfaces / inverse roles, not the light defaults (mapping a dark app naively flips it to a broken light theme).
|
|
49
|
+
|
|
50
|
+
POLISH / REVAMP (weak hierarchy, cramped spacing, off-brand surfaces): call polish_ui FIRST. It keeps the original layout, content, and behavior.
|
|
51
|
+
|
|
52
|
+
BUILD IT RIGHT (architecture & screen UX): get_build_standards (atomic design, separation of concerns, the loading/empty/error/populated state contract, error handling, responsive/mobile-menu, radix; pass a topic). For a specific screen call get_screen_blueprint(screen_type) — it tells you WHICH AstralKit components to install for that screen + the must-haves. Audit existing/AI-generated code with review_app.
|
|
53
|
+
|
|
54
|
+
Hard rules: build from library components, never freestyle; 16px body floor (text-ak-base); 48px touch targets; Phosphor icons only; semantic color tokens; PRESERVE the app's theme. Prefer the resources astralkit://tokens, ://rules, ://icons, ://setup, ://standards if your client supports them.`;
|
|
47
55
|
// ─── Security: Input validation ───────────────────────────────────────────────
|
|
48
56
|
const MAX_QUERY_LENGTH = 200;
|
|
49
57
|
const MAX_SLUG_LENGTH = 100;
|
|
@@ -59,6 +67,10 @@ function validateSlug(slug) {
|
|
|
59
67
|
}
|
|
60
68
|
// ─── Security: Periodic auth revalidation ─────────────────────────────────────
|
|
61
69
|
const REVALIDATION_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes
|
|
70
|
+
// During a transient network outage, keep serving on the last good validation
|
|
71
|
+
// for up to this long rather than bricking a valid subscriber. A genuinely
|
|
72
|
+
// invalid/expired key is still caught immediately (that's an AuthError, not transient).
|
|
73
|
+
const AUTH_GRACE_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
62
74
|
class AuthGuard {
|
|
63
75
|
lastCheck;
|
|
64
76
|
apiKey;
|
|
@@ -80,11 +92,17 @@ class AuthGuard {
|
|
|
80
92
|
}
|
|
81
93
|
catch (err) {
|
|
82
94
|
if (err instanceof AuthError) {
|
|
95
|
+
// Genuine auth failure (invalid key / plan downgrade) — revoke immediately.
|
|
83
96
|
this.revoked = true;
|
|
84
97
|
console.error('[astralkit-mcp] Subscription revalidation failed — access revoked.');
|
|
98
|
+
throw err;
|
|
99
|
+
}
|
|
100
|
+
// Transient (network/timeout, ApiError). Do NOT revoke: keep serving on the
|
|
101
|
+
// last good validation within the grace window, and let the next call retry.
|
|
102
|
+
if (Date.now() - this.lastCheck < AUTH_GRACE_MS) {
|
|
103
|
+
console.error('[astralkit-mcp] Revalidation network error; serving on cached auth (will retry).');
|
|
104
|
+
return;
|
|
85
105
|
}
|
|
86
|
-
// Network errors (ApiError with TIMEOUT/NETWORK_ERROR) are transient —
|
|
87
|
-
// don't permanently revoke; the next check will retry.
|
|
88
106
|
throw err;
|
|
89
107
|
}
|
|
90
108
|
}
|
|
@@ -110,14 +128,61 @@ function imageResult(base64, mimeType) {
|
|
|
110
128
|
}
|
|
111
129
|
// ─── Recipe extraction (makes get_component build-ready / self-describing) ─────
|
|
112
130
|
const VALID_ICONS = new Set(PHOSPHOR_ICON_NAMES);
|
|
113
|
-
/**
|
|
131
|
+
/**
|
|
132
|
+
* Strip block, line, and JSX comments to shrink the recipe payload — string-aware,
|
|
133
|
+
* so it never eats comment-like text INSIDE a string/template literal (e.g. a glob
|
|
134
|
+
* "src/**\/*.tsx" or a protocol-relative URL "//cdn.example.com"). A naive regex
|
|
135
|
+
* pass corrupts such source; this walks the code tracking literal context.
|
|
136
|
+
*/
|
|
114
137
|
function stripComments(code) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
.
|
|
138
|
+
let out = '';
|
|
139
|
+
const n = code.length;
|
|
140
|
+
for (let i = 0; i < n;) {
|
|
141
|
+
const c = code[i];
|
|
142
|
+
const c2 = code[i + 1];
|
|
143
|
+
// string / template literal — copy verbatim (incl. escapes), never scan for comments inside
|
|
144
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
145
|
+
const q = c;
|
|
146
|
+
out += c;
|
|
147
|
+
i++;
|
|
148
|
+
while (i < n) {
|
|
149
|
+
if (code[i] === '\\') {
|
|
150
|
+
out += code[i] + (code[i + 1] ?? '');
|
|
151
|
+
i += 2;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
out += code[i];
|
|
155
|
+
const done = code[i] === q;
|
|
156
|
+
i++;
|
|
157
|
+
if (done)
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
// JSX comment {/* ... */}
|
|
163
|
+
if (c === '{' && c2 === '/' && code[i + 2] === '*') {
|
|
164
|
+
const end = code.indexOf('*/}', i + 3);
|
|
165
|
+
if (end !== -1) {
|
|
166
|
+
i = end + 3;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// block comment /* ... */
|
|
171
|
+
if (c === '/' && c2 === '*') {
|
|
172
|
+
const end = code.indexOf('*/', i + 2);
|
|
173
|
+
i = end === -1 ? n : end + 2;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
// line comment // ...
|
|
177
|
+
if (c === '/' && c2 === '/') {
|
|
178
|
+
while (i < n && code[i] !== '\n')
|
|
179
|
+
i++;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
out += c;
|
|
183
|
+
i++;
|
|
184
|
+
}
|
|
185
|
+
return out.replace(/[ \t]+$/gm, '').replace(/\n{3,}/g, '\n\n').trim();
|
|
121
186
|
}
|
|
122
187
|
/** Every ak-* token/utility referenced in the code (for tokensUsed). */
|
|
123
188
|
function extractAkTokens(code) {
|
|
@@ -144,10 +209,10 @@ function createServer(auth, guard, apiKey) {
|
|
|
144
209
|
server.registerTool('search_components', {
|
|
145
210
|
title: 'Search Components',
|
|
146
211
|
description: 'START HERE when building any UI with AstralKit. Search components by name, description, or category ' +
|
|
147
|
-
'to find
|
|
212
|
+
'to find the real component to INSTALL & REUSE (never freestyle one the library already has). ' +
|
|
148
213
|
'Read the descriptions: the library ranges from minimal to BOLD showcase designs (watermarks, oversized display type, complex/asymmetric layouts) — prefer the most striking option that fits the brand, not just the most literal keyword match. ' +
|
|
149
|
-
'Each result includes a previewImage URL; call get_preview(slug) to SEE a candidate and pick the best
|
|
150
|
-
'Returns matching components with metadata. (Golden path: get_coding_standards → get_design_tokens → search_components → get_preview → get_component →
|
|
214
|
+
'Each result includes a previewImage URL; call get_preview(slug) to SEE a candidate and pick the best fit, then get_component (recipe) + install_component to reuse it. ' +
|
|
215
|
+
'Returns matching components with metadata. (Golden path: get_coding_standards → get_design_tokens → search_components → get_preview → get_component → install_component (reuse + re-content) → validate_code.)',
|
|
151
216
|
inputSchema: {
|
|
152
217
|
query: z.string().max(MAX_QUERY_LENGTH).describe('Search query (component name, description, or keyword)'),
|
|
153
218
|
category: z.string().max(MAX_SLUG_LENGTH).optional().describe('Filter by category slug'),
|
|
@@ -261,7 +326,7 @@ function createServer(auth, guard, apiKey) {
|
|
|
261
326
|
});
|
|
262
327
|
server.registerTool('get_component', {
|
|
263
328
|
title: 'Get Component',
|
|
264
|
-
description: 'Get a build-ready AstralKit component to
|
|
329
|
+
description: 'Get a real, build-ready AstralKit component to INSTALL and REUSE — keep its structure/layout/polish and re-content it (swap copy, nav, logo, data) to fit the app; do not freestyle a replacement. ' +
|
|
265
330
|
'Call search_components/list_components first to find the slug. ' +
|
|
266
331
|
'Default mode "recipe" returns a lean, self-describing payload: source (comments stripped) plus ' +
|
|
267
332
|
'the exact ak-* tokens, Phosphor icons, intended font, setup, and dependencies it uses — so you never guess. ' +
|
|
@@ -301,7 +366,7 @@ function createServer(auth, guard, apiKey) {
|
|
|
301
366
|
slug: result.slug,
|
|
302
367
|
framework: fw,
|
|
303
368
|
previewImage: result.previewImage,
|
|
304
|
-
howToAdapt: '
|
|
369
|
+
howToAdapt: 'REUSE this component — install it into the project and keep its structure, layout, spacing, and polish (watermarks, display type, complex layout, effects ARE the premium character). ADAPT = RE-CONTENT only: replace placeholder copy, nav items, logo, and sample data with the real app content, and match the app theme (preserve dark/light — never flip it). Do NOT regenerate the layout into something generic, and do NOT freestyle a replacement. If it is not an exact fit, modify THIS recipe rather than building from a blank file.',
|
|
305
370
|
intendedFont: 'Inter via font-ak-sans (load with next/font and wire to --font-ak-sans — see get_setup)',
|
|
306
371
|
tokensUsed,
|
|
307
372
|
iconsUsed: icons.used,
|
|
@@ -384,6 +449,51 @@ function createServer(auth, guard, apiKey) {
|
|
|
384
449
|
}
|
|
385
450
|
return textResult(CODING_STANDARDS);
|
|
386
451
|
});
|
|
452
|
+
server.registerTool('get_build_standards', {
|
|
453
|
+
title: 'Get Build Standards',
|
|
454
|
+
description: 'Get AstralKit STRUCTURAL standards — how to build an app well, not just how to style it. ' +
|
|
455
|
+
'Covers architecture (atomic design, separation of concerns, layers, file-size limits), the ' +
|
|
456
|
+
'safe incremental refactoring process for cleaning up a messy app, the loading/empty/error/' +
|
|
457
|
+
'populated state contract, error handling, responsive/mobile-menu patterns, and Radix correctness. ' +
|
|
458
|
+
'This complements get_coding_standards (visual rules). Pass a topic to get one section; omit for the index. ' +
|
|
459
|
+
'Use it before building app structure, or when asked to clean up / refactor vibe-coded code.',
|
|
460
|
+
inputSchema: {
|
|
461
|
+
topic: z.enum(BUILD_STANDARD_TOPICS).optional()
|
|
462
|
+
.describe(`Which section to return. One of: ${BUILD_STANDARD_TOPICS.join(', ')}. Omit to get the index of all topics.`),
|
|
463
|
+
},
|
|
464
|
+
}, async ({ topic }) => {
|
|
465
|
+
try {
|
|
466
|
+
await guard.check();
|
|
467
|
+
}
|
|
468
|
+
catch (err) {
|
|
469
|
+
if (err instanceof AuthError)
|
|
470
|
+
return errorResult(err.message);
|
|
471
|
+
throw err;
|
|
472
|
+
}
|
|
473
|
+
return textResult(buildStandardsGuide(topic));
|
|
474
|
+
});
|
|
475
|
+
server.registerTool('get_screen_blueprint', {
|
|
476
|
+
title: 'Get Screen Blueprint',
|
|
477
|
+
description: 'Get a UX blueprint for a specific screen type — structure, must-haves, anti-patterns, the ' +
|
|
478
|
+
'four-state (loading/empty/error/populated) requirement, responsive/mobile notes, and exactly ' +
|
|
479
|
+
'which AstralKit components to INSTALL for this screen (categories + ready search_components queries + polish_ui archetypes) — reuse and re-content them, do not freestyle. ' +
|
|
480
|
+
'Call this FIRST when building a dashboard, navigation, pricing page, marketing/landing page, ' +
|
|
481
|
+
'auth screen, onboarding flow, or settings page, so the screen is built right and on-library.',
|
|
482
|
+
inputSchema: {
|
|
483
|
+
screen_type: z.enum(SCREEN_TYPES)
|
|
484
|
+
.describe(`The kind of screen to build. One of: ${SCREEN_TYPES.join(', ')}.`),
|
|
485
|
+
},
|
|
486
|
+
}, async ({ screen_type }) => {
|
|
487
|
+
try {
|
|
488
|
+
await guard.check();
|
|
489
|
+
}
|
|
490
|
+
catch (err) {
|
|
491
|
+
if (err instanceof AuthError)
|
|
492
|
+
return errorResult(err.message);
|
|
493
|
+
throw err;
|
|
494
|
+
}
|
|
495
|
+
return textResult(buildScreenBlueprint(screen_type));
|
|
496
|
+
});
|
|
387
497
|
server.registerTool('get_setup', {
|
|
388
498
|
title: 'Get Setup',
|
|
389
499
|
description: 'Get the exact project setup AstralKit needs to RENDER correctly: the CSS imports (theme + utilities), ' +
|
|
@@ -484,8 +594,8 @@ function createServer(auth, guard, apiKey) {
|
|
|
484
594
|
server.registerTool('get_preview', {
|
|
485
595
|
title: 'Get Preview Image',
|
|
486
596
|
description: 'Get a rendered PREVIEW IMAGE of a component so you can SEE its design before building. ' +
|
|
487
|
-
'Use it to compare candidates from search_components and pick the
|
|
488
|
-
'then call get_component (recipe) to
|
|
597
|
+
'Use it to compare candidates from search_components and pick the best fit — ' +
|
|
598
|
+
'then call get_component (recipe) to install and reuse the chosen one. Returns the image.',
|
|
489
599
|
inputSchema: {
|
|
490
600
|
slug: z.string().max(MAX_SLUG_LENGTH).describe('Component slug to preview'),
|
|
491
601
|
},
|
|
@@ -570,9 +680,12 @@ function createServer(auth, guard, apiKey) {
|
|
|
570
680
|
fix: `Replace with the nearest AstralKit token — ${tokenSuggestions([...new Set(rawTailwindColors)])}. (Full crosswalk via get_design_tokens.)`,
|
|
571
681
|
});
|
|
572
682
|
}
|
|
573
|
-
|
|
683
|
+
// Negative lookbehind for a word-char/hyphen so `h-[…]` inside `min-h-[…]`
|
|
684
|
+
// (and `w-[…]` inside `max-w-[…]`) is NOT falsely captured — those min-/max-
|
|
685
|
+
// prefixed values are legitimate (min-h touch targets, max-w width clamps).
|
|
686
|
+
const arbitraryValues = code.match(/(?<![\w-])(p|px|py|pt|pb|pl|pr|m|mx|my|mt|mb|ml|mr|gap|space-[xy]|w|h|text|rounded|leading)-\[[^\]]+\]/g);
|
|
574
687
|
if (arbitraryValues) {
|
|
575
|
-
const nonExempt = arbitraryValues.filter(v => !v.startsWith('
|
|
688
|
+
const nonExempt = arbitraryValues.filter(v => !v.startsWith('h-14'));
|
|
576
689
|
if (nonExempt.length > 0) {
|
|
577
690
|
issues.push({
|
|
578
691
|
rule: 'arbitrary-value',
|
|
@@ -665,6 +778,36 @@ function createServer(auth, guard, apiKey) {
|
|
|
665
778
|
fix: 'GSAP is not licensed for sold products/templates. Use Framer Motion (the MIT `motion` package) or CSS animations / the astralkit motion presets instead.',
|
|
666
779
|
});
|
|
667
780
|
}
|
|
781
|
+
// Cheap structural smells (the deep structural audit lives in review_app).
|
|
782
|
+
if (/catch\s*\([^)]*\)\s*\{\s*\}/.test(code)) {
|
|
783
|
+
issues.push({
|
|
784
|
+
rule: 'empty-catch',
|
|
785
|
+
detail: 'Empty catch block silently swallows errors.',
|
|
786
|
+
fix: 'Every catch must log AND update the UI (error state / recovery toast). See get_build_standards("error-handling").',
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
if (/(?:^|[^.\w])alert\s*\(/.test(code)) {
|
|
790
|
+
issues.push({
|
|
791
|
+
rule: 'alert-used',
|
|
792
|
+
detail: 'alert() used for user feedback.',
|
|
793
|
+
fix: 'Use a toast (react-hot-toast / Sonner / Radix Toast) — error toasts should not auto-dismiss. See get_build_standards("error-handling").',
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
if (/\{\s*[\w.]+\.length\s*(?:>\s*0\s*)?&&\s*[(<]/.test(code) &&
|
|
797
|
+
!/length\s*===\s*0|length\s*<\s*1|!\w+\??\.length|\bEmptyState\b|isEmpty/i.test(code)) {
|
|
798
|
+
issues.push({
|
|
799
|
+
rule: 'ghost-render',
|
|
800
|
+
detail: 'A list is rendered only when non-empty, with no empty branch (the "Ghost" — users see blank space).',
|
|
801
|
+
fix: 'Add an explicit empty branch before the list: if (!data?.length) return <EmptyState .../>. See get_build_standards("state-lifecycle").',
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
if (/return\s*\(?\s*<\w+[^>]*>\s*(No data|Nothing here|No results|Empty)\b/i.test(code)) {
|
|
805
|
+
issues.push({
|
|
806
|
+
rule: 'naked-empty-state',
|
|
807
|
+
detail: 'A bare "No data"-style empty state with no icon, explanation, or action.',
|
|
808
|
+
fix: 'Use a real empty state (icon + title + description + CTA) — reuse atoms/empty-state.tsx. See get_build_standards("empty-states").',
|
|
809
|
+
});
|
|
810
|
+
}
|
|
668
811
|
return jsonResult({
|
|
669
812
|
valid: issues.length === 0,
|
|
670
813
|
issueCount: issues.length,
|
|
@@ -674,6 +817,36 @@ function createServer(auth, guard, apiKey) {
|
|
|
674
817
|
: `Found ${issues.length} issue${issues.length === 1 ? '' : 's'}. Apply each fix and re-run validate_code until clean. (validate_code checks conventions, not compilation — also run a real build/typecheck.)`,
|
|
675
818
|
});
|
|
676
819
|
});
|
|
820
|
+
server.registerTool('review_app', {
|
|
821
|
+
title: 'Review App (structural audit)',
|
|
822
|
+
description: 'Audit code for STRUCTURAL "messy app" smells — the architectural counterpart to validate_code ' +
|
|
823
|
+
'(which checks visual/token issues). Flags a missing loading/empty/error state contract, the ' +
|
|
824
|
+
'"Ghost" and "Naked Empty" anti-patterns, swallowed errors (empty catch, alert(), raw errors ' +
|
|
825
|
+
'shown to users), atomic-design / separation-of-concerns violations, navigation with no mobile ' +
|
|
826
|
+
'menu, and Radix a11y gaps. Pass a single `code` snippet, or a `files` map (path -> contents) ' +
|
|
827
|
+
'for cross-file checks (file-size limits, atoms that fetch, fat page.tsx, routes missing ' +
|
|
828
|
+
'error.tsx/loading.tsx). Heuristic, not a compiler — pair it with validate_code.',
|
|
829
|
+
inputSchema: {
|
|
830
|
+
code: z.string().max(MAX_CODE_LENGTH).optional().describe('A single TSX/JSX file or snippet to audit.'),
|
|
831
|
+
files: z.record(z.string(), z.string().max(MAX_CODE_LENGTH)).optional()
|
|
832
|
+
.describe('A map of { "path/to/file.tsx": "contents" } for multi-file checks (file size, layering, route boundaries).'),
|
|
833
|
+
screen_type: z.enum(SCREEN_TYPES).optional()
|
|
834
|
+
.describe(`Optional screen type for screen-specific guidance. One of: ${SCREEN_TYPES.join(', ')}.`),
|
|
835
|
+
},
|
|
836
|
+
}, async ({ code, files, screen_type }) => {
|
|
837
|
+
try {
|
|
838
|
+
await guard.check();
|
|
839
|
+
}
|
|
840
|
+
catch (err) {
|
|
841
|
+
if (err instanceof AuthError)
|
|
842
|
+
return errorResult(err.message);
|
|
843
|
+
throw err;
|
|
844
|
+
}
|
|
845
|
+
if (!code && (!files || Object.keys(files).length === 0)) {
|
|
846
|
+
return errorResult('Provide either `code` (a snippet) or `files` (a path -> contents map) to review.');
|
|
847
|
+
}
|
|
848
|
+
return jsonResult(reviewApp({ code, files, screenType: screen_type }));
|
|
849
|
+
});
|
|
677
850
|
// Boosters (Plus/Lifetime only)
|
|
678
851
|
if (canAccessBoosters(auth.tier)) {
|
|
679
852
|
server.registerTool('list_boosters', {
|
|
@@ -783,6 +956,12 @@ function createServer(auth, guard, apiKey) {
|
|
|
783
956
|
contents: [{ uri: 'astralkit://polish', text: buildPolishGuide(), mimeType: 'text/markdown' }],
|
|
784
957
|
};
|
|
785
958
|
});
|
|
959
|
+
server.registerResource('AstralKit Build Standards', 'astralkit://standards', { description: 'Structural standards — atomic design, separation of concerns, the loading/empty/error/populated state contract, error handling, responsive/mobile-menu, Radix correctness, and the safe refactoring process (the architectural sibling of the visual coding rules)', mimeType: 'text/markdown' }, async () => {
|
|
960
|
+
await guard.check();
|
|
961
|
+
return {
|
|
962
|
+
contents: [{ uri: 'astralkit://standards', text: buildStandardsGuide(), mimeType: 'text/markdown' }],
|
|
963
|
+
};
|
|
964
|
+
});
|
|
786
965
|
// ─── Prompts ─────────────────────────────────────────────────────────
|
|
787
966
|
server.registerPrompt('build-with-astralkit', {
|
|
788
967
|
title: 'Build with AstralKit',
|