@valentia-ai-skills/framework 1.0.13 → 2.0.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/bin/cli.js CHANGED
@@ -4,11 +4,12 @@
4
4
  * ai-skills CLI — @valentia-ai-skills/framework
5
5
  *
6
6
  * Usage:
7
- * npx ai-skills setup # Enter email → lookup team → install team's skills
8
- * npx ai-skills update # Re-fetch skills from Supabase for your team
9
- * npx ai-skills status # Show installed skills and team info
7
+ * npx ai-skills setup # Enter email → lookup team → install full toolkit
8
+ * npx ai-skills update # Re-fetch toolkit from Supabase for your team
9
+ * npx ai-skills status # Show installed toolkit and team info
10
10
  * npx ai-skills list # List all locally available skills
11
11
  * npx ai-skills doctor # Health check
12
+ * npx ai-skills analyze # Analyze last commit against active skills
12
13
  */
13
14
 
14
15
  const fs = require("fs");
@@ -31,6 +32,111 @@ const ANALYZE_FUNCTION_URL =
31
32
  const SCAN_FUNCTION_URL =
32
33
  process.env.AI_SKILLS_SCAN_URL || "https://znshdhjquohrzvbnloki.supabase.co/functions/v1/scan-results";
33
34
 
35
+ // ── Language Detection ──
36
+
37
+ const LANGUAGE_DETECTORS = {
38
+ typescript: {
39
+ files: ["tsconfig.json", "tsconfig.base.json"],
40
+ packageDeps: ["typescript"],
41
+ extensions: [".ts", ".tsx"],
42
+ },
43
+ javascript: {
44
+ files: ["jsconfig.json"],
45
+ packageDeps: [],
46
+ extensions: [".js", ".jsx"],
47
+ },
48
+ python: {
49
+ files: ["requirements.txt", "Pipfile", "pyproject.toml", "setup.py", "setup.cfg"],
50
+ packageDeps: [],
51
+ extensions: [".py"],
52
+ },
53
+ golang: {
54
+ files: ["go.mod", "go.sum"],
55
+ packageDeps: [],
56
+ extensions: [".go"],
57
+ },
58
+ rust: {
59
+ files: ["Cargo.toml", "Cargo.lock"],
60
+ packageDeps: [],
61
+ extensions: [".rs"],
62
+ },
63
+ java: {
64
+ files: ["pom.xml", "build.gradle", "build.gradle.kts"],
65
+ packageDeps: [],
66
+ extensions: [".java"],
67
+ },
68
+ kotlin: {
69
+ files: ["build.gradle.kts"],
70
+ packageDeps: [],
71
+ extensions: [".kt", ".kts"],
72
+ },
73
+ swift: {
74
+ files: ["Package.swift", "*.xcodeproj"],
75
+ packageDeps: [],
76
+ extensions: [".swift"],
77
+ },
78
+ php: {
79
+ files: ["composer.json", "artisan"],
80
+ packageDeps: [],
81
+ extensions: [".php"],
82
+ },
83
+ csharp: {
84
+ files: ["*.csproj", "*.sln"],
85
+ packageDeps: [],
86
+ extensions: [".cs"],
87
+ },
88
+ cpp: {
89
+ files: ["CMakeLists.txt", "Makefile"],
90
+ packageDeps: [],
91
+ extensions: [".cpp", ".cc", ".h", ".hpp"],
92
+ },
93
+ };
94
+
95
+ function detectLanguages() {
96
+ const detected = [];
97
+
98
+ // Check package.json for TS dependency
99
+ let pkgDeps = {};
100
+ try {
101
+ const pkgPath = path.join(PROJECT_ROOT, "package.json");
102
+ if (fs.existsSync(pkgPath)) {
103
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
104
+ pkgDeps = { ...pkg.dependencies, ...pkg.devDependencies };
105
+ }
106
+ } catch { /* ignore */ }
107
+
108
+ for (const [lang, detector] of Object.entries(LANGUAGE_DETECTORS)) {
109
+ // Check marker files
110
+ const hasFile = detector.files.some((f) => {
111
+ if (f.includes("*")) {
112
+ // Glob pattern — check if any matching file exists
113
+ try {
114
+ const dir = fs.readdirSync(PROJECT_ROOT);
115
+ const ext = f.replace("*", "");
116
+ return dir.some((d) => d.endsWith(ext));
117
+ } catch { return false; }
118
+ }
119
+ return fs.existsSync(path.join(PROJECT_ROOT, f));
120
+ });
121
+
122
+ // Check package.json dependencies
123
+ const hasDep = detector.packageDeps.some((dep) => dep in pkgDeps);
124
+
125
+ if (hasFile || hasDep) {
126
+ detected.push(lang);
127
+ }
128
+ }
129
+
130
+ // If package.json exists but no typescript detected, it's javascript
131
+ if (fs.existsSync(path.join(PROJECT_ROOT, "package.json")) && !detected.includes("typescript") && !detected.includes("javascript")) {
132
+ detected.push("javascript");
133
+ }
134
+
135
+ return detected;
136
+ }
137
+
138
+ // ── Tool Configs ──
139
+
34
140
  const TOOL_CONFIGS = {
35
141
  "claude-code": {
36
142
  name: "Claude Code",
@@ -69,6 +175,7 @@ const TOOL_CONFIGS = {
69
175
  const C = {
70
176
  green: "\x1b[32m", yellow: "\x1b[33m", blue: "\x1b[34m",
71
177
  red: "\x1b[31m", dim: "\x1b[2m", bold: "\x1b[1m", reset: "\x1b[0m",
178
+ cyan: "\x1b[36m", magenta: "\x1b[35m",
72
179
  };
73
180
 
74
181
  // ── Helpers ──
@@ -163,16 +270,47 @@ function filterSkillsForProject(skills) {
163
270
  const projectName = getProjectName();
164
271
  return skills.filter((skill) => {
165
272
  if (skill.scope !== "project") return true;
166
- // Only install project skills matching this project
167
273
  return skill.project_name === projectName;
168
274
  });
169
275
  }
170
276
 
277
+ function extractFrontmatter(content) {
278
+ const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
279
+ if (!match) return {};
280
+ const fm = {};
281
+ for (const line of match[1].split("\n")) {
282
+ if (!line.startsWith(" ") && line.includes(":")) {
283
+ const [key, ...rest] = line.split(":");
284
+ fm[key.trim()] = rest.join(":").trim().replace(/^["']|["']$/g, "");
285
+ }
286
+ }
287
+ return fm;
288
+ }
289
+
290
+ function getLocalSkills() {
291
+ const skills = [];
292
+ if (!fs.existsSync(SKILLS_SOURCE)) return skills;
293
+ for (const cat of ["global", "stack"]) {
294
+ const catDir = path.join(SKILLS_SOURCE, cat);
295
+ if (!fs.existsSync(catDir)) continue;
296
+ for (const item of fs.readdirSync(catDir)) {
297
+ const skillMd = path.join(catDir, item, "SKILL.md");
298
+ if (fs.existsSync(skillMd)) {
299
+ const content = fs.readFileSync(skillMd, "utf-8");
300
+ const fm = extractFrontmatter(content);
301
+ skills.push({ name: fm.name || item, scope: cat, stack: fm.stack || null, version: fm.version || "?", content });
302
+ }
303
+ }
304
+ }
305
+ return skills;
306
+ }
307
+
308
+ // ── Installation Functions ──
309
+
171
310
  function installSkillsForTool(toolKey, skills) {
172
311
  const tool = TOOL_CONFIGS[toolKey];
173
312
  if (!tool) return 0;
174
313
 
175
- // Filter: only install project-scoped skills that match this project
176
314
  const filtered = filterSkillsForProject(skills);
177
315
 
178
316
  if (tool.format === "skill-folder") {
@@ -182,7 +320,6 @@ function installSkillsForTool(toolKey, skills) {
182
320
  mkdirp(dest);
183
321
  fs.writeFileSync(path.join(dest, "SKILL.md"), skill.content);
184
322
 
185
- // Write reference files if the skill has any
186
323
  if (skill.references && typeof skill.references === "object") {
187
324
  for (const [filePath, fileContent] of Object.entries(skill.references)) {
188
325
  const refDest = path.join(dest, filePath);
@@ -204,14 +341,12 @@ function installSkillsForTool(toolKey, skills) {
204
341
  content += `# Last updated: ${new Date().toISOString().split("T")[0]}\n\n`;
205
342
 
206
343
  for (const skill of filtered) {
207
- // Strip YAML frontmatter for rules files
208
344
  const body = skill.content.replace(/^---[\s\S]*?---\n*/m, "").trim();
209
345
  content += `\n${"=".repeat(60)}\n`;
210
346
  content += `# SKILL: ${skill.name} (${skill.scope})\n`;
211
347
  content += `${"=".repeat(60)}\n\n`;
212
348
  content += body + "\n";
213
349
 
214
- // Inline reference files for rules-file format
215
350
  if (skill.references && typeof skill.references === "object") {
216
351
  for (const [refPath, refContent] of Object.entries(skill.references)) {
217
352
  content += `\n--- Reference: ${refPath} ---\n`;
@@ -227,35 +362,250 @@ function installSkillsForTool(toolKey, skills) {
227
362
  return 0;
228
363
  }
229
364
 
230
- function extractFrontmatter(content) {
231
- const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
232
- if (!match) return {};
233
- const fm = {};
234
- for (const line of match[1].split("\n")) {
235
- if (!line.startsWith(" ") && line.includes(":")) {
236
- const [key, ...rest] = line.split(":");
237
- fm[key.trim()] = rest.join(":").trim().replace(/^["']|["']$/g, "");
365
+ function installAgentsForTool(toolKey, agents) {
366
+ const tool = TOOL_CONFIGS[toolKey];
367
+ if (!tool || !agents || agents.length === 0) return 0;
368
+
369
+ if (tool.format === "skill-folder") {
370
+ // Claude Code: individual agent .md files
371
+ const agentsDir = path.join(PROJECT_ROOT, ".claude", "agents");
372
+ mkdirp(agentsDir);
373
+ for (const agent of agents) {
374
+ const content = [
375
+ "---",
376
+ `name: ${agent.name}`,
377
+ `description: ${agent.display_name || agent.description}`,
378
+ agent.model_preference ? `model: ${agent.model_preference}` : null,
379
+ agent.allowed_tools ? `tools: ${JSON.stringify(agent.allowed_tools)}` : null,
380
+ agent.max_turns ? `max_turns: ${agent.max_turns}` : null,
381
+ "---",
382
+ "",
383
+ agent.instructions,
384
+ ].filter(Boolean).join("\n");
385
+ fs.writeFileSync(path.join(agentsDir, `${agent.name}.md`), content);
238
386
  }
387
+ return agents.length;
239
388
  }
240
- return fm;
389
+
390
+ if (tool.format === "rules-file") {
391
+ // Other tools: merge agents into a rules file
392
+ const rulesDir = path.dirname(path.join(PROJECT_ROOT, tool.rulesFile));
393
+ const agentsFile = path.join(rulesDir, "ai-agents.mdc");
394
+ mkdirp(rulesDir);
395
+
396
+ let content = `# AI Agent Definitions\n`;
397
+ content += `# Auto-generated by @valentia-ai-skills/framework\n\n`;
398
+
399
+ for (const agent of agents) {
400
+ content += `\n${"=".repeat(60)}\n`;
401
+ content += `# AGENT: ${agent.name} (${agent.scope}) — ${agent.model_preference || "sonnet"}\n`;
402
+ content += `${"=".repeat(60)}\n\n`;
403
+ content += agent.instructions + "\n";
404
+ }
405
+
406
+ fs.writeFileSync(agentsFile, content);
407
+ return agents.length;
408
+ }
409
+
410
+ return 0;
241
411
  }
242
412
 
243
- function getLocalSkills() {
244
- const skills = [];
245
- if (!fs.existsSync(SKILLS_SOURCE)) return skills;
246
- for (const cat of ["global", "stack"]) {
247
- const catDir = path.join(SKILLS_SOURCE, cat);
248
- if (!fs.existsSync(catDir)) continue;
249
- for (const item of fs.readdirSync(catDir)) {
250
- const skillMd = path.join(catDir, item, "SKILL.md");
251
- if (fs.existsSync(skillMd)) {
252
- const content = fs.readFileSync(skillMd, "utf-8");
253
- const fm = extractFrontmatter(content);
254
- skills.push({ name: fm.name || item, scope: cat, stack: fm.stack || null, version: fm.version || "?", content });
413
+ function installCommandsForTool(toolKey, commands) {
414
+ const tool = TOOL_CONFIGS[toolKey];
415
+ if (!tool || !commands || commands.length === 0) return 0;
416
+
417
+ if (tool.format === "skill-folder") {
418
+ // Claude Code: individual command .md files
419
+ const commandsDir = path.join(PROJECT_ROOT, ".claude", "commands");
420
+ mkdirp(commandsDir);
421
+ for (const cmd of commands) {
422
+ const content = [
423
+ "---",
424
+ `name: ${cmd.name}`,
425
+ `description: ${cmd.display_name || cmd.description}`,
426
+ cmd.agent_ref ? `agent: ${cmd.agent_ref}` : null,
427
+ "---",
428
+ "",
429
+ cmd.instructions,
430
+ ].filter(Boolean).join("\n");
431
+ fs.writeFileSync(path.join(commandsDir, `${cmd.name}.md`), content);
432
+ }
433
+ return commands.length;
434
+ }
435
+
436
+ if (tool.format === "rules-file") {
437
+ // Other tools: merge commands into rules file
438
+ const rulesDir = path.dirname(path.join(PROJECT_ROOT, tool.rulesFile));
439
+ const commandsFile = path.join(rulesDir, "ai-commands.mdc");
440
+ mkdirp(rulesDir);
441
+
442
+ let content = `# AI Command Definitions\n`;
443
+ content += `# Auto-generated by @valentia-ai-skills/framework\n\n`;
444
+
445
+ for (const cmd of commands) {
446
+ content += `\n${"=".repeat(60)}\n`;
447
+ content += `# COMMAND: /${cmd.name} (${cmd.scope})\n`;
448
+ content += `${"=".repeat(60)}\n\n`;
449
+ content += cmd.instructions + "\n";
450
+ }
451
+
452
+ fs.writeFileSync(commandsFile, content);
453
+ return commands.length;
454
+ }
455
+
456
+ return 0;
457
+ }
458
+
459
+ function installHooksForTool(toolKey, hooks) {
460
+ const tool = TOOL_CONFIGS[toolKey];
461
+ if (!tool || !hooks || hooks.length === 0) return 0;
462
+
463
+ if (tool.format === "skill-folder") {
464
+ // Claude Code: write hook scripts + settings.local.json
465
+ const hooksScriptDir = path.join(PROJECT_ROOT, ".ai-skills", "hooks");
466
+ mkdirp(hooksScriptDir);
467
+
468
+ // Write individual hook scripts
469
+ for (const hook of hooks) {
470
+ if (hook.script_content) {
471
+ const scriptPath = path.join(hooksScriptDir, `${hook.name}.js`);
472
+ fs.writeFileSync(scriptPath, hook.script_content);
255
473
  }
256
474
  }
475
+
476
+ // Build Claude Code hooks config for settings.local.json
477
+ const hooksByEvent = {};
478
+ for (const hook of hooks) {
479
+ if (!hooksByEvent[hook.event]) hooksByEvent[hook.event] = [];
480
+ hooksByEvent[hook.event].push({
481
+ matcher: hook.tool_match || "*",
482
+ hooks: [
483
+ {
484
+ type: hook.hook_type || "command",
485
+ command: hook.command,
486
+ ...(hook.timeout_ms ? { timeout: hook.timeout_ms } : {}),
487
+ },
488
+ ],
489
+ });
490
+ }
491
+
492
+ // Read existing settings.local.json or create new
493
+ const settingsPath = path.join(PROJECT_ROOT, ".claude", "settings.local.json");
494
+ let settings = {};
495
+ if (fs.existsSync(settingsPath)) {
496
+ try { settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8")); } catch { settings = {}; }
497
+ }
498
+
499
+ // Merge hooks into settings (preserve existing non-hook settings)
500
+ settings.hooks = hooksByEvent;
501
+
502
+ mkdirp(path.dirname(settingsPath));
503
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
504
+
505
+ return hooks.length;
257
506
  }
258
- return skills;
507
+
508
+ // Other tools don't support hooks in the same way — skip
509
+ return 0;
510
+ }
511
+
512
+ function installRulesForTool(toolKey, rules, detectedLanguages) {
513
+ const tool = TOOL_CONFIGS[toolKey];
514
+ if (!tool || !rules || rules.length === 0) return 0;
515
+
516
+ // Filter rules by detected languages (auto_detect rules only install if language found)
517
+ const filtered = rules.filter((rule) => {
518
+ if (rule.language === "all") return true; // Universal rules always install
519
+ if (!rule.auto_detect) return true; // Non-auto-detect rules always install
520
+ return detectedLanguages.includes(rule.language); // Auto-detect: only if language found
521
+ });
522
+
523
+ if (filtered.length === 0) return 0;
524
+
525
+ if (tool.format === "skill-folder") {
526
+ // Claude Code: individual rule .md files
527
+ const rulesDir = path.join(PROJECT_ROOT, ".claude", "rules");
528
+ mkdirp(rulesDir);
529
+ for (const rule of filtered) {
530
+ fs.writeFileSync(path.join(rulesDir, `${rule.name}.md`), rule.content);
531
+ }
532
+ return filtered.length;
533
+ }
534
+
535
+ if (tool.format === "rules-file") {
536
+ // Other tools: merge rules into a rules file
537
+ const rulesDir = path.dirname(path.join(PROJECT_ROOT, tool.rulesFile));
538
+ const rulesFile = path.join(rulesDir, "ai-rules.mdc");
539
+ mkdirp(rulesDir);
540
+
541
+ let content = `# AI Coding Rules\n`;
542
+ content += `# Auto-generated by @valentia-ai-skills/framework\n\n`;
543
+
544
+ for (const rule of filtered) {
545
+ content += `\n${"=".repeat(60)}\n`;
546
+ content += `# RULE: ${rule.name} (${rule.language})\n`;
547
+ content += `${"=".repeat(60)}\n\n`;
548
+ content += rule.content + "\n";
549
+ }
550
+
551
+ fs.writeFileSync(rulesFile, content);
552
+ return filtered.length;
553
+ }
554
+
555
+ return 0;
556
+ }
557
+
558
+ function installMcpConfigsForTool(toolKey, mcpConfigs) {
559
+ const tool = TOOL_CONFIGS[toolKey];
560
+ if (!tool || !mcpConfigs || mcpConfigs.length === 0) return 0;
561
+
562
+ if (tool.format === "skill-folder") {
563
+ // Claude Code: merge into .claude/mcp-servers.json
564
+ // NOTE: We write templates with {{PLACEHOLDER}} — user must fill in actual values
565
+ const mcpPath = path.join(PROJECT_ROOT, ".claude", "mcp-servers.json");
566
+ let existing = {};
567
+ if (fs.existsSync(mcpPath)) {
568
+ try { existing = JSON.parse(fs.readFileSync(mcpPath, "utf-8")); } catch { existing = {}; }
569
+ }
570
+
571
+ if (!existing.mcpServers) existing.mcpServers = {};
572
+
573
+ for (const mcp of mcpConfigs) {
574
+ // Only add if not already configured (don't overwrite user's actual values)
575
+ if (!existing.mcpServers[mcp.name]) {
576
+ existing.mcpServers[mcp.name] = mcp.config_template;
577
+ }
578
+ }
579
+
580
+ mkdirp(path.dirname(mcpPath));
581
+ fs.writeFileSync(mcpPath, JSON.stringify(existing, null, 2) + "\n");
582
+ return mcpConfigs.length;
583
+ }
584
+
585
+ // Other tools: write a reference file listing available MCP configs
586
+ if (tool.format === "rules-file") {
587
+ const rulesDir = path.dirname(path.join(PROJECT_ROOT, tool.rulesFile));
588
+ const mcpFile = path.join(rulesDir, "ai-mcp-configs.md");
589
+ mkdirp(rulesDir);
590
+
591
+ let content = `# Available MCP Server Configurations\n`;
592
+ content += `# These MCP servers are recommended for your team's toolkit\n\n`;
593
+
594
+ for (const mcp of mcpConfigs) {
595
+ content += `## ${mcp.display_name || mcp.name}\n`;
596
+ content += `${mcp.description}\n`;
597
+ content += `- Package: \`${mcp.package_name}\`\n`;
598
+ if (mcp.required_env_vars?.length) {
599
+ content += `- Required env vars: ${mcp.required_env_vars.join(", ")}\n`;
600
+ }
601
+ content += `\n`;
602
+ }
603
+
604
+ fs.writeFileSync(mcpFile, content);
605
+ return mcpConfigs.length;
606
+ }
607
+
608
+ return 0;
259
609
  }
260
610
 
261
611
  // ── Email + OTP Verification ──
@@ -290,11 +640,9 @@ async function requestOtpForEmail(emailInput) {
290
640
  throw new Error(response.error);
291
641
  }
292
642
 
293
- // OTP sent successfully
294
643
  console.log(c("green", `\n✓ Found: ${response.user_name}`));
295
644
 
296
645
  if (response.fallback_code) {
297
- // Email delivery not configured — show code in terminal
298
646
  console.log(c("yellow", `\n Your verification code: ${c("bold", response.fallback_code)}\n`));
299
647
  } else {
300
648
  console.log(c("dim", ` A verification code has been sent to ${email}\n`));
@@ -340,22 +688,29 @@ async function cmdSetup() {
340
688
  console.log(c("yellow", "No AI coding tools detected. Will create a generic .ai-rules file.\n"));
341
689
  tools.push("generic");
342
690
  } else {
343
- console.log(`Detected: ${tools.map((t) => c("green", TOOL_CONFIGS[t]?.name || t)).join(", ")}\n`);
691
+ console.log(`${c("bold", "AI Tools:")} ${tools.map((t) => c("green", TOOL_CONFIGS[t]?.name || t)).join(", ")}`);
692
+ }
693
+
694
+ // 2. Detect languages
695
+ const detectedLanguages = detectLanguages();
696
+ if (detectedLanguages.length > 0) {
697
+ console.log(`${c("bold", "Languages:")} ${detectedLanguages.map((l) => c("cyan", l)).join(", ")}`);
344
698
  }
699
+ console.log("");
345
700
 
346
- // 2. Ask for email
701
+ // 3. Ask for email
347
702
  let email = await ask(`${c("bold", "Enter your work email:")} `);
348
703
 
349
- let skills;
704
+ let response;
350
705
  let teamName = null;
351
706
 
352
707
  try {
353
- // 3. Request OTP (with 1 retry for wrong email)
708
+ // 4. Request OTP
354
709
  const otpResult = await requestOtpForEmail(email);
355
710
  email = otpResult.email;
356
711
 
357
- // 4. Verify OTP and get skills
358
- const response = await verifyOtp(email);
712
+ // 5. Verify OTP and get full toolkit
713
+ response = await verifyOtp(email);
359
714
 
360
715
  if (response.team) {
361
716
  teamName = response.team.name;
@@ -369,60 +724,108 @@ async function cmdSetup() {
369
724
  } else if (response.message) {
370
725
  console.log(c("yellow", `⚠ ${response.message}`));
371
726
  }
372
-
373
- skills = response.skills || [];
374
-
375
- if (skills.length === 0) {
376
- console.log(c("yellow", "\n⚠ No skills are enabled for your team. Contact your Team Lead."));
377
- process.exit(1);
378
- }
379
-
380
- // Show skill counts with project context
381
- const projectName = getProjectName();
382
- const teamSkillCount = skills.filter((s) => s.scope !== "project").length;
383
- const projectSkillCount = skills.filter((s) => s.scope === "project" && s.project_name === projectName).length;
384
- const skippedProjectCount = skills.filter((s) => s.scope === "project" && s.project_name !== projectName).length;
385
-
386
- console.log(` ${c("bold", teamSkillCount)} team skill(s)`);
387
- if (projectSkillCount > 0) {
388
- console.log(` ${c("bold", projectSkillCount)} project skill(s) for ${c("green", projectName)}`);
389
- }
390
- if (skippedProjectCount > 0) {
391
- console.log(c("dim", ` ${skippedProjectCount} project skill(s) skipped (different project)`));
392
- }
393
- console.log("");
394
-
395
727
  } catch (err) {
396
728
  console.log(c("red", `\n✗ Could not complete verification: ${err.message}`));
397
729
  console.log(c("dim", " Please check your internet connection and try again.\n"));
398
730
  process.exit(1);
399
731
  }
400
732
 
401
- if (skills.length === 0) {
402
- console.log(c("red", "No skills available. Contact your Framework Admin."));
733
+ // Extract all entity types from response
734
+ const skills = response.skills || [];
735
+ const agents = response.agents || [];
736
+ const commands = response.commands || [];
737
+ const hooks = response.hooks || [];
738
+ const rules = response.rules || [];
739
+ const mcpConfigs = response.mcp_configs || [];
740
+
741
+ const totalEntities = skills.length + agents.length + commands.length + hooks.length + rules.length + mcpConfigs.length;
742
+
743
+ if (totalEntities === 0) {
744
+ console.log(c("yellow", "\n⚠ No toolkit items are enabled for your team. Contact your Team Lead."));
403
745
  process.exit(1);
404
746
  }
405
747
 
406
- // 5. Install for each tool (project skills are filtered by project name inside)
407
- const filteredSkills = filterSkillsForProject(skills);
748
+ // Show toolkit summary
749
+ console.log(c("blue", "\n Toolkit Summary:"));
750
+ if (skills.length > 0) console.log(` ${c("green", skills.length.toString().padStart(2))} skill(s)`);
751
+ if (agents.length > 0) console.log(` ${c("green", agents.length.toString().padStart(2))} agent(s)`);
752
+ if (commands.length > 0) console.log(` ${c("green", commands.length.toString().padStart(2))} command(s)`);
753
+ if (hooks.length > 0) console.log(` ${c("green", hooks.length.toString().padStart(2))} hook(s)`);
754
+ if (rules.length > 0) console.log(` ${c("green", rules.length.toString().padStart(2))} rule(s)`);
755
+ if (mcpConfigs.length > 0) console.log(` ${c("green", mcpConfigs.length.toString().padStart(2))} MCP config(s)`);
756
+ console.log("");
757
+
758
+ // 6. Install for each tool
408
759
  for (const toolKey of tools) {
409
760
  const tool = TOOL_CONFIGS[toolKey];
410
761
  if (!tool) continue;
411
762
  console.log(c("yellow", `Installing for ${tool.name}...`));
412
- const count = installSkillsForTool(toolKey, skills);
413
763
 
414
- if (tool.format === "skill-folder") {
415
- for (const s of filteredSkills) {
416
- const label = s.scope === "project" ? `${s.name} ${c("dim", "(project)")}` : s.name;
417
- console.log(` ${c("green", "")} ${label}`);
764
+ // Skills
765
+ const skillCount = installSkillsForTool(toolKey, skills);
766
+ if (skillCount > 0) {
767
+ if (tool.format === "skill-folder") {
768
+ for (const s of filterSkillsForProject(skills)) {
769
+ console.log(` ${c("green", "✓")} ${c("dim", "skill:")} ${s.name}`);
770
+ }
771
+ } else {
772
+ console.log(` ${c("green", "✓")} ${skillCount} skills merged into ${tool.rulesFile}`);
773
+ }
774
+ }
775
+
776
+ // Agents
777
+ const agentCount = installAgentsForTool(toolKey, agents);
778
+ if (agentCount > 0) {
779
+ if (tool.format === "skill-folder") {
780
+ for (const a of agents) console.log(` ${c("green", "✓")} ${c("dim", "agent:")} ${a.name}`);
781
+ } else {
782
+ console.log(` ${c("green", "✓")} ${agentCount} agents merged`);
783
+ }
784
+ }
785
+
786
+ // Commands
787
+ const cmdCount = installCommandsForTool(toolKey, commands);
788
+ if (cmdCount > 0) {
789
+ if (tool.format === "skill-folder") {
790
+ for (const cmd of commands) console.log(` ${c("green", "✓")} ${c("dim", "command:")} /${cmd.name}`);
791
+ } else {
792
+ console.log(` ${c("green", "✓")} ${cmdCount} commands merged`);
793
+ }
794
+ }
795
+
796
+ // Hooks (Claude Code only)
797
+ const hookCount = installHooksForTool(toolKey, hooks);
798
+ if (hookCount > 0) {
799
+ for (const h of hooks) console.log(` ${c("green", "✓")} ${c("dim", "hook:")} ${h.name} (${h.event})`);
800
+ }
801
+
802
+ // Rules (filtered by detected languages)
803
+ const ruleCount = installRulesForTool(toolKey, rules, detectedLanguages);
804
+ if (ruleCount > 0) {
805
+ if (tool.format === "skill-folder") {
806
+ const installed = rules.filter((r) => r.language === "all" || !r.auto_detect || detectedLanguages.includes(r.language));
807
+ for (const r of installed) console.log(` ${c("green", "✓")} ${c("dim", "rule:")} ${r.name}`);
808
+ } else {
809
+ console.log(` ${c("green", "✓")} ${ruleCount} rules merged`);
810
+ }
811
+ }
812
+
813
+ // MCP Configs
814
+ const mcpCount = installMcpConfigsForTool(toolKey, mcpConfigs);
815
+ if (mcpCount > 0) {
816
+ for (const m of mcpConfigs) {
817
+ const envNote = m.required_env_vars?.length ? c("yellow", ` (needs: ${m.required_env_vars.join(", ")})`) : "";
818
+ console.log(` ${c("green", "✓")} ${c("dim", "mcp:")} ${m.name}${envNote}`);
418
819
  }
419
- } else {
420
- console.log(` ${c("green", "✓")} ${tool.rulesFile} (${count} skills merged)`);
421
820
  }
821
+
422
822
  console.log("");
423
823
  }
424
824
 
425
- // 6. Save config
825
+ // 7. Save config
826
+ const filteredSkills = filterSkillsForProject(skills);
827
+ const installedRules = rules.filter((r) => r.language === "all" || !r.auto_detect || detectedLanguages.includes(r.language));
828
+
426
829
  const config = {
427
830
  version: require(path.join(__dirname, "..", "package.json")).version,
428
831
  email,
@@ -431,13 +834,19 @@ async function cmdSetup() {
431
834
  source: "supabase",
432
835
  analyzeUrl: ANALYZE_FUNCTION_URL,
433
836
  scanUrl: SCAN_FUNCTION_URL,
837
+ detectedLanguages,
434
838
  tools,
435
839
  skills: filteredSkills.map((s) => ({ name: s.name, scope: s.scope, version: s.version, ...(s.project_name ? { project_name: s.project_name } : {}) })),
840
+ agents: agents.map((a) => ({ name: a.name, scope: a.scope, version: a.version })),
841
+ commands: commands.map((cmd) => ({ name: cmd.name, scope: cmd.scope, version: cmd.version })),
842
+ hooks: hooks.map((h) => ({ name: h.name, scope: h.scope, version: h.version })),
843
+ rules: installedRules.map((r) => ({ name: r.name, scope: r.scope, version: r.version, language: r.language })),
844
+ mcpConfigs: mcpConfigs.map((m) => ({ name: m.name, scope: m.scope, version: m.version })),
436
845
  installedAt: new Date().toISOString(),
437
846
  };
438
847
  saveConfig(config);
439
848
 
440
- // 7. Install post-commit analysis hook
849
+ // 8. Install post-commit analysis hook
441
850
  try {
442
851
  const { installHook } = require(path.join(__dirname, "..", "hooks", "install-hook.js"));
443
852
  const hookResult = installHook(PROJECT_ROOT);
@@ -445,14 +854,15 @@ async function cmdSetup() {
445
854
  console.log(`${c("green", "✓")} Post-commit analysis hook installed`);
446
855
  }
447
856
  } catch {
448
- // Hook installation is optional — don't fail setup
857
+ // Hook installation is optional
449
858
  }
450
859
 
451
- // 8. Summary
860
+ // 9. Summary
452
861
  console.log(c("blue", "\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"));
453
- console.log(c("green", "Setup complete!"));
454
- console.log(` ${skills.length} skills installed for ${tools.length} tool(s)`);
862
+ console.log(c("green", "Setup complete!"));
863
+ console.log(` ${totalEntities} items installed for ${tools.length} tool(s)`);
455
864
  if (teamName) console.log(` Team: ${teamName}`);
865
+ if (detectedLanguages.length > 0) console.log(` Languages: ${detectedLanguages.join(", ")}`);
456
866
  console.log(`\n Config saved to ${c("dim", ".ai-skills.json")}`);
457
867
  console.log(` To update: ${c("bold", "npx ai-skills update")}\n`);
458
868
  }
@@ -466,24 +876,20 @@ async function cmdUpdate() {
466
876
  process.exit(1);
467
877
  }
468
878
 
469
- const email = config.email;
879
+ let email = config.email;
470
880
  console.log(c("dim", `Updating for ${email}...`));
471
881
 
472
- let skills;
882
+ let response;
473
883
  let teamName = config.team;
474
884
 
475
885
  try {
476
- // Request OTP for the saved email
477
886
  const otpResult = await requestOtpForEmail(email);
478
887
  email = otpResult.email;
479
-
480
- // Verify OTP
481
- const response = await verifyOtp(email);
482
- skills = response.skills || [];
888
+ response = await verifyOtp(email);
483
889
  teamName = response.team?.name || config.team;
484
890
 
485
891
  if (response.team) {
486
- console.log(c("green", `✓ Team: ${teamName} (${skills.length} skills)`));
892
+ console.log(c("green", `✓ Team: ${teamName}`));
487
893
  } else {
488
894
  console.log(c("yellow", `⚠ ${response.message || "No team found."}`));
489
895
  }
@@ -493,26 +899,55 @@ async function cmdUpdate() {
493
899
  process.exit(1);
494
900
  }
495
901
 
902
+ const skills = response.skills || [];
903
+ const agents = response.agents || [];
904
+ const commands = response.commands || [];
905
+ const hooks = response.hooks || [];
906
+ const rules = response.rules || [];
907
+ const mcpConfigs = response.mcp_configs || [];
908
+ const detectedLanguages = detectLanguages();
909
+
496
910
  const tools = config.tools || detectTools();
497
911
  for (const toolKey of tools) {
498
912
  const tool = TOOL_CONFIGS[toolKey];
499
913
  if (!tool) continue;
500
- console.log(c("yellow", `Updating ${tool.name}...`));
914
+ console.log(c("yellow", `\nUpdating ${tool.name}...`));
915
+
501
916
  installSkillsForTool(toolKey, skills);
917
+ installAgentsForTool(toolKey, agents);
918
+ installCommandsForTool(toolKey, commands);
919
+ installHooksForTool(toolKey, hooks);
920
+ installRulesForTool(toolKey, rules, detectedLanguages);
921
+ installMcpConfigsForTool(toolKey, mcpConfigs);
502
922
 
503
923
  if (tool.format === "skill-folder") {
504
- for (const s of skills) console.log(` ${c("green", "✓")} ${s.name}`);
924
+ if (skills.length > 0) console.log(` ${c("green", "✓")} ${skills.length} skills`);
925
+ if (agents.length > 0) console.log(` ${c("green", "✓")} ${agents.length} agents`);
926
+ if (commands.length > 0) console.log(` ${c("green", "✓")} ${commands.length} commands`);
927
+ if (hooks.length > 0) console.log(` ${c("green", "✓")} ${hooks.length} hooks`);
928
+ const installedRules = rules.filter((r) => r.language === "all" || !r.auto_detect || detectedLanguages.includes(r.language));
929
+ if (installedRules.length > 0) console.log(` ${c("green", "✓")} ${installedRules.length} rules`);
930
+ if (mcpConfigs.length > 0) console.log(` ${c("green", "✓")} ${mcpConfigs.length} MCP configs`);
505
931
  } else {
506
932
  console.log(` ${c("green", "✓")} ${tool.rulesFile} updated`);
507
933
  }
508
934
  }
509
935
 
936
+ const installedRules = rules.filter((r) => r.language === "all" || !r.auto_detect || detectedLanguages.includes(r.language));
937
+ const totalEntities = skills.length + agents.length + commands.length + hooks.length + installedRules.length + mcpConfigs.length;
938
+
510
939
  config.updatedAt = new Date().toISOString();
511
940
  config.team = teamName;
941
+ config.detectedLanguages = detectedLanguages;
512
942
  config.skills = skills.map((s) => ({ name: s.name, scope: s.scope, version: s.version }));
943
+ config.agents = agents.map((a) => ({ name: a.name, scope: a.scope, version: a.version }));
944
+ config.commands = commands.map((cmd) => ({ name: cmd.name, scope: cmd.scope, version: cmd.version }));
945
+ config.hooks = hooks.map((h) => ({ name: h.name, scope: h.scope, version: h.version }));
946
+ config.rules = installedRules.map((r) => ({ name: r.name, scope: r.scope, version: r.version, language: r.language }));
947
+ config.mcpConfigs = mcpConfigs.map((m) => ({ name: m.name, scope: m.scope, version: m.version }));
513
948
  saveConfig(config);
514
949
 
515
- console.log(c("green", `\n✅ Updated ${skills.length} skills!\n`));
950
+ console.log(c("green", `\nUpdated! ${totalEntities} items across ${tools.length} tool(s).\n`));
516
951
  }
517
952
 
518
953
  function cmdStatus() {
@@ -526,13 +961,34 @@ function cmdStatus() {
526
961
  if (config) {
527
962
  console.log(`Email: ${c("bold", config.email)}`);
528
963
  console.log(`Team: ${config.team ? c("green", config.team) : c("yellow", "none")}`);
964
+ console.log(`Project: ${c("bold", config.project || getProjectName())}`);
529
965
  console.log(`Source: ${config.source === "supabase" ? c("green", "Supabase (live)") : c("yellow", "local fallback")}`);
530
966
  console.log(`Installed: ${config.installedAt?.split("T")[0]}`);
531
967
  if (config.updatedAt) console.log(`Updated: ${config.updatedAt.split("T")[0]}`);
532
968
  console.log(`Tools: ${config.tools?.map((t) => TOOL_CONFIGS[t]?.name || t).join(", ")}`);
533
- console.log(`\nSkills (${config.skills?.length || 0}):`);
534
- for (const s of config.skills || []) {
535
- console.log(` ${c("green", "✓")} ${s.name} ${c("dim", `v${s.version}`)} ${c("dim", `(${s.scope})`)}`);
969
+ if (config.detectedLanguages?.length) {
970
+ console.log(`Languages: ${config.detectedLanguages.join(", ")}`);
971
+ }
972
+
973
+ // Entity summary table
974
+ const entities = [
975
+ { label: "Skills", icon: "📝", items: config.skills },
976
+ { label: "Agents", icon: "🤖", items: config.agents },
977
+ { label: "Commands", icon: "⚡", items: config.commands },
978
+ { label: "Hooks", icon: "🪝", items: config.hooks },
979
+ { label: "Rules", icon: "📏", items: config.rules },
980
+ { label: "MCP Configs", icon: "🔌", items: config.mcpConfigs },
981
+ ];
982
+
983
+ console.log("");
984
+ for (const entity of entities) {
985
+ const items = entity.items || [];
986
+ if (items.length === 0) continue;
987
+ console.log(`${c("bold", entity.label)} (${items.length}):`);
988
+ for (const item of items) {
989
+ const extra = item.language ? ` ${c("dim", `[${item.language}]`)}` : "";
990
+ console.log(` ${c("green", "✓")} ${item.name} ${c("dim", `v${item.version}`)} ${c("dim", `(${item.scope})`)}${extra}`);
991
+ }
536
992
  }
537
993
  } else {
538
994
  console.log(`Installed: ${c("red", "No")} — run 'npx ai-skills setup'`);
@@ -542,6 +998,15 @@ function cmdStatus() {
542
998
  for (const [key, tool] of Object.entries(TOOL_CONFIGS)) {
543
999
  console.log(` ${tool.detect() ? c("green", "●") : c("dim", "○")} ${tool.name}`);
544
1000
  }
1001
+
1002
+ const languages = detectLanguages();
1003
+ if (languages.length > 0) {
1004
+ console.log(`\nDetected languages:`);
1005
+ for (const lang of languages) {
1006
+ console.log(` ${c("cyan", "●")} ${lang}`);
1007
+ }
1008
+ }
1009
+
545
1010
  console.log("");
546
1011
  }
547
1012
 
@@ -558,7 +1023,7 @@ function cmdList() {
558
1023
  console.log(` ${s.name.padEnd(22)} ${c("dim", `v${s.version}`)} ${c("dim", `(${s.scope})`)}`);
559
1024
  }
560
1025
  console.log(c("dim", `\n ${skills.length} skills available`));
561
- console.log(c("dim", " Note: 'npx ai-skills setup' installs team-specific skills from Supabase\n"));
1026
+ console.log(c("dim", " Note: 'npx ai-skills setup' installs team-specific toolkit from Supabase\n"));
562
1027
  }
563
1028
 
564
1029
  function cmdDoctor() {
@@ -573,6 +1038,18 @@ function cmdDoctor() {
573
1038
  } else {
574
1039
  console.log(c("green", `✓ Config found (${config.email}, team: ${config.team || "none"})`));
575
1040
 
1041
+ // Count entities
1042
+ const entityCounts = [];
1043
+ if (config.skills?.length) entityCounts.push(`${config.skills.length} skills`);
1044
+ if (config.agents?.length) entityCounts.push(`${config.agents.length} agents`);
1045
+ if (config.commands?.length) entityCounts.push(`${config.commands.length} commands`);
1046
+ if (config.hooks?.length) entityCounts.push(`${config.hooks.length} hooks`);
1047
+ if (config.rules?.length) entityCounts.push(`${config.rules.length} rules`);
1048
+ if (config.mcpConfigs?.length) entityCounts.push(`${config.mcpConfigs.length} MCP configs`);
1049
+ if (entityCounts.length > 0) {
1050
+ console.log(c("green", `✓ Installed: ${entityCounts.join(", ")}`));
1051
+ }
1052
+
576
1053
  // Check tool files
577
1054
  for (const toolKey of config.tools || []) {
578
1055
  const tool = TOOL_CONFIGS[toolKey];
@@ -588,6 +1065,50 @@ function cmdDoctor() {
588
1065
  console.log(c("red", `✗ ${tool.name}: ${tool.skillsDir}/ missing`));
589
1066
  issues++;
590
1067
  }
1068
+
1069
+ // Check agents dir
1070
+ const agentsDir = path.join(PROJECT_ROOT, ".claude", "agents");
1071
+ if (fs.existsSync(agentsDir)) {
1072
+ const agentCount = fs.readdirSync(agentsDir).filter((f) => f.endsWith(".md")).length;
1073
+ console.log(c("green", `✓ ${tool.name}: ${agentCount} agents in .claude/agents/`));
1074
+ }
1075
+
1076
+ // Check commands dir
1077
+ const commandsDir = path.join(PROJECT_ROOT, ".claude", "commands");
1078
+ if (fs.existsSync(commandsDir)) {
1079
+ const cmdCount = fs.readdirSync(commandsDir).filter((f) => f.endsWith(".md")).length;
1080
+ console.log(c("green", `✓ ${tool.name}: ${cmdCount} commands in .claude/commands/`));
1081
+ }
1082
+
1083
+ // Check rules dir
1084
+ const rulesDir = path.join(PROJECT_ROOT, ".claude", "rules");
1085
+ if (fs.existsSync(rulesDir)) {
1086
+ const ruleCount = fs.readdirSync(rulesDir).filter((f) => f.endsWith(".md")).length;
1087
+ console.log(c("green", `✓ ${tool.name}: ${ruleCount} rules in .claude/rules/`));
1088
+ }
1089
+
1090
+ // Check hooks
1091
+ const settingsPath = path.join(PROJECT_ROOT, ".claude", "settings.local.json");
1092
+ if (fs.existsSync(settingsPath)) {
1093
+ try {
1094
+ const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
1095
+ if (settings.hooks) {
1096
+ const hookCount = Object.values(settings.hooks).flat().length;
1097
+ console.log(c("green", `✓ ${tool.name}: ${hookCount} hooks in settings.local.json`));
1098
+ }
1099
+ } catch { /* ignore */ }
1100
+ }
1101
+
1102
+ // Check MCP configs
1103
+ const mcpPath = path.join(PROJECT_ROOT, ".claude", "mcp-servers.json");
1104
+ if (fs.existsSync(mcpPath)) {
1105
+ try {
1106
+ const mcp = JSON.parse(fs.readFileSync(mcpPath, "utf-8"));
1107
+ const mcpCount = Object.keys(mcp.mcpServers || mcp).length;
1108
+ console.log(c("green", `✓ ${tool.name}: ${mcpCount} MCP servers configured`));
1109
+ } catch { /* ignore */ }
1110
+ }
1111
+
591
1112
  } else if (tool.rulesFile) {
592
1113
  const rulesPath = path.join(PROJECT_ROOT, tool.rulesFile);
593
1114
  if (fs.existsSync(rulesPath)) {
@@ -620,7 +1141,7 @@ function cmdDoctor() {
620
1141
  function finish() {
621
1142
  console.log(
622
1143
  issues === 0
623
- ? c("green", "\n✅ Everything looks good!\n")
1144
+ ? c("green", "\nEverything looks good!\n")
624
1145
  : c("yellow", `\n⚠ ${issues} issue(s) found.\n`)
625
1146
  );
626
1147
  }
@@ -666,7 +1187,6 @@ async function cmdAnalyze() {
666
1187
  commitMessage = require("child_process").execSync('git log -1 --format="%s"', execOpt).trim();
667
1188
  filesChanged = require("child_process").execSync("git diff --name-only HEAD~1 HEAD", execOpt).trim().split("\n").filter(Boolean);
668
1189
  } else {
669
- // Analyze staged changes or last commit
670
1190
  diff = require("child_process").execSync("git diff --cached", execOpt).trim();
671
1191
  if (!diff) {
672
1192
  diff = require("child_process").execSync("git diff HEAD~1 HEAD", execOpt).trim();
@@ -690,7 +1210,6 @@ async function cmdAnalyze() {
690
1210
  process.exit(0);
691
1211
  }
692
1212
 
693
- // Truncate diff if needed
694
1213
  if (diff.length > 10000) {
695
1214
  diff = diff.slice(0, 4000) + "\n\n[...truncated...]\n\n" + diff.slice(-4000);
696
1215
  }
@@ -699,7 +1218,6 @@ async function cmdAnalyze() {
699
1218
  console.log(c("dim", `Commit: ${commitHash?.slice(0, 8)} — ${commitMessage}`));
700
1219
  console.log(c("dim", `Branch: ${branch}\n`));
701
1220
 
702
- // Get project name
703
1221
  let projectName = null;
704
1222
  try {
705
1223
  const pkgPath = path.join(PROJECT_ROOT, "package.json");
@@ -726,7 +1244,6 @@ async function cmdAnalyze() {
726
1244
  throw new Error(result.error);
727
1245
  }
728
1246
 
729
- // Display results
730
1247
  console.log(c("blue", "━━━ Analysis Results ━━━\n"));
731
1248
  const score = result.overall_score || 0;
732
1249
  const scoreColor = score >= 80 ? "green" : score >= 50 ? "yellow" : "red";
@@ -778,9 +1295,9 @@ switch (command) {
778
1295
  ${c("blue", "AI Skills Framework")} — @valentia-ai-skills/framework
779
1296
 
780
1297
  Usage:
781
- npx ai-skills setup Enter email → fetch team's skills → install
782
- npx ai-skills update Re-fetch and update skills for your team
783
- npx ai-skills status Show installed skills, team, and tools
1298
+ npx ai-skills setup Enter email → fetch team's toolkit → install
1299
+ npx ai-skills update Re-fetch and update toolkit for your team
1300
+ npx ai-skills status Show installed toolkit, team, and tools
784
1301
  npx ai-skills list List locally bundled skills
785
1302
  npx ai-skills analyze Analyze last commit against active skills
786
1303
  npx ai-skills doctor Health check (config + API + tools)
@@ -788,6 +1305,8 @@ Usage:
788
1305
  Flags:
789
1306
  analyze --last Analyze the most recent commit
790
1307
 
1308
+ Toolkit includes: skills, agents, commands, hooks, rules, MCP configs.
1309
+
791
1310
  Environment:
792
1311
  AI_SKILLS_API_URL Override the Supabase Edge Function URL
793
1312
  `); break;