@ftarganski/omni-ai 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-3RZELMF2.js +214 -0
- package/dist/chunk-5WELBZWN.js +70 -0
- package/dist/chunk-6APAA6WD.js +495 -0
- package/dist/chunk-6OPRALDC.js +163 -0
- package/dist/chunk-6YFFZMXY.js +104 -0
- package/dist/chunk-AG6NZIJ3.js +122 -0
- package/dist/chunk-AWMSN7OB.js +451 -0
- package/dist/chunk-JTXDF5KG.js +156 -0
- package/dist/chunk-M4QJF7CV.js +57 -0
- package/dist/chunk-PPTEJ2FH.js +102 -0
- package/dist/chunk-S5MK6LBH.js +136 -0
- package/dist/chunk-TFU437SW.js +107 -0
- package/dist/chunk-Y4EYGADJ.js +216 -0
- package/dist/cli/bin.js +2723 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +42 -0
- package/dist/mcp.d.ts +1 -0
- package/dist/mcp.js +86 -0
- package/dist/memory.d.ts +1 -0
- package/dist/memory.js +320 -0
- package/dist/provider-anthropic.d.ts +1 -0
- package/dist/provider-anthropic.js +120 -0
- package/dist/provider-google.d.ts +1 -0
- package/dist/provider-google.js +141 -0
- package/dist/provider-openai.d.ts +1 -0
- package/dist/provider-openai.js +214 -0
- package/dist/skills/backend.d.ts +1 -0
- package/dist/skills/backend.js +12 -0
- package/dist/skills/code.d.ts +1 -0
- package/dist/skills/code.js +6 -0
- package/dist/skills/frontend.d.ts +1 -0
- package/dist/skills/frontend.js +10 -0
- package/dist/skills/fs.d.ts +1 -0
- package/dist/skills/fs.js +10 -0
- package/dist/skills/git.d.ts +1 -0
- package/dist/skills/git.js +12 -0
- package/dist/skills/http.d.ts +1 -0
- package/dist/skills/http.js +6 -0
- package/dist/skills/index.d.ts +1 -0
- package/dist/skills/index.js +60 -0
- package/dist/skills/multimodal.d.ts +1 -0
- package/dist/skills/multimodal.js +6 -0
- package/dist/skills/qa.d.ts +1 -0
- package/dist/skills/qa.js +8 -0
- package/dist/skills/ux.d.ts +1 -0
- package/dist/skills/ux.js +6 -0
- package/dist/src-6MUVU5ML.js +8 -0
- package/dist/src-ZHTGR7T6.js +8 -0
- package/package.json +136 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// ../skills/src/ux/audit-accessibility.ts
|
|
2
|
+
import { readdir, readFile } from "fs/promises";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
var InputSchema = z.object({
|
|
6
|
+
path: z.string().describe("File path (.tsx/.ts) or directory to audit recursively"),
|
|
7
|
+
recursive: z.boolean().default(false).describe("Scan all .tsx files in the directory recursively")
|
|
8
|
+
});
|
|
9
|
+
var RULES = [
|
|
10
|
+
{
|
|
11
|
+
id: "img-missing-alt",
|
|
12
|
+
severity: "critical",
|
|
13
|
+
description: "<img> without alt attribute \u2014 screen readers will read the file name",
|
|
14
|
+
suggestion: 'Add alt="" for decorative images or alt="descriptive text" for informative ones',
|
|
15
|
+
pattern: /<img(?![^>]*\balt=)[^>]*/
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: "onclick-non-interactive",
|
|
19
|
+
severity: "critical",
|
|
20
|
+
description: "onClick on a non-interactive element (div/span/p) without role or tabIndex",
|
|
21
|
+
suggestion: 'Use <button> instead, or add role="button" tabIndex={0} onKeyDown handler',
|
|
22
|
+
pattern: /<(div|span|p|section|article)\s[^>]*onClick/,
|
|
23
|
+
isIssue: (_match, line) => !/role=["']button["']/.test(line) && !/tabIndex/.test(line)
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: "link-blank-no-rel",
|
|
27
|
+
severity: "moderate",
|
|
28
|
+
description: 'target="_blank" without rel="noopener noreferrer" \u2014 security risk and unexpected UX',
|
|
29
|
+
suggestion: 'Add rel="noopener noreferrer" to all target="_blank" links',
|
|
30
|
+
pattern: /target=["']_blank["']/,
|
|
31
|
+
isIssue: (_match, line) => !/rel=/.test(line)
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: "hardcoded-color",
|
|
35
|
+
severity: "moderate",
|
|
36
|
+
description: "Hardcoded Tailwind color scale class \u2014 will break dark mode and theme consistency",
|
|
37
|
+
suggestion: "Replace with shadcn/ui semantic tokens: bg-background, text-foreground, bg-muted, text-muted-foreground, border-border",
|
|
38
|
+
pattern: /className=[^>]*["'][^"']*\b(bg|text|border|ring|outline|fill|stroke)-(white|black|gray|zinc|slate|stone|neutral|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-\d+[^"']*["']/
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: "icon-button-no-label",
|
|
42
|
+
severity: "critical",
|
|
43
|
+
description: "Button containing only an icon without aria-label \u2014 invisible to screen readers",
|
|
44
|
+
suggestion: 'Add aria-label="Descriptive action name" to the button',
|
|
45
|
+
pattern: /<button(?![^>]*aria-label)[^>]*>\s*<[A-Z][a-zA-Z]+\s*(?:size|className|strokeWidth)[^/]*\/>\s*<\/button>/
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: "input-missing-label",
|
|
49
|
+
severity: "critical",
|
|
50
|
+
description: "<input> or <Input> without aria-label, aria-labelledby, or associated <label>",
|
|
51
|
+
suggestion: "Wrap with Form.Item + Form.Label, or add aria-label directly to the input",
|
|
52
|
+
pattern: /<(?:input|Input)\s(?![^>]*(?:aria-label|aria-labelledby|id=))[^>]*/,
|
|
53
|
+
isIssue: (_match, _line, allLines, lineIndex) => {
|
|
54
|
+
const context = allLines.slice(Math.max(0, lineIndex - 5), lineIndex + 5).join("\n");
|
|
55
|
+
return !/<label|Form\.Label|htmlFor/.test(context);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "autofocus",
|
|
60
|
+
severity: "low",
|
|
61
|
+
description: "autoFocus used \u2014 can disorient screen reader users and break keyboard flow",
|
|
62
|
+
suggestion: "Manage focus programmatically with useEffect + ref.focus() only when truly needed (e.g. after modal opens)",
|
|
63
|
+
pattern: /\bautoFocus\b/
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "inline-style",
|
|
67
|
+
severity: "low",
|
|
68
|
+
description: "Inline style attribute \u2014 bypasses theme system and cannot adapt to dark mode",
|
|
69
|
+
suggestion: "Replace with Tailwind utility classes or shadcn/ui semantic tokens",
|
|
70
|
+
pattern: /\bstyle=\{/
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: "no-focus-visible",
|
|
74
|
+
severity: "moderate",
|
|
75
|
+
description: "Interactive element may be missing focus-visible styles",
|
|
76
|
+
suggestion: "Ensure focus-visible:ring-2 focus-visible:ring-ring is present on all focusable elements",
|
|
77
|
+
pattern: /<(button|a|input|select|textarea|Input|Button|Link)[^>]*(?:className|class)=["'][^"']*["'][^>]*>/,
|
|
78
|
+
isIssue: (_match, line) => !/focus-visible/.test(line) && !/focus:ring/.test(line)
|
|
79
|
+
}
|
|
80
|
+
];
|
|
81
|
+
async function auditFile(filePath) {
|
|
82
|
+
const content = await readFile(filePath, "utf-8");
|
|
83
|
+
const lines = content.split("\n");
|
|
84
|
+
const issues = [];
|
|
85
|
+
for (let i = 0; i < lines.length; i++) {
|
|
86
|
+
const line = lines[i];
|
|
87
|
+
for (const rule of RULES) {
|
|
88
|
+
rule.pattern.lastIndex = 0;
|
|
89
|
+
const match = rule.pattern.exec(line);
|
|
90
|
+
if (!match) continue;
|
|
91
|
+
const isIssue = rule.isIssue ? rule.isIssue(match, line, lines, i) : true;
|
|
92
|
+
if (!isIssue) continue;
|
|
93
|
+
issues.push({
|
|
94
|
+
file: filePath,
|
|
95
|
+
line: i + 1,
|
|
96
|
+
severity: rule.severity,
|
|
97
|
+
rule: rule.id,
|
|
98
|
+
description: rule.description,
|
|
99
|
+
suggestion: rule.suggestion,
|
|
100
|
+
snippet: line.trim().slice(0, 120)
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return issues;
|
|
105
|
+
}
|
|
106
|
+
var MAX_DEPTH = 10;
|
|
107
|
+
var MAX_FILES = 500;
|
|
108
|
+
async function collectFiles(dir, recursive, depth = 0, results = []) {
|
|
109
|
+
if (depth > MAX_DEPTH || results.length >= MAX_FILES) return results;
|
|
110
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
111
|
+
for (const entry of entries) {
|
|
112
|
+
if (results.length >= MAX_FILES) break;
|
|
113
|
+
if (entry.name === "node_modules" || entry.name === "dist") continue;
|
|
114
|
+
const full = join(dir, entry.name);
|
|
115
|
+
if (entry.isDirectory() && recursive) {
|
|
116
|
+
await collectFiles(full, recursive, depth + 1, results);
|
|
117
|
+
} else if (entry.isFile() && (entry.name.endsWith(".tsx") || entry.name.endsWith(".ts"))) {
|
|
118
|
+
results.push(full);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return results;
|
|
122
|
+
}
|
|
123
|
+
var auditAccessibilitySkill = {
|
|
124
|
+
name: "audit-accessibility",
|
|
125
|
+
description: "Heuristic static scan of React TSX files for accessibility and dark-mode issues. Detects: missing alt, onClick on non-interactive elements, icon-only buttons without aria-label, inputs without labels, target=_blank without rel, hardcoded colors, missing focus-visible styles, autoFocus misuse, inline styles.",
|
|
126
|
+
async execute(input) {
|
|
127
|
+
const { path, recursive } = InputSchema.parse(input);
|
|
128
|
+
let files;
|
|
129
|
+
try {
|
|
130
|
+
const entries = await readdir(path);
|
|
131
|
+
files = await collectFiles(path, recursive ?? false);
|
|
132
|
+
void entries;
|
|
133
|
+
} catch {
|
|
134
|
+
files = [path];
|
|
135
|
+
}
|
|
136
|
+
const allIssues = [];
|
|
137
|
+
for (const file of files) {
|
|
138
|
+
allIssues.push(...await auditFile(file));
|
|
139
|
+
}
|
|
140
|
+
const critical = allIssues.filter((i) => i.severity === "critical").length;
|
|
141
|
+
const moderate = allIssues.filter((i) => i.severity === "moderate").length;
|
|
142
|
+
const low = allIssues.filter((i) => i.severity === "low").length;
|
|
143
|
+
return {
|
|
144
|
+
totalFiles: files.length,
|
|
145
|
+
totalIssues: allIssues.length,
|
|
146
|
+
critical,
|
|
147
|
+
moderate,
|
|
148
|
+
low,
|
|
149
|
+
issues: allIssues
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
export {
|
|
155
|
+
auditAccessibilitySkill
|
|
156
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// ../skills/src/code/search-code.ts
|
|
2
|
+
import { readdir, readFile } from "fs/promises";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
var InputSchema = z.object({
|
|
6
|
+
directory: z.string().describe("Root directory to search in"),
|
|
7
|
+
pattern: z.string().describe("Text substring or regex pattern to search for"),
|
|
8
|
+
extensions: z.array(z.string()).default([".ts", ".tsx"]).describe("File extensions to include in the search"),
|
|
9
|
+
maxResults: z.number().int().positive().default(30).describe("Maximum number of matching lines to return"),
|
|
10
|
+
useRegex: z.boolean().default(false).describe("Treat pattern as a regular expression")
|
|
11
|
+
});
|
|
12
|
+
async function searchInFile(filePath, matcher, results, maxResults) {
|
|
13
|
+
if (results.length >= maxResults) return;
|
|
14
|
+
const text = await readFile(filePath, "utf-8");
|
|
15
|
+
const lines = text.split("\n");
|
|
16
|
+
for (let i = 0; i < lines.length; i++) {
|
|
17
|
+
if (results.length >= maxResults) break;
|
|
18
|
+
if (matcher(lines[i])) {
|
|
19
|
+
results.push({ file: filePath, line: i + 1, content: lines[i].trim() });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async function walkAndSearch(dir, extensions, matcher, results, maxResults) {
|
|
24
|
+
if (results.length >= maxResults) return;
|
|
25
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
26
|
+
for (const entry of entries) {
|
|
27
|
+
if (results.length >= maxResults) break;
|
|
28
|
+
if (entry.name === "node_modules" || entry.name === "dist") continue;
|
|
29
|
+
const fullPath = join(dir, entry.name);
|
|
30
|
+
if (entry.isDirectory()) {
|
|
31
|
+
await walkAndSearch(fullPath, extensions, matcher, results, maxResults);
|
|
32
|
+
} else if (entry.isFile() && extensions.some((ext) => entry.name.endsWith(ext))) {
|
|
33
|
+
await searchInFile(fullPath, matcher, results, maxResults);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
var searchCodeSkill = {
|
|
38
|
+
name: "search-code",
|
|
39
|
+
description: "Search for a text pattern across TypeScript/TSX source files. Use this to find existing components, hooks, types or patterns before generating new code \u2014 avoiding duplicates and understanding existing conventions.",
|
|
40
|
+
async execute(input) {
|
|
41
|
+
const { directory, pattern, extensions, maxResults, useRegex } = InputSchema.parse(input);
|
|
42
|
+
let matcher;
|
|
43
|
+
if (useRegex) {
|
|
44
|
+
const re = new RegExp(pattern);
|
|
45
|
+
matcher = (line) => re.test(line);
|
|
46
|
+
} else {
|
|
47
|
+
matcher = (line) => line.includes(pattern);
|
|
48
|
+
}
|
|
49
|
+
const results = [];
|
|
50
|
+
await walkAndSearch(directory, extensions, matcher, results, maxResults);
|
|
51
|
+
return results;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export {
|
|
56
|
+
searchCodeSkill
|
|
57
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// ../skills/src/qa/analyze-test-coverage.ts
|
|
2
|
+
import { readdir } from "fs/promises";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
var InputSchema = z.object({
|
|
6
|
+
directory: z.string().describe("Root directory to scan for source files and their tests"),
|
|
7
|
+
extensions: z.array(z.string()).default([".ts", ".tsx"]).describe("Source file extensions to check"),
|
|
8
|
+
ignorePatterns: z.array(z.string()).default(["index.ts", "index.tsx", ".spec.", ".test.", ".d.ts", ".module.ts"]).describe("Substrings that mark files to skip (index, spec, declaration, module files)")
|
|
9
|
+
});
|
|
10
|
+
async function collectFiles(dir, exts, ignore) {
|
|
11
|
+
const entries = await readdir(dir, { withFileTypes: true }).catch(() => null);
|
|
12
|
+
if (!entries) return [];
|
|
13
|
+
const files = [];
|
|
14
|
+
for (const entry of entries) {
|
|
15
|
+
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === ".git") continue;
|
|
16
|
+
const fullPath = join(dir, entry.name);
|
|
17
|
+
if (entry.isDirectory()) {
|
|
18
|
+
files.push(...await collectFiles(fullPath, exts, ignore));
|
|
19
|
+
} else if (entry.isFile() && exts.some((e) => entry.name.endsWith(e)) && !ignore.some((p) => entry.name.includes(p) || fullPath.includes(p))) {
|
|
20
|
+
files.push(fullPath);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return files;
|
|
24
|
+
}
|
|
25
|
+
async function collectAllFiles(dir) {
|
|
26
|
+
const files = await collectFiles(dir, [".ts", ".tsx"], []);
|
|
27
|
+
return new Set(files);
|
|
28
|
+
}
|
|
29
|
+
function specPathCandidates(filePath) {
|
|
30
|
+
const base = filePath.replace(/\.(tsx?)$/, "");
|
|
31
|
+
return [`${base}.spec.ts`, `${base}.spec.tsx`, `${base}.test.ts`, `${base}.test.tsx`];
|
|
32
|
+
}
|
|
33
|
+
var analyzeTestCoverageSkill = {
|
|
34
|
+
name: "analyze-test-coverage",
|
|
35
|
+
description: "Scan a directory and identify which source files have a corresponding .spec.ts test file and which do not. Use this before a QA pass to prioritize which files need tests written.",
|
|
36
|
+
async execute(input) {
|
|
37
|
+
const { directory, extensions, ignorePatterns } = InputSchema.parse(input);
|
|
38
|
+
const sourceFiles = await collectFiles(directory, extensions, ignorePatterns);
|
|
39
|
+
const allFiles = await collectAllFiles(directory);
|
|
40
|
+
const covered = [];
|
|
41
|
+
const uncovered = [];
|
|
42
|
+
for (const file of sourceFiles) {
|
|
43
|
+
const hasSpec = specPathCandidates(file).some((s) => allFiles.has(s));
|
|
44
|
+
if (hasSpec) covered.push(file);
|
|
45
|
+
else uncovered.push(file);
|
|
46
|
+
}
|
|
47
|
+
const total = covered.length + uncovered.length;
|
|
48
|
+
return {
|
|
49
|
+
covered,
|
|
50
|
+
uncovered,
|
|
51
|
+
coverageRate: total === 0 ? 1 : covered.length / total
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// ../skills/src/qa/find-test-pattern.ts
|
|
57
|
+
import { readdir as readdir2, readFile } from "fs/promises";
|
|
58
|
+
import { join as join2 } from "path";
|
|
59
|
+
import { z as z2 } from "zod";
|
|
60
|
+
var patternFilters = {
|
|
61
|
+
service: (name) => name.endsWith(".service.spec.ts"),
|
|
62
|
+
component: (name) => /^[A-Z]/.test(name) && (name.endsWith(".spec.ts") || name.endsWith(".spec.tsx")),
|
|
63
|
+
hook: (name) => name.startsWith("use") && (name.endsWith(".spec.ts") || name.endsWith(".spec.tsx")),
|
|
64
|
+
resolver: (name) => name.endsWith(".resolver.spec.ts")
|
|
65
|
+
};
|
|
66
|
+
var InputSchema2 = z2.object({
|
|
67
|
+
patternType: z2.enum(["service", "component", "hook", "resolver"]).describe("Type of test pattern to find examples of"),
|
|
68
|
+
directory: z2.string().describe("Root directory to search in"),
|
|
69
|
+
maxExamples: z2.number().int().positive().default(2).describe("Maximum number of test examples to return")
|
|
70
|
+
});
|
|
71
|
+
async function walkForTests(dir, filter, results, max) {
|
|
72
|
+
if (results.length >= max) return;
|
|
73
|
+
const entries = await readdir2(dir, { withFileTypes: true }).catch(() => null);
|
|
74
|
+
if (!entries) return;
|
|
75
|
+
for (const entry of entries) {
|
|
76
|
+
if (results.length >= max) break;
|
|
77
|
+
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === ".git") continue;
|
|
78
|
+
const fullPath = join2(dir, entry.name);
|
|
79
|
+
if (entry.isDirectory()) {
|
|
80
|
+
await walkForTests(fullPath, filter, results, max);
|
|
81
|
+
} else if (entry.isFile() && filter(entry.name)) {
|
|
82
|
+
const content = await readFile(fullPath, "utf-8");
|
|
83
|
+
results.push({ file: fullPath, content });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
var findTestPatternSkill = {
|
|
88
|
+
name: "find-test-pattern",
|
|
89
|
+
description: "Find real test file examples (service, component, hook, resolver) from the codebase. Use this before writing tests to understand the exact test structure, setup() pattern, mocking conventions and assertion style already in use.",
|
|
90
|
+
async execute(input) {
|
|
91
|
+
const { patternType, directory, maxExamples } = InputSchema2.parse(input);
|
|
92
|
+
const filter = patternFilters[patternType];
|
|
93
|
+
const results = [];
|
|
94
|
+
await walkForTests(directory, filter, results, maxExamples);
|
|
95
|
+
return results;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export {
|
|
100
|
+
analyzeTestCoverageSkill,
|
|
101
|
+
findTestPatternSkill
|
|
102
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// ../skills/src/frontend/analyze-component.ts
|
|
2
|
+
import { readFile } from "fs/promises";
|
|
3
|
+
import { resolve } from "path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
var InputSchema = z.object({
|
|
6
|
+
path: z.string().describe("Path to the React component file (.tsx or .ts)")
|
|
7
|
+
});
|
|
8
|
+
function extractComponentName(source, filePath) {
|
|
9
|
+
const fnMatch = /(?:export\s+(?:default\s+)?function\s+|const\s+)([A-Z]\w*)/.exec(source);
|
|
10
|
+
if (fnMatch) return fnMatch[1];
|
|
11
|
+
const parts = filePath.split(/[/\\]/);
|
|
12
|
+
return (parts[parts.length - 1] ?? "").replace(/\.(tsx?|jsx?)$/, "");
|
|
13
|
+
}
|
|
14
|
+
function extractPropsInterface(source) {
|
|
15
|
+
const ifaceMatch = /interface\s+(\w*Props\w*)\s*\{([^}]*)\}/.exec(source);
|
|
16
|
+
if (ifaceMatch) return `interface ${ifaceMatch[1]} {${ifaceMatch[2]}}`;
|
|
17
|
+
const typeMatch = /type\s+(\w*Props\w*)\s*=\s*\{([^}]*)\}/.exec(source);
|
|
18
|
+
if (typeMatch) return `type ${typeMatch[1]} = {${typeMatch[2]}}`;
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
function extractHooksUsed(source) {
|
|
22
|
+
const matches = source.match(/\buse[A-Z]\w*/g) ?? [];
|
|
23
|
+
return [...new Set(matches)];
|
|
24
|
+
}
|
|
25
|
+
function extractNamedExports(source) {
|
|
26
|
+
return [...source.matchAll(/export\s+(?:const|function|class|type|interface|enum)\s+(\w+)/g)].map((m) => m[1]);
|
|
27
|
+
}
|
|
28
|
+
var analyzeComponentSkill = {
|
|
29
|
+
name: "analyze-component",
|
|
30
|
+
description: "Parse a React component file and extract its name, props interface, hooks used, named exports, and whether it has a default export. Use this to understand an existing component's contract before creating a related component or hook.",
|
|
31
|
+
async execute(input) {
|
|
32
|
+
const { path } = InputSchema.parse(input);
|
|
33
|
+
const source = await readFile(resolve(path), "utf-8");
|
|
34
|
+
return {
|
|
35
|
+
componentName: extractComponentName(source, path),
|
|
36
|
+
propsInterface: extractPropsInterface(source),
|
|
37
|
+
hooksUsed: extractHooksUsed(source),
|
|
38
|
+
namedExports: extractNamedExports(source),
|
|
39
|
+
hasDefaultExport: /export\s+default\s+/.test(source)
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// ../skills/src/frontend/analyze-module-structure.ts
|
|
45
|
+
import { readdir } from "fs/promises";
|
|
46
|
+
import { join } from "path";
|
|
47
|
+
import { z as z2 } from "zod";
|
|
48
|
+
var InputSchema2 = z2.object({
|
|
49
|
+
directory: z2.string().describe("Root directory of the React module or feature folder")
|
|
50
|
+
});
|
|
51
|
+
async function collectFiles(dir) {
|
|
52
|
+
const entries = await readdir(dir, { withFileTypes: true }).catch(() => null);
|
|
53
|
+
if (!entries) return [];
|
|
54
|
+
const files = [];
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
if (entry.name === "node_modules" || entry.name === "dist") continue;
|
|
57
|
+
const fullPath = join(dir, entry.name);
|
|
58
|
+
if (entry.isDirectory()) {
|
|
59
|
+
files.push(...await collectFiles(fullPath));
|
|
60
|
+
} else if (entry.isFile() && /\.(tsx?|jsx?)$/.test(entry.name)) {
|
|
61
|
+
files.push(fullPath);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return files;
|
|
65
|
+
}
|
|
66
|
+
function categorize(files) {
|
|
67
|
+
const result = { components: [], hooks: [], pages: [], stores: [], indexFiles: [] };
|
|
68
|
+
for (const f of files) {
|
|
69
|
+
const name = f.split(/[/\\]/).pop() ?? "";
|
|
70
|
+
if (name.includes(".spec.") || name.includes(".test.")) continue;
|
|
71
|
+
if (name === "index.ts" || name === "index.tsx") result.indexFiles.push(f);
|
|
72
|
+
else if (name.startsWith("use")) result.hooks.push(f);
|
|
73
|
+
else if (/page|route/i.test(name)) result.pages.push(f);
|
|
74
|
+
else if (/store|context|provider/i.test(name)) result.stores.push(f);
|
|
75
|
+
else if (/^[A-Z]/.test(name)) result.components.push(f);
|
|
76
|
+
}
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
var analyzeModuleStructureSkill = {
|
|
80
|
+
name: "analyze-module-structure",
|
|
81
|
+
description: "Map the file structure of a React module directory: lists components, hooks, pages, stores and index files. Use this before creating a new feature module to understand the existing layout and avoid duplicating files.",
|
|
82
|
+
async execute(input) {
|
|
83
|
+
const { directory } = InputSchema2.parse(input);
|
|
84
|
+
const files = await collectFiles(directory);
|
|
85
|
+
return categorize(files);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// ../skills/src/frontend/find-component-pattern.ts
|
|
90
|
+
import { readdir as readdir2, readFile as readFile2 } from "fs/promises";
|
|
91
|
+
import { join as join2 } from "path";
|
|
92
|
+
import { z as z3 } from "zod";
|
|
93
|
+
var patternFilters = {
|
|
94
|
+
component: (name) => /^[A-Z]/.test(name) && (name.endsWith(".tsx") || name.endsWith(".ts")) && !name.includes(".spec."),
|
|
95
|
+
hook: (name) => name.startsWith("use") && (name.endsWith(".ts") || name.endsWith(".tsx")) && !name.includes(".spec."),
|
|
96
|
+
page: (name) => /page|route/i.test(name) && (name.endsWith(".tsx") || name.endsWith(".ts")) && !name.includes(".spec."),
|
|
97
|
+
module: (name) => (name === "index.ts" || name === "index.tsx" || /module/i.test(name)) && !name.includes(".spec.")
|
|
98
|
+
};
|
|
99
|
+
var InputSchema3 = z3.object({
|
|
100
|
+
patternType: z3.enum(["component", "hook", "page", "module"]).describe("Type of React pattern to find examples of"),
|
|
101
|
+
directory: z3.string().describe("Root directory to search in"),
|
|
102
|
+
maxExamples: z3.number().int().positive().default(3).describe("Maximum number of examples to return")
|
|
103
|
+
});
|
|
104
|
+
async function walkForComponent(dir, filter, results, max) {
|
|
105
|
+
if (results.length >= max) return;
|
|
106
|
+
const entries = await readdir2(dir, { withFileTypes: true }).catch(() => null);
|
|
107
|
+
if (!entries) return;
|
|
108
|
+
for (const entry of entries) {
|
|
109
|
+
if (results.length >= max) break;
|
|
110
|
+
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === ".git") continue;
|
|
111
|
+
const fullPath = join2(dir, entry.name);
|
|
112
|
+
if (entry.isDirectory()) {
|
|
113
|
+
await walkForComponent(fullPath, filter, results, max);
|
|
114
|
+
} else if (entry.isFile() && filter(entry.name)) {
|
|
115
|
+
const content = await readFile2(fullPath, "utf-8");
|
|
116
|
+
results.push({ file: fullPath, content });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
var findComponentPatternSkill = {
|
|
121
|
+
name: "find-component-pattern",
|
|
122
|
+
description: "Find real examples of React patterns (component, hook, page, module) from the codebase. Use this before generating new UI code to understand the exact naming conventions, file structure and import patterns already in use.",
|
|
123
|
+
async execute(input) {
|
|
124
|
+
const { patternType, directory, maxExamples } = InputSchema3.parse(input);
|
|
125
|
+
const filter = patternFilters[patternType];
|
|
126
|
+
const results = [];
|
|
127
|
+
await walkForComponent(directory, filter, results, maxExamples);
|
|
128
|
+
return results;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export {
|
|
133
|
+
analyzeComponentSkill,
|
|
134
|
+
analyzeModuleStructureSkill,
|
|
135
|
+
findComponentPatternSkill
|
|
136
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// ../skills/src/fs/list-directory.ts
|
|
2
|
+
import { readdir } from "fs/promises";
|
|
3
|
+
import { join, resolve, sep } from "path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
var InputSchema = z.object({
|
|
6
|
+
path: z.string().describe("Directory path to list"),
|
|
7
|
+
recursive: z.boolean().default(false).describe("Whether to list files in subdirectories recursively"),
|
|
8
|
+
extensions: z.array(z.string()).optional().describe('Filter results by file extension, e.g. [".ts", ".tsx"]')
|
|
9
|
+
});
|
|
10
|
+
function assertSafePath(inputPath) {
|
|
11
|
+
const cwd = process.cwd();
|
|
12
|
+
const resolved = resolve(cwd, inputPath);
|
|
13
|
+
const cwdWithSep = cwd.endsWith(sep) ? cwd : cwd + sep;
|
|
14
|
+
if (resolved !== cwd && !resolved.startsWith(cwdWithSep)) {
|
|
15
|
+
throw new Error(`Access denied: "${inputPath}" resolves outside the working directory`);
|
|
16
|
+
}
|
|
17
|
+
return resolved;
|
|
18
|
+
}
|
|
19
|
+
async function walk(dir, recursive, extensions) {
|
|
20
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
21
|
+
const results = [];
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
const fullPath = join(dir, entry.name);
|
|
24
|
+
if (entry.isDirectory()) {
|
|
25
|
+
if (recursive) {
|
|
26
|
+
results.push(...await walk(fullPath, recursive, extensions));
|
|
27
|
+
}
|
|
28
|
+
} else if (entry.isFile()) {
|
|
29
|
+
const include = !extensions || extensions.some((ext) => entry.name.endsWith(ext));
|
|
30
|
+
if (include) results.push(fullPath);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return results;
|
|
34
|
+
}
|
|
35
|
+
var listDirectorySkill = {
|
|
36
|
+
name: "list-directory",
|
|
37
|
+
description: "List files in a directory. Use this to discover existing components, pages or hooks before creating new ones, avoiding duplicates.",
|
|
38
|
+
async execute(input) {
|
|
39
|
+
const { path, recursive, extensions } = InputSchema.parse(input);
|
|
40
|
+
const safePath = assertSafePath(path);
|
|
41
|
+
return await walk(safePath, recursive, extensions);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// ../skills/src/fs/read-file.ts
|
|
46
|
+
import { readFile } from "fs/promises";
|
|
47
|
+
import { resolve as resolve2, sep as sep2 } from "path";
|
|
48
|
+
import { z as z2 } from "zod";
|
|
49
|
+
var InputSchema2 = z2.object({
|
|
50
|
+
path: z2.string().describe("Absolute or relative path to the file to read")
|
|
51
|
+
});
|
|
52
|
+
function assertSafePath2(inputPath) {
|
|
53
|
+
const cwd = process.cwd();
|
|
54
|
+
const resolved = resolve2(cwd, inputPath);
|
|
55
|
+
const cwdWithSep = cwd.endsWith(sep2) ? cwd : cwd + sep2;
|
|
56
|
+
if (resolved !== cwd && !resolved.startsWith(cwdWithSep)) {
|
|
57
|
+
throw new Error(`Access denied: "${inputPath}" resolves outside the working directory`);
|
|
58
|
+
}
|
|
59
|
+
return resolved;
|
|
60
|
+
}
|
|
61
|
+
var readFileSkill = {
|
|
62
|
+
name: "read-file",
|
|
63
|
+
description: "Read the full text content of a file. Use this to inspect existing components, hooks, pages, config files or any source file before generating new code.",
|
|
64
|
+
async execute(input) {
|
|
65
|
+
const { path } = InputSchema2.parse(input);
|
|
66
|
+
const safePath = assertSafePath2(path);
|
|
67
|
+
return await readFile(safePath, "utf-8");
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// ../skills/src/fs/write-file.ts
|
|
72
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
73
|
+
import { dirname, resolve as resolve3, sep as sep3 } from "path";
|
|
74
|
+
import { z as z3 } from "zod";
|
|
75
|
+
var InputSchema3 = z3.object({
|
|
76
|
+
path: z3.string().describe("Absolute or relative path where the file will be written"),
|
|
77
|
+
content: z3.string().describe("Full text content to write"),
|
|
78
|
+
createDirs: z3.boolean().default(true).describe("Create intermediate directories if they do not exist")
|
|
79
|
+
});
|
|
80
|
+
function assertSafePath3(inputPath) {
|
|
81
|
+
const cwd = process.cwd();
|
|
82
|
+
const resolved = resolve3(cwd, inputPath);
|
|
83
|
+
const cwdWithSep = cwd.endsWith(sep3) ? cwd : cwd + sep3;
|
|
84
|
+
if (resolved !== cwd && !resolved.startsWith(cwdWithSep)) {
|
|
85
|
+
throw new Error(`Access denied: "${inputPath}" resolves outside the working directory`);
|
|
86
|
+
}
|
|
87
|
+
return resolved;
|
|
88
|
+
}
|
|
89
|
+
var writeFileSkill = {
|
|
90
|
+
name: "write-file",
|
|
91
|
+
description: "Write or overwrite a file with the given content. Use this to create new components, hooks, pages or any source file after generating the code.",
|
|
92
|
+
async execute(input) {
|
|
93
|
+
const { path, content, createDirs } = InputSchema3.parse(input);
|
|
94
|
+
const safePath = assertSafePath3(path);
|
|
95
|
+
if (createDirs) {
|
|
96
|
+
await mkdir(dirname(safePath), { recursive: true });
|
|
97
|
+
}
|
|
98
|
+
await writeFile(safePath, content, "utf-8");
|
|
99
|
+
return `Written: ${safePath}`;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export {
|
|
104
|
+
listDirectorySkill,
|
|
105
|
+
readFileSkill,
|
|
106
|
+
writeFileSkill
|
|
107
|
+
};
|