@illicomedia/skills-mcp-server 1.0.1 → 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/README.md CHANGED
File without changes
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 = 'File-presence checks only: gates are not executed and file contents are not validated. A full pass means the expected files exist, not that the project is compliant.';
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: hasExceptions,
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: hasExceptions
33
- ? 'Exception register file present (schema and expiry not checked).'
34
- : 'No .qa-exceptions.yaml found. Deviations from quality gates should be tracked there.',
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 3: RFC 9116 security.txt
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) || fs.existsSync(secTxtDist) || fs.existsSync(secTxtRoot);
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: 'security.txt present',
44
- passed: hasSecTxt,
45
- severity: 'blocker',
46
- details: hasSecTxt
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 4: Safe gitignore configuration
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 ignoresEnv = false;
154
+ let gitignoreContent = '';
54
155
  if (hasGitignore) {
55
- const gitignoreContent = fs.readFileSync(gitignore, 'utf-8');
56
- ignoresEnv = gitignoreContent.includes('.env');
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: hasGitignore && ignoresEnv,
62
- severity: 'blocker',
63
- details: hasGitignore && ignoresEnv
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 5: QA scripts in package.json
68
- const pkgJsonPath = path.join(resolvedPath, 'package.json');
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 hasTypeCheck = false;
71
- if (fs.existsSync(pkgJsonPath)) {
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'] || pkg.scripts?.['qa:starter']);
75
- hasTypeCheck = Boolean(pkg.scripts?.['type-check'] || pkg.scripts?.['tsc']);
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: 'qa:starter or qa:standard script declared',
296
+ name: qaCheckName,
84
297
  passed: hasQaStandard,
85
298
  severity: 'blocker',
86
- details: hasQaStandard
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: 'type-check script declared',
346
+ name: typeCheckName,
93
347
  passed: hasTypeCheck,
94
348
  severity: 'blocker',
95
- details: hasTypeCheck
96
- ? 'package.json declares a type-check script (script not executed).'
97
- : 'No type-check or tsc script in package.json.',
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 (!hasSecTxt) {
108
- recommendations.push('Generate public/.well-known/security.txt with valid Contact and Expires directives.');
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 (!hasQaStandard) {
111
- recommendations.push('Add "qa:starter" and "qa:standard" scripts to package.json to ensure local and CI parity.');
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/catalog.d.ts CHANGED
File without changes
package/dist/catalog.js CHANGED
File without changes
File without changes
package/dist/executor.js CHANGED
@@ -3,7 +3,8 @@
3
3
  import { runComplianceAudit } from './audit.js';
4
4
  import { SKILLS_CATALOG, getAvailableSkills, loadSkillContent } from './catalog.js';
5
5
  import { installSkill, syncWorkspaceSkills } from './installer.js';
6
- import { generateCanaryWatermark, validateLicense } from './license.js';
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') {
@@ -46,20 +47,28 @@ export class ToolExecutor {
46
47
  }, null, 2);
47
48
  }
48
49
  case 'list_skills': {
49
- const skills = getAvailableSkills(license.tier);
50
+ const accessibleIds = new Set(getAvailableSkills(license.tier).map((s) => s.id));
50
51
  const categoryFilter = args.category;
51
- const filtered = categoryFilter
52
- ? skills.filter((s) => s.category === categoryFilter)
53
- : skills;
54
- const freeCount = SKILLS_CATALOG.filter((s) => s.tier === 'free').length;
55
- const totalCount = SKILLS_CATALOG.length;
56
- const advisory = license.fallbackMode
57
- ? `\n> [!NOTE] Running on Free Foundation tier (${freeCount} MIT skills). Upgrade to Pro or Enterprise at https://www.illicomedia.com/qa-skills/pricing to unlock all ${totalCount} skills.\n`
52
+ const listed = categoryFilter
53
+ ? SKILLS_CATALOG.filter((s) => s.category === categoryFilter)
54
+ : SKILLS_CATALOG;
55
+ const accessibleCount = listed.filter((s) => accessibleIds.has(s.id)).length;
56
+ const advisory = accessibleCount < listed.length
57
+ ? `\n> [!NOTE] ${license.message}\n> Skills marked 🔒 require a signed Pro or Enterprise license file (${PROJECT_LICENSE_FILE}). Plans: ${PRICING_URL}\n`
58
58
  : '';
59
- const list = filtered
60
- .map((s) => `- **\`${s.id}\`** [${s.tier.toUpperCase()}] (${s.name}): ${s.description}\n *Key Gates:* ${s.rulesSummary.join(', ')}`)
61
- .join('\n');
62
- return `# Available Fredo QA Skills (${filtered.length} accessible under ${license.tier.toUpperCase()} plan)\n${advisory}\n${list}`;
59
+ const cell = (value) => value.replace(/\|/g, '\\|').replace(/\s+/g, ' ');
60
+ const rows = listed.map((s) => {
61
+ const access = accessibleIds.has(s.id)
62
+ ? '✅ Available'
63
+ : `🔒 Requires ${s.tier === 'enterprise' ? 'Enterprise' : 'Pro'} license`;
64
+ return `| \`${s.id}\` | ${s.tier.toUpperCase()} | ${access} | ${s.category} | ${cell(s.name)}: ${cell(s.description)} |`;
65
+ });
66
+ const table = [
67
+ '| Skill | Tier | Access | Category | Description |',
68
+ '| --- | --- | --- | --- | --- |',
69
+ ...rows,
70
+ ].join('\n');
71
+ return `# Fredo QA Skills catalog (${accessibleCount} accessible under ${license.tier.toUpperCase()} plan, ${listed.length} listed)\n${advisory}\n${table}`;
63
72
  }
64
73
  case 'get_skill_rules': {
65
74
  const skillId = args.skill_id;
@@ -75,20 +84,32 @@ export class ToolExecutor {
75
84
  case 'install_skill': {
76
85
  const skillId = args.skill_id;
77
86
  const projectPath = args.project_path || process.cwd();
78
- const res = installSkill(skillId, projectPath, license);
87
+ const confirm = Boolean(args.confirm);
88
+ const res = installSkill(skillId, projectPath, license, { confirm });
79
89
  return JSON.stringify(res, null, 2);
80
90
  }
81
91
  case 'sync_workspace_skills': {
82
92
  const projectPath = args.project_path || process.cwd();
83
93
  const requestedSkills = args.skills;
84
- const res = syncWorkspaceSkills(projectPath, requestedSkills, license);
94
+ const confirm = Boolean(args.confirm);
95
+ const res = syncWorkspaceSkills(projectPath, requestedSkills, license, { confirm });
85
96
  return res.summaryMessage;
86
97
  }
87
- case 'audit_compliance': {
98
+ case 'quick_check': {
88
99
  const projectPath = args.project_path || process.cwd();
89
100
  const report = runComplianceAudit(projectPath);
90
101
  return JSON.stringify(report, null, 2);
91
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
+ }
92
113
  default:
93
114
  throw new Error(`Unsupported tool: ${name}`);
94
115
  }
package/dist/index.d.ts CHANGED
File without changes
package/dist/index.js CHANGED
File without changes
@@ -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.map((r) => ` - ✓ **\`${r.skillId}\`** [${r.tier?.toUpperCase()}]: ${r.skillName}`).join('\n');
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 = `# Fredo QA Skills Workspace Sync\n\n`;
196
- summary += `Target: \`${projectPath}\`\n`;
197
- summary += `Active Plan: **${license.tier.toUpperCase()}**\n\n`;
198
- if (installed.length > 0) {
199
- summary += `### Successfully Installed (${installed.length}):\n${installedList}\n\n`;
200
- }
201
- if (skippedUnauthorized.length > 0) {
202
- summary += `### Requires Authentication (${skippedUnauthorized.length}):\n${skippedList}\n\n`;
203
- summary += `> [!WARNING] To unlock commercial skills, activate your license with the \`set_license_key\` tool.\n`;
204
- summary += `> Purchase or manage licenses: https://www.illicomedia.com/qa-skills/pricing\n\n`;
205
- }
206
- summary += `### Next Steps for AI Agent:\n`;
207
- summary += `- Reference installed skills from \`.agents/skills/<skill_id>/SKILL.md\`\n`;
208
- summary += `- Run relevant inner loop checks before commits\n`;
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,
@@ -8,4 +8,4 @@ export declare const TRUSTED_LICENSE_KEYS: Readonly<Record<string, string>>;
8
8
  * Release date of this build. A license unlocks this build only when its
9
9
  * `updates_until` is on or after this date (version retention).
10
10
  */
11
- export declare const SERVER_RELEASE_DATE = "2026-10-07T00:00:00Z";
11
+ export declare const SERVER_RELEASE_DATE = "2026-10-23T00:00:00Z";
@@ -6,12 +6,12 @@
6
6
  * While this map is empty, no paid license verifies and the server runs in free mode.
7
7
  */
8
8
  export const TRUSTED_LICENSE_KEYS = Object.freeze({
9
- 'fredo-qa-root-2026': `-----BEGIN PUBLIC KEY-----
10
- MCowBQYDK2VwAyEAzNnesPRQ2KrjmbcwWw9xJRtIOLwG+1xeb7LNf7iPOUs=
9
+ 'fredo-qa-2026-10': `-----BEGIN PUBLIC KEY-----
10
+ MCowBQYDK2VwAyEAPrQoO+oWLJAO81k/6uR0tRZGsDVmkvrc6CCHzO1YfpU=
11
11
  -----END PUBLIC KEY-----`,
12
12
  });
13
13
  /**
14
14
  * Release date of this build. A license unlocks this build only when its
15
15
  * `updates_until` is on or after this date (version retention).
16
16
  */
17
- export const SERVER_RELEASE_DATE = '2026-10-07T00:00:00Z';
17
+ export const SERVER_RELEASE_DATE = '2026-10-23T00:00:00Z';
File without changes
@@ -66,7 +66,7 @@ export function resolveSkillLicense(meta, license, searchDir) {
66
66
  if (meta.tier === 'free') {
67
67
  return content;
68
68
  }
69
- return `${content}\n\n---\nCOMMERCIAL ACTIVATION CERTIFICATE\nLicensee: ${license.customerName || 'Licensed Organization'}\nLicense Tier: ${meta.tier.toUpperCase()}\nLicense: ${license.licenseId || 'unknown'} (Ed25519 signature verified)\nUpdates Until: ${license.updatesUntil || 'unknown'}\nAuthorized by: Frédéric Charles Viallet (www.illicomedia.com/qa-skills/)`;
69
+ return `${content}\n\n---\nCOMMERCIAL LICENSE ACTIVATION\nLicensee: ${license.customerName || 'Licensed Organization'}\nLicense Tier: ${meta.tier.toUpperCase()}\nLicense: ${license.licenseId || 'unknown'} (Ed25519 signature verified)\nUpdates Until: ${license.updatesUntil || 'unknown'}\nAuthorized by: Frédéric Charles Viallet (www.illicomedia.com/qa-skills/)`;
70
70
  }
71
71
  }
72
72
  catch {
package/dist/license.d.ts CHANGED
@@ -1,4 +1,8 @@
1
1
  import type { LicenseValidationResult } from './types.js';
2
+ export declare const PRICING_URL = "https://www.illicomedia.com/qa-skills/pricing";
3
+ /** License file format version understood by this build (see LICENSE_SCHEMA_URL). */
4
+ export declare const LICENSE_SCHEMA_VERSION = 1;
5
+ export declare const LICENSE_SCHEMA_URL = "https://www.illicomedia.com/schemas/qa-license-v1.schema.json";
2
6
  export declare const LICENSE_FILE_ENV = "FREDO_QA_LICENSE_FILE";
3
7
  export declare const USER_LICENSE_PATH: string[];
4
8
  export declare const PROJECT_LICENSE_FILE = ".qa-license.json";
package/dist/license.js CHANGED
@@ -6,7 +6,7 @@ import * as os from 'node:os';
6
6
  import * as path from 'node:path';
7
7
  import { SERVER_RELEASE_DATE, TRUSTED_LICENSE_KEYS } from './license-keys.js';
8
8
  const HMAC_SALT = 'fredo-qa-skills-provenance-v1';
9
- const PRICING_URL = 'https://www.illicomedia.com/qa-skills/pricing';
9
+ export const PRICING_URL = 'https://www.illicomedia.com/qa-skills/pricing';
10
10
  const MAX_LICENSE_BYTES = 64 * 1024;
11
11
  const ISO_INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/;
12
12
  const BASE64 = /^[A-Za-z0-9+/]+={0,2}$/;
@@ -19,7 +19,17 @@ const STRING_FIELDS = [
19
19
  'key_id',
20
20
  'signature',
21
21
  ];
22
- const ALLOWED_FIELDS = new Set([...STRING_FIELDS, 'tier', 'seats', 'binding']);
22
+ const ALLOWED_FIELDS = new Set([
23
+ ...STRING_FIELDS,
24
+ 'tier',
25
+ 'seats',
26
+ 'binding',
27
+ 'schema_version',
28
+ '$schema',
29
+ ]);
30
+ /** License file format version understood by this build (see LICENSE_SCHEMA_URL). */
31
+ export const LICENSE_SCHEMA_VERSION = 1;
32
+ export const LICENSE_SCHEMA_URL = 'https://www.illicomedia.com/schemas/qa-license-v1.schema.json';
23
33
  export const LICENSE_FILE_ENV = 'FREDO_QA_LICENSE_FILE';
24
34
  export const USER_LICENSE_PATH = ['.config', 'fredo-qa', 'license.json'];
25
35
  export const PROJECT_LICENSE_FILE = '.qa-license.json';
@@ -76,6 +86,14 @@ function parseLicense(text) {
76
86
  return 'license file must contain a JSON object';
77
87
  }
78
88
  const record = data;
89
+ if (record.schema_version !== LICENSE_SCHEMA_VERSION) {
90
+ return Number.isInteger(record.schema_version) && record.schema_version > LICENSE_SCHEMA_VERSION
91
+ ? `license format version ${String(record.schema_version)} requires a newer MCP server (this build reads version ${LICENSE_SCHEMA_VERSION}); update the MCP server`
92
+ : `license schema_version must be ${LICENSE_SCHEMA_VERSION}`;
93
+ }
94
+ if (record.$schema !== undefined && record.$schema !== LICENSE_SCHEMA_URL) {
95
+ return `license $schema must be ${LICENSE_SCHEMA_URL}`;
96
+ }
79
97
  const unknownField = Object.keys(record).find((key) => !ALLOWED_FIELDS.has(key));
80
98
  if (unknownField) {
81
99
  return `license field '${unknownField}' is not supported`;
package/dist/server.d.ts CHANGED
File without changes
package/dist/server.js CHANGED
@@ -3,6 +3,7 @@
3
3
  import * as readline from 'node:readline';
4
4
  import { ToolExecutor } from './executor.js';
5
5
  import { MCP_TOOLS, findToolDefinition, validateToolParams } from './tools.js';
6
+ import { SERVER_VERSION } from './version.js';
6
7
  export { MCP_TOOLS };
7
8
  export const DEFAULT_API_ENDPOINT = process.env.FREDO_QA_API_URL || 'https://www.illicomedia.com/api';
8
9
  export class FredoQaMcpServer {
@@ -33,7 +34,7 @@ export class FredoQaMcpServer {
33
34
  },
34
35
  serverInfo: {
35
36
  name: '@illicomedia/skills-mcp-server',
36
- version: '1.0.0',
37
+ version: SERVER_VERSION,
37
38
  description: 'Official Fredo QA Skills Model Context Protocol Server',
38
39
  },
39
40
  },
package/dist/tools.d.ts CHANGED
File without changes
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. Enforces authentication for paid Pro and Enterprise skills.',
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/. Respects license tiers and reports auth requirements.',
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: 'audit_compliance',
108
- description: 'Check that the files and scripts Fredo QA Skills expects are present in a project (AGENTS.md, .qa-exceptions.yaml, security.txt, .gitignore, QA scripts). Presence only: gates are not run and contents are not validated.',
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 audit',
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/dist/types.d.ts CHANGED
File without changes
package/dist/types.js CHANGED
File without changes
@@ -0,0 +1,2 @@
1
+ /** Version of this package, read from the package.json shipped next to dist/ (single source of truth). */
2
+ export declare const SERVER_VERSION: string;
@@ -0,0 +1,5 @@
1
+ // SPDX-FileCopyrightText: 2026 Frédéric Charles Viallet
2
+ // SPDX-License-Identifier: MIT
3
+ import { readFileSync } from 'node:fs';
4
+ /** Version of this package, read from the package.json shipped next to dist/ (single source of truth). */
5
+ export const SERVER_VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@illicomedia/skills-mcp-server",
3
- "version": "1.0.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",