@illicomedia/skills-mcp-server 1.1.0 → 1.1.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/dist/audit.d.ts +1 -0
- package/dist/audit.js +495 -41
- package/dist/executor.js +16 -3
- package/dist/installer.d.ts +12 -2
- package/dist/installer.js +66 -20
- package/dist/tools.js +22 -5
- package/package.json +1 -1
package/dist/audit.d.ts
CHANGED
|
@@ -19,5 +19,6 @@ export interface AuditReport {
|
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
21
|
* Checks that the files and scripts expected by Fredo QA Skills are present in a project.
|
|
22
|
+
* Context-aware across stacks (Rust, Go, Python, C/C++, Java, PHP, Node.js) and project types.
|
|
22
23
|
*/
|
|
23
24
|
export declare function runComplianceAudit(projectPath: string): AuditReport;
|
package/dist/audit.js
CHANGED
|
@@ -2,13 +2,40 @@
|
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
import * as fs from 'node:fs';
|
|
4
4
|
import * as path from 'node:path';
|
|
5
|
-
const AUDIT_SCOPE = '
|
|
5
|
+
const AUDIT_SCOPE = 'Fast preflight sanity check: verifies presence of scaffolding, agent directives, and baseline hygiene. Gates are not executed and compilers are not run.';
|
|
6
6
|
/**
|
|
7
7
|
* Checks that the files and scripts expected by Fredo QA Skills are present in a project.
|
|
8
|
+
* Context-aware across stacks (Rust, Go, Python, C/C++, Java, PHP, Node.js) and project types.
|
|
8
9
|
*/
|
|
9
10
|
export function runComplianceAudit(projectPath) {
|
|
10
11
|
const resolvedPath = path.resolve(projectPath);
|
|
12
|
+
const parentPath = path.dirname(resolvedPath);
|
|
11
13
|
const results = [];
|
|
14
|
+
const fileExists = (rel) => fs.existsSync(path.join(resolvedPath, rel));
|
|
15
|
+
const parentFileExists = (rel) => resolvedPath !== parentPath && fs.existsSync(path.join(parentPath, rel));
|
|
16
|
+
const hasDir = (rel) => {
|
|
17
|
+
try {
|
|
18
|
+
const p = path.join(resolvedPath, rel);
|
|
19
|
+
return fs.existsSync(p) && fs.statSync(p).isDirectory();
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
// Stack detection
|
|
26
|
+
const isRust = fileExists('Cargo.toml');
|
|
27
|
+
const isGo = fileExists('go.mod');
|
|
28
|
+
const isNode = fileExists('package.json');
|
|
29
|
+
const isPython = fileExists('pyproject.toml') ||
|
|
30
|
+
fileExists('requirements.txt') ||
|
|
31
|
+
fileExists('setup.py') ||
|
|
32
|
+
fileExists('Pipfile');
|
|
33
|
+
const hasMakefile = fileExists('Makefile') || fileExists('justfile') || fileExists('scripts/qa.sh');
|
|
34
|
+
const isWebProject = hasDir('public') ||
|
|
35
|
+
fileExists('index.html') ||
|
|
36
|
+
fileExists('src/index.html') ||
|
|
37
|
+
hasDir('dist') ||
|
|
38
|
+
hasDir('www');
|
|
12
39
|
// Check 1: AGENTS.md policy presence
|
|
13
40
|
const agentsMd = path.join(resolvedPath, 'AGENTS.md');
|
|
14
41
|
const hasAgentsMd = fs.existsSync(agentsMd);
|
|
@@ -21,80 +48,484 @@ export function runComplianceAudit(projectPath) {
|
|
|
21
48
|
? 'AGENTS.md file present (content not evaluated).'
|
|
22
49
|
: 'AGENTS.md not found. Coding agents have no repository-level QA instructions.',
|
|
23
50
|
});
|
|
24
|
-
// Check 2: .qa-exceptions governance
|
|
51
|
+
// Check 2: .qa-exceptions.yaml governance (passes when file exists and unexpired, or project is clean without exceptions)
|
|
25
52
|
const exceptionsFile = path.join(resolvedPath, '.qa-exceptions.yaml');
|
|
26
53
|
const hasExceptions = fs.existsSync(exceptionsFile);
|
|
54
|
+
let exceptionsPassed = true;
|
|
55
|
+
let exceptionsSeverity = hasExceptions ? 'warning' : 'info';
|
|
56
|
+
let exceptionsDetails = 'No .qa-exceptions.yaml required (clean state: 0 exceptions, or handled natively in compiler/linter config).';
|
|
57
|
+
if (hasExceptions) {
|
|
58
|
+
try {
|
|
59
|
+
const content = fs.readFileSync(exceptionsFile, 'utf-8');
|
|
60
|
+
const dateMatches = content.matchAll(/(?:expires_at|expires):\s*["']?([0-9]{4}-[0-9]{2}-[0-9]{2})["']?/g);
|
|
61
|
+
const expiredList = [];
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
for (const match of dateMatches) {
|
|
64
|
+
const expDate = Date.parse(match[1]);
|
|
65
|
+
if (!Number.isNaN(expDate) && expDate < now) {
|
|
66
|
+
expiredList.push(match[1]);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (expiredList.length > 0) {
|
|
70
|
+
exceptionsPassed = false;
|
|
71
|
+
exceptionsSeverity = 'blocker';
|
|
72
|
+
exceptionsDetails = `Exception register contains ${expiredList.length} expired entry(ies) (expired on ${expiredList.join(', ')}). Expired entries fail closed per qa-core.`;
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
exceptionsDetails =
|
|
76
|
+
'Exception register file present (all declared expiration dates are active).';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
exceptionsDetails = 'Exception register file present (schema and expiry not checked).';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
27
83
|
results.push({
|
|
28
84
|
category: 'governance',
|
|
29
85
|
name: '.qa-exceptions.yaml present',
|
|
30
|
-
passed:
|
|
86
|
+
passed: exceptionsPassed,
|
|
87
|
+
severity: exceptionsSeverity,
|
|
88
|
+
details: exceptionsDetails,
|
|
89
|
+
});
|
|
90
|
+
// Check 3: Root LICENSE file presence (Legal Governance)
|
|
91
|
+
const hasLicense = fileExists('LICENSE') ||
|
|
92
|
+
fileExists('LICENSE.md') ||
|
|
93
|
+
fileExists('LICENSE.txt') ||
|
|
94
|
+
fileExists('LICENCE') ||
|
|
95
|
+
fileExists('LICENCE.md') ||
|
|
96
|
+
fileExists('COPYING') ||
|
|
97
|
+
fileExists('COPYING.md') ||
|
|
98
|
+
fileExists('COPYING.txt') ||
|
|
99
|
+
parentFileExists('LICENSE') ||
|
|
100
|
+
parentFileExists('LICENSE.md') ||
|
|
101
|
+
parentFileExists('COPYING');
|
|
102
|
+
results.push({
|
|
103
|
+
category: 'governance',
|
|
104
|
+
name: 'LICENSE file present',
|
|
105
|
+
passed: hasLicense,
|
|
31
106
|
severity: 'warning',
|
|
32
|
-
details:
|
|
33
|
-
? '
|
|
34
|
-
: 'No
|
|
107
|
+
details: hasLicense
|
|
108
|
+
? 'LICENSE file present in project root (license terms not evaluated).'
|
|
109
|
+
: 'No LICENSE, LICENSE.md, or COPYING file found in project root.',
|
|
35
110
|
});
|
|
36
|
-
// Check
|
|
111
|
+
// Check 4: Vulnerability disclosure policy (RFC 9116 security.txt for web, SECURITY.md for offline CLI/libraries)
|
|
37
112
|
const secTxtPublic = path.join(resolvedPath, 'public', '.well-known', 'security.txt');
|
|
38
113
|
const secTxtDist = path.join(resolvedPath, 'dist', '.well-known', 'security.txt');
|
|
39
114
|
const secTxtRoot = path.join(resolvedPath, '.well-known', 'security.txt');
|
|
40
|
-
const hasSecTxt = fs.existsSync(secTxtPublic) ||
|
|
115
|
+
const hasSecTxt = fs.existsSync(secTxtPublic) ||
|
|
116
|
+
fs.existsSync(secTxtDist) ||
|
|
117
|
+
fs.existsSync(secTxtRoot) ||
|
|
118
|
+
fileExists('website/public/.well-known/security.txt') ||
|
|
119
|
+
fileExists('website/dist/.well-known/security.txt') ||
|
|
120
|
+
fileExists('apps/web/public/.well-known/security.txt') ||
|
|
121
|
+
fileExists('client/public/.well-known/security.txt');
|
|
122
|
+
const hasSecurityMd = fileExists('SECURITY.md') || fileExists('.github/SECURITY.md');
|
|
123
|
+
const secCheckName = isWebProject
|
|
124
|
+
? 'security.txt present'
|
|
125
|
+
: hasSecTxt
|
|
126
|
+
? 'security.txt present'
|
|
127
|
+
: 'SECURITY.md present';
|
|
128
|
+
const secPassed = hasSecTxt || (!isWebProject && hasSecurityMd);
|
|
129
|
+
let secDetails = 'No security policy found.';
|
|
130
|
+
if (hasSecTxt) {
|
|
131
|
+
secDetails =
|
|
132
|
+
'security.txt file present in a .well-known/ directory (RFC 9116 fields not checked).';
|
|
133
|
+
}
|
|
134
|
+
else if (hasSecurityMd) {
|
|
135
|
+
secDetails =
|
|
136
|
+
'SECURITY.md file present (standard vulnerability disclosure policy for offline/CLI repositories).';
|
|
137
|
+
}
|
|
138
|
+
else if (isWebProject) {
|
|
139
|
+
secDetails = 'No security.txt found in public/, dist/, or root .well-known/ directories.';
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
secDetails = 'No SECURITY.md found in repository root.';
|
|
143
|
+
}
|
|
41
144
|
results.push({
|
|
42
145
|
category: 'security',
|
|
43
|
-
name:
|
|
44
|
-
passed:
|
|
45
|
-
severity: 'blocker',
|
|
46
|
-
details:
|
|
47
|
-
? 'security.txt file present in a .well-known/ directory (RFC 9116 fields not checked).'
|
|
48
|
-
: 'No security.txt found in public/, dist/, or root .well-known/ directories.',
|
|
146
|
+
name: secCheckName,
|
|
147
|
+
passed: secPassed,
|
|
148
|
+
severity: isWebProject ? 'blocker' : 'warning',
|
|
149
|
+
details: secDetails,
|
|
49
150
|
});
|
|
50
|
-
// Check
|
|
151
|
+
// Check 5: Safe gitignore configuration for .env
|
|
51
152
|
const gitignore = path.join(resolvedPath, '.gitignore');
|
|
52
153
|
const hasGitignore = fs.existsSync(gitignore);
|
|
53
|
-
let
|
|
154
|
+
let gitignoreContent = '';
|
|
54
155
|
if (hasGitignore) {
|
|
55
|
-
|
|
56
|
-
|
|
156
|
+
try {
|
|
157
|
+
gitignoreContent = fs.readFileSync(gitignore, 'utf-8');
|
|
158
|
+
}
|
|
159
|
+
catch { }
|
|
160
|
+
}
|
|
161
|
+
else if (parentFileExists('.gitignore')) {
|
|
162
|
+
try {
|
|
163
|
+
gitignoreContent = fs.readFileSync(path.join(parentPath, '.gitignore'), 'utf-8');
|
|
164
|
+
}
|
|
165
|
+
catch { }
|
|
166
|
+
}
|
|
167
|
+
const hasEnvFiles = fileExists('.env') ||
|
|
168
|
+
fileExists('.env.local') ||
|
|
169
|
+
fileExists('.env.prod') ||
|
|
170
|
+
fileExists('.env.example');
|
|
171
|
+
const ignoresEnv = gitignoreContent.includes('.env');
|
|
172
|
+
const envPassed = ignoresEnv;
|
|
173
|
+
let envSeverity = 'warning';
|
|
174
|
+
let envDetails = '';
|
|
175
|
+
if (hasEnvFiles || isWebProject) {
|
|
176
|
+
envSeverity = 'blocker';
|
|
177
|
+
envDetails = ignoresEnv
|
|
178
|
+
? '.gitignore contains the text ".env" (ignore rules and other secret files not evaluated).'
|
|
179
|
+
: '.gitignore missing or does not mention .env (also ignore private keys and credentials).';
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
// Offline / CLI binary or non-web project without env files:
|
|
183
|
+
envSeverity = 'warning';
|
|
184
|
+
envDetails = ignoresEnv
|
|
185
|
+
? '.gitignore contains the text ".env".'
|
|
186
|
+
: '.gitignore does not mention .env (recommended practice even for offline projects to prevent accidental secret commits).';
|
|
57
187
|
}
|
|
58
188
|
results.push({
|
|
59
189
|
category: 'security',
|
|
60
190
|
name: '.gitignore mentions .env',
|
|
61
|
-
passed:
|
|
62
|
-
severity:
|
|
63
|
-
details:
|
|
64
|
-
? '.gitignore contains the text ".env" (ignore rules and other secret files not evaluated).'
|
|
65
|
-
: '.gitignore missing or does not mention .env (also ignore private keys and credentials).',
|
|
191
|
+
passed: envPassed,
|
|
192
|
+
severity: envSeverity,
|
|
193
|
+
details: envDetails,
|
|
66
194
|
});
|
|
67
|
-
// Check
|
|
68
|
-
const
|
|
195
|
+
// Check 6: Private key and secret hygiene in .gitignore
|
|
196
|
+
const keyIgnoreRegex = /(?:\*\.(?:pem|key|p12|pkcs12)|id_rsa|id_ed25519|\.aws)/;
|
|
197
|
+
const ignoresKeys = keyIgnoreRegex.test(gitignoreContent);
|
|
198
|
+
let hasRawKeyFile = false;
|
|
199
|
+
try {
|
|
200
|
+
const rootEntries = fs.readdirSync(resolvedPath);
|
|
201
|
+
for (const entry of rootEntries) {
|
|
202
|
+
if ((entry.endsWith('.pem') ||
|
|
203
|
+
entry.endsWith('.key') ||
|
|
204
|
+
entry.endsWith('.p12') ||
|
|
205
|
+
entry.endsWith('.pkcs12') ||
|
|
206
|
+
entry === 'id_rsa' ||
|
|
207
|
+
entry === 'id_ed25519') &&
|
|
208
|
+
!ignoresKeys) {
|
|
209
|
+
hasRawKeyFile = true;
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
catch { }
|
|
215
|
+
let keyPassed = true;
|
|
216
|
+
let keySeverity = 'warning';
|
|
217
|
+
let keyDetails = '';
|
|
218
|
+
if (hasRawKeyFile) {
|
|
219
|
+
keyPassed = false;
|
|
220
|
+
keySeverity = 'blocker';
|
|
221
|
+
keyDetails = 'Raw private key or certificate file detected in workspace root.';
|
|
222
|
+
}
|
|
223
|
+
else if (ignoresKeys) {
|
|
224
|
+
keyPassed = true;
|
|
225
|
+
keySeverity = 'warning';
|
|
226
|
+
keyDetails = '.gitignore contains rules for private key and credential files.';
|
|
227
|
+
}
|
|
228
|
+
else if (isWebProject || hasEnvFiles || isNode) {
|
|
229
|
+
keyPassed = false;
|
|
230
|
+
keySeverity = 'warning';
|
|
231
|
+
keyDetails =
|
|
232
|
+
'.gitignore does not contain explicit rules ignoring private key files (*.pem, *.key, *.p12).';
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
keyPassed = true;
|
|
236
|
+
keySeverity = 'info';
|
|
237
|
+
keyDetails =
|
|
238
|
+
'No private keys detected; explicit key ignore rules recommended for multi-contributor repositories.';
|
|
239
|
+
}
|
|
240
|
+
results.push({
|
|
241
|
+
category: 'security',
|
|
242
|
+
name: 'private key and secret hygiene in .gitignore',
|
|
243
|
+
passed: keyPassed,
|
|
244
|
+
severity: keySeverity,
|
|
245
|
+
details: keyDetails,
|
|
246
|
+
});
|
|
247
|
+
// Check 7: Canonical QA scripts / quality gates
|
|
69
248
|
let hasQaStandard = false;
|
|
70
|
-
let
|
|
71
|
-
|
|
249
|
+
let qaStandardDetails = '';
|
|
250
|
+
let qaCheckName = 'qa:starter or qa:standard script declared';
|
|
251
|
+
if (isNode) {
|
|
252
|
+
const pkgJsonPath = path.join(resolvedPath, 'package.json');
|
|
72
253
|
try {
|
|
73
254
|
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
74
|
-
hasQaStandard = Boolean(pkg.scripts?.['qa:standard'] ||
|
|
75
|
-
|
|
255
|
+
hasQaStandard = Boolean(pkg.scripts?.['qa:standard'] ||
|
|
256
|
+
pkg.scripts?.['qa:starter'] ||
|
|
257
|
+
pkg.scripts?.['test']);
|
|
76
258
|
}
|
|
77
259
|
catch {
|
|
78
260
|
// Ignored
|
|
79
261
|
}
|
|
262
|
+
qaStandardDetails = hasQaStandard
|
|
263
|
+
? 'package.json declares qa:starter or qa:standard (script not executed).'
|
|
264
|
+
: 'No qa:starter / qa:standard script in package.json.';
|
|
265
|
+
}
|
|
266
|
+
else if (isRust) {
|
|
267
|
+
hasQaStandard = true;
|
|
268
|
+
qaCheckName = 'qa:starter / cargo quality gates declared';
|
|
269
|
+
qaStandardDetails =
|
|
270
|
+
'Rust project detected (Cargo.toml). Canonical QA available via cargo test and cargo clippy.';
|
|
271
|
+
}
|
|
272
|
+
else if (isGo) {
|
|
273
|
+
hasQaStandard = true;
|
|
274
|
+
qaCheckName = 'qa:starter / go quality gates declared';
|
|
275
|
+
qaStandardDetails =
|
|
276
|
+
'Go project detected (go.mod). Canonical QA available via go test and golangci-lint.';
|
|
277
|
+
}
|
|
278
|
+
else if (isPython) {
|
|
279
|
+
hasQaStandard = fileExists('pyproject.toml') || fileExists('tox.ini') || hasMakefile;
|
|
280
|
+
qaCheckName = 'qa:starter / python quality gates declared';
|
|
281
|
+
qaStandardDetails = hasQaStandard
|
|
282
|
+
? 'Python project detected. QA configured via pyproject.toml / test runner.'
|
|
283
|
+
: 'No test runner or pyproject.toml found for Python project.';
|
|
284
|
+
}
|
|
285
|
+
else if (hasMakefile) {
|
|
286
|
+
hasQaStandard = true;
|
|
287
|
+
qaCheckName = 'qa:starter / makefile quality gates declared';
|
|
288
|
+
qaStandardDetails = 'Makefile / scripts/qa.sh detected for canonical quality gate execution.';
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
hasQaStandard = false;
|
|
292
|
+
qaStandardDetails = 'No qa:starter / qa:standard script in package.json.';
|
|
80
293
|
}
|
|
81
294
|
results.push({
|
|
82
295
|
category: 'quality',
|
|
83
|
-
name:
|
|
296
|
+
name: qaCheckName,
|
|
84
297
|
passed: hasQaStandard,
|
|
85
298
|
severity: 'blocker',
|
|
86
|
-
details:
|
|
87
|
-
? 'package.json declares qa:starter or qa:standard (script not executed).'
|
|
88
|
-
: 'No qa:starter / qa:standard script in package.json.',
|
|
299
|
+
details: qaStandardDetails,
|
|
89
300
|
});
|
|
301
|
+
// Check 8: Static typing / static analysis / compiler checks
|
|
302
|
+
let hasTypeCheck = false;
|
|
303
|
+
let typeCheckDetails = '';
|
|
304
|
+
let typeCheckName = 'type-check script declared';
|
|
305
|
+
if (isNode) {
|
|
306
|
+
const pkgJsonPath = path.join(resolvedPath, 'package.json');
|
|
307
|
+
try {
|
|
308
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
309
|
+
hasTypeCheck = Boolean(pkg.scripts?.['type-check'] ||
|
|
310
|
+
pkg.scripts?.['tsc'] ||
|
|
311
|
+
pkg.scripts?.['check'] ||
|
|
312
|
+
fileExists('tsconfig.json'));
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
// Ignored
|
|
316
|
+
}
|
|
317
|
+
typeCheckDetails = hasTypeCheck
|
|
318
|
+
? 'package.json declares a type-check script (script not executed).'
|
|
319
|
+
: 'No type-check or tsc script in package.json.';
|
|
320
|
+
}
|
|
321
|
+
else if (isRust) {
|
|
322
|
+
hasTypeCheck = true;
|
|
323
|
+
typeCheckName = 'compile-time type safety & borrow checking (rustc)';
|
|
324
|
+
typeCheckDetails = 'Rust compiler enforces compile-time type safety and borrow checking.';
|
|
325
|
+
}
|
|
326
|
+
else if (isGo) {
|
|
327
|
+
hasTypeCheck = true;
|
|
328
|
+
typeCheckName = 'compile-time type safety (go build)';
|
|
329
|
+
typeCheckDetails = 'Go compiler enforces compile-time type safety.';
|
|
330
|
+
}
|
|
331
|
+
else if (isPython) {
|
|
332
|
+
hasTypeCheck = fileExists('mypy.ini') || fileExists('pyproject.toml');
|
|
333
|
+
typeCheckName = 'static typing / type-check configured';
|
|
334
|
+
typeCheckDetails = hasTypeCheck
|
|
335
|
+
? 'Python type checking or linter configuration detected.'
|
|
336
|
+
: 'No mypy.ini or type checking configuration detected.';
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
hasTypeCheck = hasQaStandard;
|
|
340
|
+
typeCheckDetails = hasTypeCheck
|
|
341
|
+
? 'Static analysis provided by build system.'
|
|
342
|
+
: 'No type-check or tsc script in package.json.';
|
|
343
|
+
}
|
|
90
344
|
results.push({
|
|
91
345
|
category: 'quality',
|
|
92
|
-
name:
|
|
346
|
+
name: typeCheckName,
|
|
93
347
|
passed: hasTypeCheck,
|
|
94
348
|
severity: 'blocker',
|
|
95
|
-
details:
|
|
96
|
-
|
|
97
|
-
|
|
349
|
+
details: typeCheckDetails,
|
|
350
|
+
});
|
|
351
|
+
// Check 9: Automated test suite declared
|
|
352
|
+
let hasTestSuite = false;
|
|
353
|
+
let testSuiteDetails = '';
|
|
354
|
+
if (isNode) {
|
|
355
|
+
const pkgJsonPath = path.join(resolvedPath, 'package.json');
|
|
356
|
+
let hasTestScript = false;
|
|
357
|
+
try {
|
|
358
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
359
|
+
hasTestScript = Boolean(pkg.scripts?.test ||
|
|
360
|
+
pkg.scripts?.['test:unit'] ||
|
|
361
|
+
pkg.scripts?.['test:all'] ||
|
|
362
|
+
pkg.scripts?.['test:e2e'] ||
|
|
363
|
+
pkg.scripts?.['test:run']);
|
|
364
|
+
}
|
|
365
|
+
catch { }
|
|
366
|
+
const hasTestConfigFile = fileExists('vitest.config.ts') ||
|
|
367
|
+
fileExists('vitest.config.js') ||
|
|
368
|
+
fileExists('vitest.config.mjs') ||
|
|
369
|
+
fileExists('jest.config.js') ||
|
|
370
|
+
fileExists('jest.config.ts') ||
|
|
371
|
+
fileExists('playwright.config.ts');
|
|
372
|
+
hasTestSuite = hasTestScript || hasTestConfigFile || hasDir('tests') || hasDir('test');
|
|
373
|
+
testSuiteDetails = hasTestSuite
|
|
374
|
+
? 'package.json declares test script or test runner configuration (tests not executed).'
|
|
375
|
+
: 'No test runner script or test config declared in package.json.';
|
|
376
|
+
}
|
|
377
|
+
else if (isRust) {
|
|
378
|
+
hasTestSuite = true;
|
|
379
|
+
testSuiteDetails =
|
|
380
|
+
'Rust test framework declared via cargo test / Cargo.toml (tests not executed).';
|
|
381
|
+
}
|
|
382
|
+
else if (isGo) {
|
|
383
|
+
hasTestSuite = true;
|
|
384
|
+
testSuiteDetails = 'Go test framework declared via go test / go.mod (tests not executed).';
|
|
385
|
+
}
|
|
386
|
+
else if (isPython) {
|
|
387
|
+
hasTestSuite =
|
|
388
|
+
fileExists('pytest.ini') ||
|
|
389
|
+
fileExists('tox.ini') ||
|
|
390
|
+
fileExists('setup.cfg') ||
|
|
391
|
+
hasDir('tests') ||
|
|
392
|
+
hasDir('test');
|
|
393
|
+
testSuiteDetails = hasTestSuite
|
|
394
|
+
? 'Python test runner configuration or tests/ directory detected (tests not executed).'
|
|
395
|
+
: 'No pytest.ini, tox.ini, or tests/ directory found for Python project.';
|
|
396
|
+
}
|
|
397
|
+
else if (fileExists('composer.json')) {
|
|
398
|
+
hasTestSuite =
|
|
399
|
+
fileExists('phpunit.xml') ||
|
|
400
|
+
fileExists('phpunit.xml.dist') ||
|
|
401
|
+
fileExists('pest.php') ||
|
|
402
|
+
hasDir('tests');
|
|
403
|
+
testSuiteDetails = hasTestSuite
|
|
404
|
+
? 'PHPUnit or Pest test configuration detected (tests not executed).'
|
|
405
|
+
: 'No phpunit.xml or tests/ directory found for PHP project.';
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
hasTestSuite =
|
|
409
|
+
hasDir('tests') ||
|
|
410
|
+
hasDir('test') ||
|
|
411
|
+
fileExists('scripts/test.sh') ||
|
|
412
|
+
(hasMakefile &&
|
|
413
|
+
fileExists('Makefile') &&
|
|
414
|
+
fs.readFileSync(path.join(resolvedPath, 'Makefile'), 'utf-8').includes('test:'));
|
|
415
|
+
testSuiteDetails = hasTestSuite
|
|
416
|
+
? 'Test suite configuration or tests/ directory detected (tests not executed).'
|
|
417
|
+
: 'No automated test runner script or tests/ directory detected.';
|
|
418
|
+
}
|
|
419
|
+
results.push({
|
|
420
|
+
category: 'quality',
|
|
421
|
+
name: 'automated test suite declared',
|
|
422
|
+
passed: hasTestSuite,
|
|
423
|
+
severity: 'blocker',
|
|
424
|
+
details: testSuiteDetails,
|
|
425
|
+
});
|
|
426
|
+
// Check 10: Dependency lockfile presence
|
|
427
|
+
let hasLockfile = false;
|
|
428
|
+
let lockfileDetails = '';
|
|
429
|
+
let lockfileSeverity = 'warning';
|
|
430
|
+
const hasManifest = isNode || isRust || isGo || isPython || fileExists('composer.json') || fileExists('pom.xml');
|
|
431
|
+
if (isNode) {
|
|
432
|
+
hasLockfile =
|
|
433
|
+
fileExists('pnpm-lock.yaml') ||
|
|
434
|
+
fileExists('package-lock.json') ||
|
|
435
|
+
fileExists('yarn.lock') ||
|
|
436
|
+
fileExists('bun.lockb') ||
|
|
437
|
+
parentFileExists('pnpm-lock.yaml') ||
|
|
438
|
+
parentFileExists('package-lock.json');
|
|
439
|
+
lockfileDetails = hasLockfile
|
|
440
|
+
? 'Dependency lockfile present for reproducible dependency resolution.'
|
|
441
|
+
: 'Node package.json detected without committed lockfile (pnpm-lock.yaml, package-lock.json).';
|
|
442
|
+
}
|
|
443
|
+
else if (isRust) {
|
|
444
|
+
hasLockfile = fileExists('Cargo.lock');
|
|
445
|
+
lockfileDetails = hasLockfile
|
|
446
|
+
? 'Cargo.lock present for reproducible dependency resolution.'
|
|
447
|
+
: 'Cargo.lock missing. Binaries and end-user artifacts should commit Cargo.lock.';
|
|
448
|
+
}
|
|
449
|
+
else if (isGo) {
|
|
450
|
+
hasLockfile = fileExists('go.sum');
|
|
451
|
+
lockfileDetails = hasLockfile
|
|
452
|
+
? 'go.sum present for reproducible dependency resolution.'
|
|
453
|
+
: 'go.sum missing for Go module dependency resolution.';
|
|
454
|
+
}
|
|
455
|
+
else if (isPython) {
|
|
456
|
+
hasLockfile =
|
|
457
|
+
fileExists('poetry.lock') ||
|
|
458
|
+
fileExists('Pipfile.lock') ||
|
|
459
|
+
fileExists('uv.lock') ||
|
|
460
|
+
fileExists('pdm.lock') ||
|
|
461
|
+
fileExists('requirements.lock');
|
|
462
|
+
lockfileDetails = hasLockfile
|
|
463
|
+
? 'Python dependency lockfile present for reproducible dependency resolution.'
|
|
464
|
+
: 'No Python dependency lockfile found (poetry.lock, uv.lock, Pipfile.lock).';
|
|
465
|
+
}
|
|
466
|
+
else if (fileExists('composer.json')) {
|
|
467
|
+
hasLockfile = fileExists('composer.lock');
|
|
468
|
+
lockfileDetails = hasLockfile
|
|
469
|
+
? 'composer.lock present for reproducible dependency resolution.'
|
|
470
|
+
: 'composer.lock missing for PHP Composer project.';
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
hasLockfile = true;
|
|
474
|
+
lockfileSeverity = 'info';
|
|
475
|
+
lockfileDetails = 'No dependency manifest detected; lockfile requirement not applicable.';
|
|
476
|
+
}
|
|
477
|
+
results.push({
|
|
478
|
+
category: 'supply-chain',
|
|
479
|
+
name: 'dependency lockfile present',
|
|
480
|
+
passed: hasLockfile,
|
|
481
|
+
severity: lockfileSeverity,
|
|
482
|
+
details: lockfileDetails,
|
|
483
|
+
});
|
|
484
|
+
// Check 11: Continuous integration workflow definition
|
|
485
|
+
const checkCiDir = (dirRel) => {
|
|
486
|
+
try {
|
|
487
|
+
const dirPath = path.join(resolvedPath, dirRel);
|
|
488
|
+
if (!fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory())
|
|
489
|
+
return false;
|
|
490
|
+
const files = fs.readdirSync(dirPath);
|
|
491
|
+
return files.some((f) => f.endsWith('.yml') || f.endsWith('.yaml'));
|
|
492
|
+
}
|
|
493
|
+
catch {
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
const checkParentCiDir = (dirRel) => {
|
|
498
|
+
try {
|
|
499
|
+
if (resolvedPath === parentPath)
|
|
500
|
+
return false;
|
|
501
|
+
const dirPath = path.join(parentPath, dirRel);
|
|
502
|
+
if (!fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory())
|
|
503
|
+
return false;
|
|
504
|
+
const files = fs.readdirSync(dirPath);
|
|
505
|
+
return files.some((f) => f.endsWith('.yml') || f.endsWith('.yaml'));
|
|
506
|
+
}
|
|
507
|
+
catch {
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
const hasCiWorkflow = checkCiDir('.github/workflows') ||
|
|
512
|
+
checkParentCiDir('.github/workflows') ||
|
|
513
|
+
fileExists('.gitlab-ci.yml') ||
|
|
514
|
+
fileExists('.circleci/config.yml') ||
|
|
515
|
+
fileExists('azure-pipelines.yml') ||
|
|
516
|
+
fileExists('bitbucket-pipelines.yml') ||
|
|
517
|
+
fileExists('Jenkinsfile') ||
|
|
518
|
+
fileExists('.travis.yml') ||
|
|
519
|
+
fileExists('.woodpecker.yml') ||
|
|
520
|
+
fileExists('.drone.yml');
|
|
521
|
+
results.push({
|
|
522
|
+
category: 'devops',
|
|
523
|
+
name: 'CI workflow definition present',
|
|
524
|
+
passed: hasCiWorkflow,
|
|
525
|
+
severity: 'warning',
|
|
526
|
+
details: hasCiWorkflow
|
|
527
|
+
? 'CI workflow definition file present (workflow execution not evaluated).'
|
|
528
|
+
: 'No CI workflow configuration found (.github/workflows, .gitlab-ci.yml, etc.).',
|
|
98
529
|
});
|
|
99
530
|
const totalChecks = results.length;
|
|
100
531
|
const passedChecks = results.filter((r) => r.passed).length;
|
|
@@ -104,11 +535,34 @@ export function runComplianceAudit(projectPath) {
|
|
|
104
535
|
if (!hasAgentsMd) {
|
|
105
536
|
recommendations.push('Create an AGENTS.md in project root enforcing Fredo QA Skills rules for AI assistants.');
|
|
106
537
|
}
|
|
107
|
-
if (!
|
|
108
|
-
recommendations.push('
|
|
538
|
+
if (!exceptionsPassed) {
|
|
539
|
+
recommendations.push('Review and renew or remove expired exceptions in .qa-exceptions.yaml.');
|
|
540
|
+
}
|
|
541
|
+
if (!hasLicense) {
|
|
542
|
+
recommendations.push('Add a LICENSE or COPYING file in project root declaring usage and distribution terms.');
|
|
543
|
+
}
|
|
544
|
+
if (!secPassed) {
|
|
545
|
+
recommendations.push(isWebProject
|
|
546
|
+
? 'Generate public/.well-known/security.txt with RFC 9116 Contact and Expires directives.'
|
|
547
|
+
: 'Add a SECURITY.md file at repository root with responsible vulnerability disclosure instructions.');
|
|
548
|
+
}
|
|
549
|
+
if (!keyPassed && keySeverity === 'blocker') {
|
|
550
|
+
recommendations.push('Remove unignored private key or certificate files from the workspace root immediately.');
|
|
551
|
+
}
|
|
552
|
+
else if (!keyPassed) {
|
|
553
|
+
recommendations.push('Add pattern rules to .gitignore ignoring private key and credential files (*.pem, *.key, *.p12).');
|
|
554
|
+
}
|
|
555
|
+
if (!hasQaStandard && isNode) {
|
|
556
|
+
recommendations.push('Add "qa:starter" and "qa:standard" scripts to package.json for local and CI parity.');
|
|
557
|
+
}
|
|
558
|
+
if (!hasTestSuite) {
|
|
559
|
+
recommendations.push('Declare an automated test runner script in package manifest or provide a tests/ directory.');
|
|
560
|
+
}
|
|
561
|
+
if (!hasLockfile && hasManifest) {
|
|
562
|
+
recommendations.push('Commit a dependency lockfile (e.g. pnpm-lock.yaml, Cargo.lock, go.sum) for reproducible dependency resolution.');
|
|
109
563
|
}
|
|
110
|
-
if (!
|
|
111
|
-
recommendations.push('Add
|
|
564
|
+
if (!hasCiWorkflow) {
|
|
565
|
+
recommendations.push('Add a CI workflow configuration (e.g. .github/workflows/ci.yml) for automated PR validation.');
|
|
112
566
|
}
|
|
113
567
|
return {
|
|
114
568
|
projectPath: resolvedPath,
|
package/dist/executor.js
CHANGED
|
@@ -4,6 +4,7 @@ import { runComplianceAudit } from './audit.js';
|
|
|
4
4
|
import { SKILLS_CATALOG, getAvailableSkills, loadSkillContent } from './catalog.js';
|
|
5
5
|
import { installSkill, syncWorkspaceSkills } from './installer.js';
|
|
6
6
|
import { PRICING_URL, PROJECT_LICENSE_FILE, generateCanaryWatermark, validateLicense, } from './license.js';
|
|
7
|
+
import { SERVER_VERSION } from './version.js';
|
|
7
8
|
export class ToolExecutor {
|
|
8
9
|
static execute(name, args, activeLicenseKey, onLicenseUpdate, licenseOptions = {}) {
|
|
9
10
|
if (args.license_key && typeof args.license_key === 'string') {
|
|
@@ -83,20 +84,32 @@ export class ToolExecutor {
|
|
|
83
84
|
case 'install_skill': {
|
|
84
85
|
const skillId = args.skill_id;
|
|
85
86
|
const projectPath = args.project_path || process.cwd();
|
|
86
|
-
const
|
|
87
|
+
const confirm = Boolean(args.confirm);
|
|
88
|
+
const res = installSkill(skillId, projectPath, license, { confirm });
|
|
87
89
|
return JSON.stringify(res, null, 2);
|
|
88
90
|
}
|
|
89
91
|
case 'sync_workspace_skills': {
|
|
90
92
|
const projectPath = args.project_path || process.cwd();
|
|
91
93
|
const requestedSkills = args.skills;
|
|
92
|
-
const
|
|
94
|
+
const confirm = Boolean(args.confirm);
|
|
95
|
+
const res = syncWorkspaceSkills(projectPath, requestedSkills, license, { confirm });
|
|
93
96
|
return res.summaryMessage;
|
|
94
97
|
}
|
|
95
|
-
case '
|
|
98
|
+
case 'quick_check': {
|
|
96
99
|
const projectPath = args.project_path || process.cwd();
|
|
97
100
|
const report = runComplianceAudit(projectPath);
|
|
98
101
|
return JSON.stringify(report, null, 2);
|
|
99
102
|
}
|
|
103
|
+
case 'get_version': {
|
|
104
|
+
return JSON.stringify({
|
|
105
|
+
version: SERVER_VERSION,
|
|
106
|
+
package: '@illicomedia/skills-mcp-server',
|
|
107
|
+
protocolVersion: '2024-11-05',
|
|
108
|
+
nodeVersion: process.version,
|
|
109
|
+
skillsCount: SKILLS_CATALOG.length,
|
|
110
|
+
licenseTier: license.tier.toUpperCase(),
|
|
111
|
+
}, null, 2);
|
|
112
|
+
}
|
|
100
113
|
default:
|
|
101
114
|
throw new Error(`Unsupported tool: ${name}`);
|
|
102
115
|
}
|
package/dist/installer.d.ts
CHANGED
|
@@ -10,6 +10,14 @@ export interface InstallSkillResult {
|
|
|
10
10
|
requiresAuth?: boolean;
|
|
11
11
|
requiredTier?: SkillTier;
|
|
12
12
|
promptRecommendation?: string;
|
|
13
|
+
previewOnly?: boolean;
|
|
14
|
+
requiresConfirmation?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface InstallSkillOptions {
|
|
17
|
+
confirm?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface SyncWorkspaceOptions {
|
|
20
|
+
confirm?: boolean;
|
|
13
21
|
}
|
|
14
22
|
export interface SyncWorkspaceResult {
|
|
15
23
|
projectPath: string;
|
|
@@ -22,8 +30,9 @@ export interface SyncWorkspaceResult {
|
|
|
22
30
|
/**
|
|
23
31
|
* Installs a specific skill into the workspace .agents/skills/<id>/ folder.
|
|
24
32
|
* Enforces authentication and active entitlement for Pro and Enterprise skills.
|
|
33
|
+
* Requires explicit confirm: true to perform disk writes (safe preview by default).
|
|
25
34
|
*/
|
|
26
|
-
export declare function installSkill(skillId: string, projectPath?: string, licenseOverride?: LicenseValidationResult): InstallSkillResult;
|
|
35
|
+
export declare function installSkill(skillId: string, projectPath?: string, licenseOverride?: LicenseValidationResult, options?: InstallSkillOptions): InstallSkillResult;
|
|
27
36
|
/**
|
|
28
37
|
* Detects relevant skills by inspecting workspace files and indicators
|
|
29
38
|
*/
|
|
@@ -31,5 +40,6 @@ export declare function detectProjectSkills(projectPath?: string): string[];
|
|
|
31
40
|
/**
|
|
32
41
|
* Synchronizes workspace skills: either auto-detects or installs an explicit list.
|
|
33
42
|
* Respects license tiers and reports auth requirements clearly.
|
|
43
|
+
* Requires explicit confirm: true to perform disk writes (safe preview by default).
|
|
34
44
|
*/
|
|
35
|
-
export declare function syncWorkspaceSkills(projectPath?: string, requestedSkills?: string[], licenseOverride?: LicenseValidationResult): SyncWorkspaceResult;
|
|
45
|
+
export declare function syncWorkspaceSkills(projectPath?: string, requestedSkills?: string[], licenseOverride?: LicenseValidationResult, options?: SyncWorkspaceOptions): SyncWorkspaceResult;
|
package/dist/installer.js
CHANGED
|
@@ -7,8 +7,9 @@ import { generateCanaryWatermark, validateLicense } from './license.js';
|
|
|
7
7
|
/**
|
|
8
8
|
* Installs a specific skill into the workspace .agents/skills/<id>/ folder.
|
|
9
9
|
* Enforces authentication and active entitlement for Pro and Enterprise skills.
|
|
10
|
+
* Requires explicit confirm: true to perform disk writes (safe preview by default).
|
|
10
11
|
*/
|
|
11
|
-
export function installSkill(skillId, projectPath = process.cwd(), licenseOverride) {
|
|
12
|
+
export function installSkill(skillId, projectPath = process.cwd(), licenseOverride, options) {
|
|
12
13
|
const meta = findSkill(skillId);
|
|
13
14
|
if (!meta) {
|
|
14
15
|
return {
|
|
@@ -52,6 +53,29 @@ export function installSkill(skillId, projectPath = process.cwd(), licenseOverri
|
|
|
52
53
|
}
|
|
53
54
|
// Installation authorized
|
|
54
55
|
const targetDir = path.join(projectPath, '.agents', 'skills', skillId);
|
|
56
|
+
const skillFilePath = path.join(targetDir, 'SKILL.md');
|
|
57
|
+
const licenseFilePath = path.join(targetDir, 'LICENSE');
|
|
58
|
+
if (!options?.confirm) {
|
|
59
|
+
return {
|
|
60
|
+
success: true,
|
|
61
|
+
skillId,
|
|
62
|
+
skillName: meta.name,
|
|
63
|
+
tier: meta.tier,
|
|
64
|
+
installedFiles: [],
|
|
65
|
+
targetDirectory: targetDir,
|
|
66
|
+
previewOnly: true,
|
|
67
|
+
requiresConfirmation: true,
|
|
68
|
+
message: `[PREVIEW ONLY - 0 files written to disk]\n` +
|
|
69
|
+
`Skill '${skillId}' (${meta.name}) is prepared for installation under ${meta.tier.toUpperCase()} tier.\n` +
|
|
70
|
+
`Target directory: ${targetDir}\n` +
|
|
71
|
+
`Files planned:\n` +
|
|
72
|
+
` - ${skillFilePath} (declarative quality directives & HMAC provenance watermark)\n` +
|
|
73
|
+
` - ${licenseFilePath} (${meta.tier.toUpperCase()} legal notice)\n\n` +
|
|
74
|
+
`⚠️ USER CONFIRMATION REQUIRED: Ask the developer for explicit confirmation in chat before modifying workspace files.\n` +
|
|
75
|
+
`Call 'install_skill' with 'confirm: true' only after the developer explicitly approves.`,
|
|
76
|
+
promptRecommendation: `Ask developer: "Would you like me to install '${meta.name}' (${skillId}) into .agents/skills/${skillId}/?"`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
55
79
|
try {
|
|
56
80
|
try {
|
|
57
81
|
const stat = fs.lstatSync(targetDir);
|
|
@@ -68,8 +92,6 @@ export function installSkill(skillId, projectPath = process.cwd(), licenseOverri
|
|
|
68
92
|
? `${generateCanaryWatermark(license.customerName || 'pro_subscriber', skillId)}\n\n${rawSkillContent}`
|
|
69
93
|
: rawSkillContent;
|
|
70
94
|
const licenseContent = loadSkillLicense(skillId, license, projectPath);
|
|
71
|
-
const skillFilePath = path.join(targetDir, 'SKILL.md');
|
|
72
|
-
const licenseFilePath = path.join(targetDir, 'LICENSE');
|
|
73
95
|
fs.writeFileSync(skillFilePath, skillContent, 'utf-8');
|
|
74
96
|
fs.writeFileSync(licenseFilePath, licenseContent, 'utf-8');
|
|
75
97
|
return {
|
|
@@ -171,16 +193,18 @@ export function detectProjectSkills(projectPath = process.cwd()) {
|
|
|
171
193
|
/**
|
|
172
194
|
* Synchronizes workspace skills: either auto-detects or installs an explicit list.
|
|
173
195
|
* Respects license tiers and reports auth requirements clearly.
|
|
196
|
+
* Requires explicit confirm: true to perform disk writes (safe preview by default).
|
|
174
197
|
*/
|
|
175
|
-
export function syncWorkspaceSkills(projectPath = process.cwd(), requestedSkills, licenseOverride) {
|
|
198
|
+
export function syncWorkspaceSkills(projectPath = process.cwd(), requestedSkills, licenseOverride, options) {
|
|
176
199
|
const license = licenseOverride || validateLicense(undefined, { projectRoot: projectPath });
|
|
200
|
+
const isConfirmed = Boolean(options?.confirm);
|
|
177
201
|
const skillsToInstall = requestedSkills && requestedSkills.length > 0
|
|
178
202
|
? requestedSkills
|
|
179
203
|
: detectProjectSkills(projectPath);
|
|
180
204
|
const installed = [];
|
|
181
205
|
const skippedUnauthorized = [];
|
|
182
206
|
for (const id of skillsToInstall) {
|
|
183
|
-
const res = installSkill(id, projectPath, license);
|
|
207
|
+
const res = installSkill(id, projectPath, license, { confirm: isConfirmed });
|
|
184
208
|
if (res.success) {
|
|
185
209
|
installed.push(res);
|
|
186
210
|
}
|
|
@@ -188,24 +212,46 @@ export function syncWorkspaceSkills(projectPath = process.cwd(), requestedSkills
|
|
|
188
212
|
skippedUnauthorized.push(res);
|
|
189
213
|
}
|
|
190
214
|
}
|
|
191
|
-
const installedList = installed
|
|
215
|
+
const installedList = installed
|
|
216
|
+
.map((r) => ` - ${isConfirmed ? '✓' : '📋'} **\`${r.skillId}\`** [${r.tier?.toUpperCase()}]: ${r.skillName}`)
|
|
217
|
+
.join('\n');
|
|
192
218
|
const skippedList = skippedUnauthorized
|
|
193
219
|
.map((r) => ` - 🔒 **\`${r.skillId}\`** [${r.requiredTier?.toUpperCase()}]: Authentication required (Use 'set_license_key')`)
|
|
194
220
|
.join('\n');
|
|
195
|
-
let summary =
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
summary +=
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
221
|
+
let summary = '';
|
|
222
|
+
if (!isConfirmed) {
|
|
223
|
+
summary = `# [DRY-RUN PREVIEW] Fredo QA Skills Workspace Sync\n\n`;
|
|
224
|
+
summary += `> [!IMPORTANT] **0 files written to disk**. User agreement is required before modifying workspace files.\n\n`;
|
|
225
|
+
summary += `Target: \`${projectPath}\`\n`;
|
|
226
|
+
summary += `Active Plan: **${license.tier.toUpperCase()}**\n\n`;
|
|
227
|
+
if (installed.length > 0) {
|
|
228
|
+
summary += `### Skills Ready to Install (${installed.length}):\n${installedList}\n\n`;
|
|
229
|
+
}
|
|
230
|
+
if (skippedUnauthorized.length > 0) {
|
|
231
|
+
summary += `### Requires Authentication (${skippedUnauthorized.length}):\n${skippedList}\n\n`;
|
|
232
|
+
summary += `> [!WARNING] To unlock commercial skills, activate your license with the \`set_license_key\` tool.\n`;
|
|
233
|
+
summary += `> Purchase or manage licenses: https://www.illicomedia.com/qa-skills/pricing\n\n`;
|
|
234
|
+
}
|
|
235
|
+
summary += `### Next Steps for AI Agent:\n`;
|
|
236
|
+
summary += `1. Ask the developer for explicit confirmation in the chat: "I detected the following QA skills for this repository (${installed.map((s) => s.skillId).join(', ')}). Would you like me to install them into .agents/skills/?"\n`;
|
|
237
|
+
summary += `2. Re-run \`sync_workspace_skills\` with \`confirm: true\` only after receiving user approval.\n`;
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
summary = `# Fredo QA Skills Workspace Sync\n\n`;
|
|
241
|
+
summary += `Target: \`${projectPath}\`\n`;
|
|
242
|
+
summary += `Active Plan: **${license.tier.toUpperCase()}**\n\n`;
|
|
243
|
+
if (installed.length > 0) {
|
|
244
|
+
summary += `### Successfully Installed (${installed.length}):\n${installedList}\n\n`;
|
|
245
|
+
}
|
|
246
|
+
if (skippedUnauthorized.length > 0) {
|
|
247
|
+
summary += `### Requires Authentication (${skippedUnauthorized.length}):\n${skippedList}\n\n`;
|
|
248
|
+
summary += `> [!WARNING] To unlock commercial skills, activate your license with the \`set_license_key\` tool.\n`;
|
|
249
|
+
summary += `> Purchase or manage licenses: https://www.illicomedia.com/qa-skills/pricing\n\n`;
|
|
250
|
+
}
|
|
251
|
+
summary += `### Next Steps for AI Agent:\n`;
|
|
252
|
+
summary += `- Reference installed skills from \`.agents/skills/<skill_id>/SKILL.md\`\n`;
|
|
253
|
+
summary += `- Run relevant inner loop checks before commits\n`;
|
|
254
|
+
}
|
|
209
255
|
return {
|
|
210
256
|
projectPath,
|
|
211
257
|
activeTier: license.tier,
|
package/dist/tools.js
CHANGED
|
@@ -30,7 +30,7 @@ export const MCP_TOOLS = [
|
|
|
30
30
|
},
|
|
31
31
|
{
|
|
32
32
|
name: 'install_skill',
|
|
33
|
-
description: 'Install a Fredo QA skill into .agents/skills/<skill_id>/ with SKILL.md and LICENSE.
|
|
33
|
+
description: 'Install a Fredo QA skill into .agents/skills/<skill_id>/ with SKILL.md and LICENSE. Requires user agreement: set confirm: true to write files to disk; when false (default), returns a safe preview without modifying the filesystem.',
|
|
34
34
|
inputSchema: {
|
|
35
35
|
type: 'object',
|
|
36
36
|
properties: {
|
|
@@ -46,6 +46,10 @@ export const MCP_TOOLS = [
|
|
|
46
46
|
type: 'string',
|
|
47
47
|
description: 'Optional signed license file (path or JSON content) to use for this session before installing',
|
|
48
48
|
},
|
|
49
|
+
confirm: {
|
|
50
|
+
type: 'boolean',
|
|
51
|
+
description: 'Explicit user confirmation to write files to disk. When false (default), returns a preview of files to be created without modifying the filesystem.',
|
|
52
|
+
},
|
|
49
53
|
},
|
|
50
54
|
required: ['skill_id'],
|
|
51
55
|
additionalProperties: false,
|
|
@@ -53,7 +57,7 @@ export const MCP_TOOLS = [
|
|
|
53
57
|
},
|
|
54
58
|
{
|
|
55
59
|
name: 'sync_workspace_skills',
|
|
56
|
-
description: 'Auto-detects project stack and provisions matching QA skills and licenses into .agents/skills/.
|
|
60
|
+
description: 'Auto-detects project stack and provisions matching QA skills and licenses into .agents/skills/. Requires user agreement: set confirm: true to write files; when false (default), returns a dry-run preview.',
|
|
57
61
|
inputSchema: {
|
|
58
62
|
type: 'object',
|
|
59
63
|
properties: {
|
|
@@ -70,6 +74,10 @@ export const MCP_TOOLS = [
|
|
|
70
74
|
type: 'string',
|
|
71
75
|
description: 'Optional signed license file (path or JSON content) to use for this session before syncing',
|
|
72
76
|
},
|
|
77
|
+
confirm: {
|
|
78
|
+
type: 'boolean',
|
|
79
|
+
description: 'Explicit user confirmation to write files to disk. When false (default), returns a dry-run preview without modifying the filesystem.',
|
|
80
|
+
},
|
|
73
81
|
},
|
|
74
82
|
additionalProperties: false,
|
|
75
83
|
},
|
|
@@ -104,20 +112,29 @@ export const MCP_TOOLS = [
|
|
|
104
112
|
},
|
|
105
113
|
},
|
|
106
114
|
{
|
|
107
|
-
name: '
|
|
108
|
-
description: '
|
|
115
|
+
name: 'quick_check',
|
|
116
|
+
description: 'Fast preflight sanity check verifying workspace scaffolding, agent policy presence (AGENTS.md), licensing, secret hygiene (.gitignore), dependency lockfile, and test runner declaration before applying QA skills. Presence and configuration only: does not run test suites or compilers.',
|
|
109
117
|
inputSchema: {
|
|
110
118
|
type: 'object',
|
|
111
119
|
properties: {
|
|
112
120
|
project_path: {
|
|
113
121
|
type: 'string',
|
|
114
|
-
description: 'Path to the root directory of the project to
|
|
122
|
+
description: 'Path to the root directory of the project to check',
|
|
115
123
|
},
|
|
116
124
|
},
|
|
117
125
|
required: ['project_path'],
|
|
118
126
|
additionalProperties: false,
|
|
119
127
|
},
|
|
120
128
|
},
|
|
129
|
+
{
|
|
130
|
+
name: 'get_version',
|
|
131
|
+
description: 'Retrieve the active package version, protocol specification, and runtime metadata of the Fredo QA MCP Server.',
|
|
132
|
+
inputSchema: {
|
|
133
|
+
type: 'object',
|
|
134
|
+
properties: {},
|
|
135
|
+
additionalProperties: false,
|
|
136
|
+
},
|
|
137
|
+
},
|
|
121
138
|
];
|
|
122
139
|
export function findToolDefinition(name) {
|
|
123
140
|
return MCP_TOOLS.find((t) => t.name === name);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@illicomedia/skills-mcp-server",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Official Fredo QA Skills Model Context Protocol (MCP) Server for Cursor, Claude Desktop, Antigravity, and Copilot",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|