add-skill-kit 3.2.3 → 3.2.4

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.
Files changed (71) hide show
  1. package/lib/agent-cli/lib/audit.js +154 -0
  2. package/lib/agent-cli/lib/audit.test.js +100 -0
  3. package/lib/agent-cli/lib/auto-learn.js +319 -0
  4. package/lib/agent-cli/lib/auto_preview.py +148 -0
  5. package/lib/agent-cli/lib/backup.js +138 -0
  6. package/lib/agent-cli/lib/backup.test.js +78 -0
  7. package/lib/agent-cli/lib/checklist.py +222 -0
  8. package/lib/agent-cli/lib/cognitive-lesson.js +476 -0
  9. package/lib/agent-cli/lib/completion.js +149 -0
  10. package/lib/agent-cli/lib/config.js +35 -0
  11. package/lib/agent-cli/lib/eslint-fix.js +238 -0
  12. package/lib/agent-cli/lib/evolution-signal.js +215 -0
  13. package/lib/agent-cli/lib/export.js +86 -0
  14. package/lib/agent-cli/lib/export.test.js +65 -0
  15. package/lib/agent-cli/lib/fix.js +337 -0
  16. package/lib/agent-cli/lib/fix.test.js +80 -0
  17. package/lib/agent-cli/lib/gemini-export.js +83 -0
  18. package/lib/agent-cli/lib/generate-registry.js +42 -0
  19. package/lib/agent-cli/lib/hooks/install-hooks.js +152 -0
  20. package/lib/agent-cli/lib/hooks/lint-learn.js +172 -0
  21. package/lib/agent-cli/lib/ignore.js +116 -0
  22. package/lib/agent-cli/lib/ignore.test.js +58 -0
  23. package/lib/agent-cli/lib/init.js +124 -0
  24. package/lib/agent-cli/lib/learn.js +255 -0
  25. package/lib/agent-cli/lib/learn.test.js +70 -0
  26. package/lib/agent-cli/lib/migrate-to-v4.js +322 -0
  27. package/lib/agent-cli/lib/proposals.js +199 -0
  28. package/lib/agent-cli/lib/proposals.test.js +56 -0
  29. package/lib/agent-cli/lib/recall.js +820 -0
  30. package/lib/agent-cli/lib/recall.test.js +107 -0
  31. package/lib/agent-cli/lib/selfevolution-bridge.js +167 -0
  32. package/lib/agent-cli/lib/session_manager.py +120 -0
  33. package/lib/agent-cli/lib/settings.js +203 -0
  34. package/lib/agent-cli/lib/skill-learn.js +296 -0
  35. package/lib/agent-cli/lib/stats.js +132 -0
  36. package/lib/agent-cli/lib/stats.test.js +94 -0
  37. package/lib/agent-cli/lib/types.js +33 -0
  38. package/lib/agent-cli/lib/ui/audit-ui.js +146 -0
  39. package/lib/agent-cli/lib/ui/backup-ui.js +107 -0
  40. package/lib/agent-cli/lib/ui/clack-helpers.js +317 -0
  41. package/lib/agent-cli/lib/ui/common.js +83 -0
  42. package/lib/agent-cli/lib/ui/completion-ui.js +126 -0
  43. package/lib/agent-cli/lib/ui/custom-select.js +69 -0
  44. package/lib/agent-cli/lib/ui/dashboard-ui.js +123 -0
  45. package/lib/agent-cli/lib/ui/evolution-signals-ui.js +107 -0
  46. package/lib/agent-cli/lib/ui/export-ui.js +94 -0
  47. package/lib/agent-cli/lib/ui/fix-all-ui.js +191 -0
  48. package/lib/agent-cli/lib/ui/help-ui.js +49 -0
  49. package/lib/agent-cli/lib/ui/index.js +169 -0
  50. package/lib/agent-cli/lib/ui/init-ui.js +56 -0
  51. package/lib/agent-cli/lib/ui/knowledge-ui.js +55 -0
  52. package/lib/agent-cli/lib/ui/learn-ui.js +706 -0
  53. package/lib/agent-cli/lib/ui/lessons-ui.js +148 -0
  54. package/lib/agent-cli/lib/ui/pretty.js +145 -0
  55. package/lib/agent-cli/lib/ui/proposals-ui.js +99 -0
  56. package/lib/agent-cli/lib/ui/recall-ui.js +342 -0
  57. package/lib/agent-cli/lib/ui/routing-demo.js +79 -0
  58. package/lib/agent-cli/lib/ui/routing-ui.js +325 -0
  59. package/lib/agent-cli/lib/ui/settings-ui.js +381 -0
  60. package/lib/agent-cli/lib/ui/stats-ui.js +123 -0
  61. package/lib/agent-cli/lib/ui/watch-ui.js +236 -0
  62. package/lib/agent-cli/lib/verify_all.py +327 -0
  63. package/lib/agent-cli/lib/watcher.js +181 -0
  64. package/lib/agent-cli/lib/watcher.test.js +85 -0
  65. package/lib/agent-cli/package.json +51 -0
  66. package/lib/agentskillskit-cli/README.md +21 -0
  67. package/lib/agentskillskit-cli/ag-smart.js +158 -0
  68. package/lib/agentskillskit-cli/package.json +51 -0
  69. package/package.json +10 -6
  70. /package/{node_modules/agentskillskit-cli → lib/agent-cli}/README.md +0 -0
  71. /package/{node_modules/agentskillskit-cli → lib/agent-cli}/bin/ag-smart.js +0 -0
@@ -0,0 +1,296 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Skill Learn - Auto-update SKILL.md from Hot Patterns
4
+ *
5
+ * When patterns reach threshold hits, automatically add them
6
+ * to the relevant SKILL.md file as learned rules.
7
+ *
8
+ * This makes the Self-Learning Engine truly "self-learning"!
9
+ *
10
+ * Usage: ag-smart sync-skills
11
+ */
12
+
13
+ import fs from "fs";
14
+ import path from "path";
15
+ import { loadKnowledge, saveKnowledge } from "./recall.js";
16
+ import { AGENT_DIR, VERSION } from "./config.js";
17
+
18
+ // ============================================================================
19
+ // CONFIGURATION
20
+ // ============================================================================
21
+
22
+ const SYNC_THRESHOLD = 10; // Hits required before syncing to SKILL.md
23
+ const SKILLS_DIR = path.join(AGENT_DIR, "skills");
24
+
25
+ // Category to skill folder mapping
26
+ const CATEGORY_MAP = {
27
+ "general": "clean-code",
28
+ "security": "vulnerability-scanner",
29
+ "code-quality": "clean-code",
30
+ "performance": "performance-profiling",
31
+ "testing": "testing-patterns",
32
+ "react": "react-patterns",
33
+ "api": "api-patterns",
34
+ "database": "database-design"
35
+ };
36
+
37
+ // ============================================================================
38
+ // SKILL UPDATER
39
+ // ============================================================================
40
+
41
+ /**
42
+ * Find the SKILL.md file for a category
43
+ * @param {string} category
44
+ * @returns {string|null}
45
+ */
46
+ function findSkillFile(category) {
47
+ const skillFolder = CATEGORY_MAP[category] || CATEGORY_MAP["general"];
48
+ const skillPath = path.join(SKILLS_DIR, skillFolder, "SKILL.md");
49
+
50
+ if (fs.existsSync(skillPath)) {
51
+ return skillPath;
52
+ }
53
+
54
+ // Fallback to clean-code
55
+ const fallback = path.join(SKILLS_DIR, "clean-code", "SKILL.md");
56
+ return fs.existsSync(fallback) ? fallback : null;
57
+ }
58
+
59
+ /**
60
+ * Parse SKILL.md structure to find best insertion point
61
+ * @param {string} skillPath
62
+ * @returns {{ sections: Array<{name: string, start: number, end: number}>, content: string }}
63
+ */
64
+ function parseSkillStructure(skillPath) {
65
+ const content = fs.readFileSync(skillPath, "utf8");
66
+ const lines = content.split("\n");
67
+ const sections = [];
68
+
69
+ let currentSection = null;
70
+
71
+ lines.forEach((line, idx) => {
72
+ // Detect section headers (## or ###)
73
+ const headerMatch = line.match(/^(#{2,3})\s+(.+)/);
74
+ if (headerMatch) {
75
+ if (currentSection) {
76
+ currentSection.end = idx - 1;
77
+ sections.push(currentSection);
78
+ }
79
+ currentSection = {
80
+ name: headerMatch[2].trim(),
81
+ level: headerMatch[1].length,
82
+ start: idx,
83
+ end: lines.length - 1
84
+ };
85
+ }
86
+ });
87
+
88
+ if (currentSection) {
89
+ sections.push(currentSection);
90
+ }
91
+
92
+ return { sections, content, lines };
93
+ }
94
+
95
+ /**
96
+ * Find best section to insert pattern based on category
97
+ * @param {Array} sections
98
+ * @param {string} category
99
+ * @returns {number} - Line number to insert at
100
+ */
101
+ function findInsertionPoint(sections, category) {
102
+ // Look for "Auto-learned" section first
103
+ const autoSection = sections.find(s =>
104
+ s.name.toLowerCase().includes("auto-learned") ||
105
+ s.name.toLowerCase().includes("learned patterns")
106
+ );
107
+ if (autoSection) {
108
+ return autoSection.end + 1;
109
+ }
110
+
111
+ // Look for relevant category section
112
+ const categoryMap = {
113
+ "security": ["security", "vulnerab"],
114
+ "performance": ["performance", "optim"],
115
+ "code-quality": ["quality", "best practice", "rule"],
116
+ "testing": ["test", "spec"]
117
+ };
118
+
119
+ const keywords = categoryMap[category] || ["rule", "pattern"];
120
+ const matchSection = sections.find(s =>
121
+ keywords.some(kw => s.name.toLowerCase().includes(kw))
122
+ );
123
+ if (matchSection) {
124
+ return matchSection.end + 1;
125
+ }
126
+
127
+ // Default: append at end
128
+ return -1; // -1 means append
129
+ }
130
+
131
+ /**
132
+ * Generate rule entry for SKILL.md
133
+ * @param {object} lesson
134
+ * @returns {string}
135
+ */
136
+ function generateRuleEntry(lesson) {
137
+ const date = new Date().toISOString().split("T")[0];
138
+ return `
139
+ ### 🤖 Auto-learned: ${lesson.id}
140
+
141
+ > **Pattern:** \`${lesson.pattern}\`
142
+ >
143
+ > ${lesson.message}
144
+
145
+ - **Severity:** ${lesson.severity}
146
+ - **Detected:** ${lesson.hitCount} times
147
+ - **Added:** ${date}
148
+
149
+ ---
150
+ `;
151
+ }
152
+
153
+ /**
154
+ * Check if pattern already exists in SKILL.md
155
+ */
156
+ function patternExistsInSkill(skillPath, pattern) {
157
+ const content = fs.readFileSync(skillPath, "utf8");
158
+ // Check multiple formats
159
+ return content.includes(`\`${pattern}\``) ||
160
+ content.includes(`Pattern:** \`${pattern}\``) ||
161
+ content.includes(`"${pattern}"`) ||
162
+ content.includes(`'${pattern}'`);
163
+ }
164
+
165
+ /**
166
+ * Insert rule at specific line or append
167
+ * @param {string} skillPath
168
+ * @param {object} lesson
169
+ * @param {string} category
170
+ */
171
+ function insertRule(skillPath, lesson, category) {
172
+ const { sections, content, lines } = parseSkillStructure(skillPath);
173
+ const insertPoint = findInsertionPoint(sections, category);
174
+ const entry = generateRuleEntry(lesson);
175
+
176
+ if (insertPoint === -1 || insertPoint >= lines.length) {
177
+ // Append at end
178
+ fs.appendFileSync(skillPath, entry, "utf8");
179
+ } else {
180
+ // Insert at specific line
181
+ lines.splice(insertPoint, 0, entry);
182
+ fs.writeFileSync(skillPath, lines.join("\n"), "utf8");
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Sync hot patterns to SKILL.md files
188
+ */
189
+ function syncSkills() {
190
+ console.log(`\n🔄 Skill Sync v${VERSION}`);
191
+ console.log(`📊 Threshold: ${SYNC_THRESHOLD} hits\n`);
192
+ console.log("─".repeat(50));
193
+
194
+ const db = loadKnowledge();
195
+
196
+ if (!db.lessons || db.lessons.length === 0) {
197
+ console.log("\nℹ️ No lessons learned yet.");
198
+ return { synced: 0, skipped: 0 };
199
+ }
200
+
201
+ // Find hot patterns
202
+ const hotPatterns = db.lessons.filter(l => (l.hitCount || 0) >= SYNC_THRESHOLD && !l.syncedToSkill);
203
+
204
+ if (hotPatterns.length === 0) {
205
+ console.log("\nℹ️ No patterns have reached the sync threshold yet.");
206
+ console.log(` Patterns need ${SYNC_THRESHOLD}+ hits to be synced to SKILL.md\n`);
207
+
208
+ // Show progress
209
+ const inProgress = db.lessons.filter(l => l.hitCount > 0 && l.hitCount < SYNC_THRESHOLD);
210
+ if (inProgress.length > 0) {
211
+ console.log("📈 Patterns in progress:\n");
212
+ inProgress.forEach(l => {
213
+ const progress = Math.round((l.hitCount / SYNC_THRESHOLD) * 100);
214
+ const bar = "█".repeat(Math.floor(progress / 10)) + "░".repeat(10 - Math.floor(progress / 10));
215
+ console.log(` [${l.id}] ${l.hitCount}/${SYNC_THRESHOLD} hits (${progress}%)`);
216
+ console.log(` ${bar}`);
217
+ });
218
+ }
219
+ return { synced: 0, skipped: 0 };
220
+ }
221
+
222
+ console.log(`\n🔥 Found ${hotPatterns.length} hot pattern(s) ready to sync:\n`);
223
+
224
+ let synced = 0;
225
+ let skipped = 0;
226
+
227
+ hotPatterns.forEach(lesson => {
228
+ const category = lesson.category || "general";
229
+ const skillPath = findSkillFile(category);
230
+
231
+ if (!skillPath) {
232
+ console.log(`⚠️ [${lesson.id}] No SKILL.md found for category: ${category}`);
233
+ skipped++;
234
+ return;
235
+ }
236
+
237
+ // Check if already synced
238
+ if (patternExistsInSkill(skillPath, lesson.pattern)) {
239
+ console.log(`⏭️ [${lesson.id}] Already in ${path.basename(path.dirname(skillPath))}/SKILL.md`);
240
+ lesson.syncedToSkill = true;
241
+ skipped++;
242
+ return;
243
+ }
244
+
245
+ // Append to SKILL.md
246
+ const entry = generateRuleEntry(lesson);
247
+ fs.appendFileSync(skillPath, entry, "utf8");
248
+
249
+ // Mark as synced
250
+ lesson.syncedToSkill = true;
251
+ lesson.syncedAt = new Date().toISOString();
252
+
253
+ console.log(`✅ [${lesson.id}] → ${path.basename(path.dirname(skillPath))}/SKILL.md`);
254
+ console.log(` Pattern: /${lesson.pattern}/`);
255
+ console.log(` Hits: ${lesson.hitCount}`);
256
+ synced++;
257
+ });
258
+
259
+ // Save updated knowledge base
260
+ saveKnowledge(db);
261
+
262
+ console.log(`\n${"─".repeat(50)}`);
263
+ console.log(`📊 Summary: ${synced} synced, ${skipped} skipped`);
264
+
265
+ if (synced > 0) {
266
+ console.log(`\n🎉 SKILL.md files updated! The agent will now remember these patterns.`);
267
+ }
268
+
269
+ return { synced, skipped };
270
+ }
271
+
272
+ // ============================================================================
273
+ // CLI
274
+ // ============================================================================
275
+
276
+ const args = process.argv.slice(2);
277
+
278
+ if (args.includes("--help")) {
279
+ console.log(`
280
+ 🔄 Skill Sync - Auto-update SKILL.md
281
+
282
+ Usage:
283
+ ag-smart sync-skills
284
+
285
+ When patterns reach ${SYNC_THRESHOLD}+ violations, they are automatically
286
+ added to the relevant SKILL.md file as learned rules.
287
+
288
+ This makes the Self-Learning Engine truly "self-learning"!
289
+
290
+ Options:
291
+ --help Show this help
292
+ `);
293
+ process.exit(0);
294
+ }
295
+
296
+ syncSkills();
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Smart Stats - Knowledge Base Statistics
4
+ *
5
+ * Display statistics about the knowledge base:
6
+ * - Total lessons learned
7
+ * - Most triggered patterns
8
+ * - Violation trends
9
+ *
10
+ * Usage: ag-smart stats
11
+ */
12
+
13
+ import { loadKnowledge } from "./recall.js";
14
+ import { VERSION } from "./config.js";
15
+ import * as p from "@clack/prompts";
16
+ import pc from "picocolors";
17
+
18
+ // ============================================================================
19
+ // STATS DISPLAY
20
+ // ============================================================================
21
+
22
+ function displayStats() {
23
+ const db = loadKnowledge();
24
+
25
+ p.intro(pc.cyan(`📊 Smart Agent Knowledge Base v${VERSION}`));
26
+
27
+ if (!db.lessons || db.lessons.length === 0) {
28
+ p.note("No lessons learned yet.\nUse 'ag-smart learn' to add patterns.", pc.dim("Empty"));
29
+ return;
30
+ }
31
+
32
+ // Summary
33
+ const totalHits = db.lessons.reduce((sum, l) => sum + (l.hitCount || 0), 0);
34
+ const errorCount = db.lessons.filter(l => l.severity === "ERROR").length;
35
+ const warningCount = db.lessons.filter(l => l.severity === "WARNING").length;
36
+
37
+ const summaryLines = [
38
+ `📚 Total Lessons: ${pc.bold(db.lessons.length)}`,
39
+ `🎯 Total Violations: ${pc.bold(totalHits)}`,
40
+ `${pc.red("❌")} ERROR patterns: ${errorCount}`,
41
+ `${pc.yellow("⚠️")} WARNING patterns: ${warningCount}`
42
+ ];
43
+ p.note(summaryLines.join("\n"), pc.dim("Summary"));
44
+
45
+ // Most triggered
46
+ const sorted = [...db.lessons]
47
+ .filter(l => l.hitCount > 0)
48
+ .sort((a, b) => (b.hitCount || 0) - (a.hitCount || 0))
49
+ .slice(0, 5);
50
+
51
+ if (sorted.length > 0) {
52
+ const triggeredLines = sorted.map((lesson, idx) => {
53
+ const bar = "█".repeat(Math.min(15, Math.ceil((lesson.hitCount / (sorted[0].hitCount || 1)) * 15)));
54
+ return `${idx + 1}. [${lesson.id}] ${lesson.hitCount} hits\n ${pc.green(bar)}\n ${pc.dim(lesson.message)}`;
55
+ });
56
+ p.note(triggeredLines.join("\n\n"), pc.dim("🔥 Most Triggered"));
57
+ }
58
+
59
+ // Auto-escalated
60
+ const escalated = db.lessons.filter(l => l.autoEscalated);
61
+ if (escalated.length > 0) {
62
+ const escalatedLines = escalated.map(l =>
63
+ `[${l.id}] → Escalated after ${l.hitCount} violations`
64
+ );
65
+ p.note(escalatedLines.join("\n"), pc.dim("⚡ Auto-Escalated"));
66
+ }
67
+
68
+ // Recent activity
69
+ const withHits = db.lessons.filter(l => l.lastHit);
70
+ if (withHits.length > 0) {
71
+ const recent = [...withHits]
72
+ .sort((a, b) => new Date(b.lastHit) - new Date(a.lastHit))
73
+ .slice(0, 3);
74
+
75
+ const recentLines = recent.map(l => {
76
+ const timeAgo = getTimeAgo(new Date(l.lastHit));
77
+ return `[${l.id}] Last hit: ${timeAgo}`;
78
+ });
79
+ p.note(recentLines.join("\n"), pc.dim("🕐 Recent Activity"));
80
+ }
81
+
82
+ // Sources breakdown
83
+ const sources = {};
84
+ db.lessons.forEach(l => {
85
+ const src = l.source || "manual";
86
+ sources[src] = (sources[src] || 0) + 1;
87
+ });
88
+
89
+ const sourceLines = Object.entries(sources).map(([src, count]) => {
90
+ const icon = src === "manual" ? "✍️" : src === "eslint" ? "🔧" : "🧪";
91
+ return `${icon} ${src}: ${count}`;
92
+ });
93
+ p.note(sourceLines.join("\n"), pc.dim("📥 Lesson Sources"));
94
+
95
+ p.outro(pc.green("Stats complete"));
96
+ }
97
+
98
+ /**
99
+ * Get human-readable time ago string
100
+ */
101
+ function getTimeAgo(date) {
102
+ const seconds = Math.floor((Date.now() - date) / 1000);
103
+
104
+ if (seconds < 60) return "just now";
105
+ if (seconds < 3600) return `${Math.floor(seconds / 60)} minutes ago`;
106
+ if (seconds < 86400) return `${Math.floor(seconds / 3600)} hours ago`;
107
+ return `${Math.floor(seconds / 86400)} days ago`;
108
+ }
109
+
110
+ // ============================================================================
111
+ // CLI
112
+ // ============================================================================
113
+
114
+ const args = process.argv.slice(2);
115
+
116
+ if (args.includes("--help")) {
117
+ console.log(`
118
+ 📊 Smart Stats - Knowledge Base Statistics
119
+
120
+ Usage:
121
+ ag-smart stats
122
+
123
+ Shows:
124
+ - Total lessons and violations
125
+ - Most triggered patterns
126
+ - Auto-escalated patterns
127
+ - Recent activity
128
+ `);
129
+ process.exit(0);
130
+ }
131
+
132
+ displayStats();
@@ -0,0 +1,94 @@
1
+ /**
2
+ * @fileoverview Tests for stats.js functionality
3
+ */
4
+ import { describe, it, expect } from "vitest";
5
+
6
+ describe("Stats - Statistics Calculation", () => {
7
+ it("calculates total hits from lessons", () => {
8
+ const lessons = [
9
+ { id: "LEARN-001", hitCount: 5 },
10
+ { id: "LEARN-002", hitCount: 3 },
11
+ { id: "LEARN-003", hitCount: 0 }
12
+ ];
13
+
14
+ const totalHits = lessons.reduce((sum, l) => sum + (l.hitCount || 0), 0);
15
+ expect(totalHits).toBe(8);
16
+ });
17
+
18
+ it("counts severity types correctly", () => {
19
+ const lessons = [
20
+ { id: "LEARN-001", severity: "ERROR" },
21
+ { id: "LEARN-002", severity: "WARNING" },
22
+ { id: "LEARN-003", severity: "ERROR" },
23
+ { id: "LEARN-004", severity: "WARNING" }
24
+ ];
25
+
26
+ const errorCount = lessons.filter(l => l.severity === "ERROR").length;
27
+ const warningCount = lessons.filter(l => l.severity === "WARNING").length;
28
+
29
+ expect(errorCount).toBe(2);
30
+ expect(warningCount).toBe(2);
31
+ });
32
+
33
+ it("sorts by hit count descending", () => {
34
+ const lessons = [
35
+ { id: "LEARN-001", hitCount: 3 },
36
+ { id: "LEARN-002", hitCount: 10 },
37
+ { id: "LEARN-003", hitCount: 5 }
38
+ ];
39
+
40
+ const sorted = [...lessons].sort((a, b) => (b.hitCount || 0) - (a.hitCount || 0));
41
+
42
+ expect(sorted[0].id).toBe("LEARN-002");
43
+ expect(sorted[1].id).toBe("LEARN-003");
44
+ expect(sorted[2].id).toBe("LEARN-001");
45
+ });
46
+
47
+ it("filters auto-escalated patterns", () => {
48
+ const lessons = [
49
+ { id: "LEARN-001", autoEscalated: true },
50
+ { id: "LEARN-002", autoEscalated: false },
51
+ { id: "LEARN-003", autoEscalated: true }
52
+ ];
53
+
54
+ const escalated = lessons.filter(l => l.autoEscalated);
55
+ expect(escalated.length).toBe(2);
56
+ });
57
+
58
+ it("groups by source correctly", () => {
59
+ const lessons = [
60
+ { id: "LEARN-001", source: "manual" },
61
+ { id: "LEARN-002", source: "eslint" },
62
+ { id: "LEARN-003", source: "manual" },
63
+ { id: "LEARN-004", source: "test-failure" }
64
+ ];
65
+
66
+ const sources = {};
67
+ lessons.forEach(l => {
68
+ const src = l.source || "manual";
69
+ sources[src] = (sources[src] || 0) + 1;
70
+ });
71
+
72
+ expect(sources["manual"]).toBe(2);
73
+ expect(sources["eslint"]).toBe(1);
74
+ expect(sources["test-failure"]).toBe(1);
75
+ });
76
+ });
77
+
78
+ describe("Stats - Time Formatting", () => {
79
+ it("formats time ago correctly", () => {
80
+ const getTimeAgo = (date) => {
81
+ const seconds = Math.floor((Date.now() - date) / 1000);
82
+ if (seconds < 60) return "just now";
83
+ if (seconds < 3600) return `${Math.floor(seconds / 60)} minutes ago`;
84
+ if (seconds < 86400) return `${Math.floor(seconds / 3600)} hours ago`;
85
+ return `${Math.floor(seconds / 86400)} days ago`;
86
+ };
87
+
88
+ const now = Date.now();
89
+ expect(getTimeAgo(now - 30000)).toBe("just now");
90
+ expect(getTimeAgo(now - 120000)).toBe("2 minutes ago");
91
+ expect(getTimeAgo(now - 7200000)).toBe("2 hours ago");
92
+ expect(getTimeAgo(now - 172800000)).toBe("2 days ago");
93
+ });
94
+ });
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @fileoverview Type Definitions for agent-skill-kit CLI
3
+ */
4
+
5
+ /**
6
+ * @typedef {Object} Lesson
7
+ * @property {string} id - Lesson ID (e.g., LEARN-001)
8
+ * @property {string} pattern - Regex pattern to match
9
+ * @property {string} message - Explanation message
10
+ * @property {'WARNING'|'ERROR'} severity - Severity level
11
+ * @property {string} addedAt - ISO timestamp
12
+ */
13
+
14
+ /**
15
+ * @typedef {Object} KnowledgeBase
16
+ * @property {Lesson[]} lessons - Array of learned lessons
17
+ */
18
+
19
+ /**
20
+ * @typedef {Object} AuditResult
21
+ * @property {string} file - File path
22
+ * @property {string[]} violations - List of violations found
23
+ * @property {boolean} passed - Whether file passed audit
24
+ */
25
+
26
+ /**
27
+ * @typedef {Object} CLIConfig
28
+ * @property {string} agentDir - Path to .agent directory
29
+ * @property {string} knowledgePath - Path to lessons file
30
+ * @property {string} rulesPath - Path to rules directory
31
+ */
32
+
33
+ export { };
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Audit UI - Compliance check with Clack UI
3
+ *
4
+ * Wrapper for the audit functionality that integrates
5
+ * with the main menu instead of spawning a separate process.
6
+ */
7
+ import * as p from "@clack/prompts";
8
+ import pc from "picocolors";
9
+ import fs from "fs";
10
+ import path from "path";
11
+ import { scanDirectory, loadKnowledge, saveKnowledge } from "../recall.js";
12
+ import { VERSION, cwd } from "../config.js";
13
+
14
+ // ============================================================================
15
+ // CONFIGURATION
16
+ // ============================================================================
17
+
18
+ const SCAN_EXTENSIONS = [".js", ".ts", ".tsx", ".jsx", ".mjs"];
19
+
20
+ // ============================================================================
21
+ // GOVERNANCE CHECKS
22
+ // ============================================================================
23
+
24
+ /**
25
+ * Check if governance files exist
26
+ * @param {string} projectRoot
27
+ * @returns {{ passed: boolean, details: string[] }}
28
+ */
29
+ function checkGovernance(projectRoot) {
30
+ const details = [];
31
+ let passed = true;
32
+
33
+ const governanceFiles = [
34
+ { path: path.join(projectRoot, ".agent", "GEMINI.md"), name: "GEMINI.md" },
35
+ { path: path.join(projectRoot, ".agent", "ARCHITECTURE.md"), name: "ARCHITECTURE.md" }
36
+ ];
37
+
38
+ governanceFiles.forEach(file => {
39
+ if (fs.existsSync(file.path)) {
40
+ details.push(`${pc.green("✓")} ${file.name} found`);
41
+ } else {
42
+ details.push(`${pc.yellow("⚠")} ${file.name} not found (optional)`);
43
+ }
44
+ });
45
+
46
+ // Check for skills
47
+ const skillsDir = path.join(projectRoot, ".agent", "skills");
48
+ if (fs.existsSync(skillsDir)) {
49
+ try {
50
+ const skills = fs.readdirSync(skillsDir).filter(f =>
51
+ fs.statSync(path.join(skillsDir, f)).isDirectory()
52
+ );
53
+ details.push(`${pc.green("✓")} ${skills.length} skill(s) loaded`);
54
+ } catch (e) {
55
+ details.push(`${pc.dim("○")} Skills directory not accessible`);
56
+ }
57
+ }
58
+
59
+ return { passed, details };
60
+ }
61
+
62
+ // ============================================================================
63
+ // AUDIT UI
64
+ // ============================================================================
65
+
66
+ /**
67
+ * Run audit with Clack UI
68
+ */
69
+ export async function runAuditUI() {
70
+ const projectRoot = cwd;
71
+
72
+ p.intro(pc.cyan(`Smart Audit v${VERSION}`));
73
+
74
+ const startTime = Date.now();
75
+ let hasErrors = false;
76
+
77
+ // Phase 1: Memory Recall
78
+ const s1 = p.spinner();
79
+ s1.start("Phase 1: Memory Recall");
80
+
81
+ const db = loadKnowledge();
82
+
83
+ if (db.lessons.length === 0) {
84
+ s1.stop(pc.dim("Phase 1: No lessons learned yet"));
85
+ } else {
86
+ const { results } = scanDirectory(projectRoot, db);
87
+
88
+ // Count violations
89
+ let totalViolations = 0;
90
+ let errorCount = 0;
91
+ let warningCount = 0;
92
+
93
+ results.forEach(result => {
94
+ result.violations.forEach(({ lesson, matches }) => {
95
+ totalViolations += matches.length;
96
+ if (lesson.severity === "ERROR") {
97
+ errorCount += matches.length;
98
+ hasErrors = true;
99
+ } else {
100
+ warningCount += matches.length;
101
+ }
102
+ });
103
+ });
104
+
105
+ if (totalViolations > 0) {
106
+ s1.stop(`Phase 1: Found ${totalViolations} violation(s)`);
107
+
108
+ // Show violations summary
109
+ if (errorCount > 0 || warningCount > 0) {
110
+ p.log.warn(`${pc.red("✖")} ${errorCount} error(s) ${pc.yellow("⚠")} ${warningCount} warning(s)`);
111
+ }
112
+
113
+ saveKnowledge(db);
114
+ } else {
115
+ s1.stop(pc.green("Phase 1: No violations found ✓"));
116
+ }
117
+ }
118
+
119
+ // Phase 2: Governance
120
+ const s2 = p.spinner();
121
+ s2.start("Phase 2: Governance Check");
122
+
123
+ const govResult = checkGovernance(projectRoot);
124
+ s2.stop("Phase 2: Governance checked");
125
+
126
+ p.note(govResult.details.join("\n"), "Governance");
127
+
128
+ // Phase 3: Summary
129
+ const duration = ((Date.now() - startTime) / 1000).toFixed(2);
130
+ const totalHits = db.lessons.reduce((sum, l) => sum + (l.hitCount || 0), 0);
131
+
132
+ p.note(
133
+ `${pc.dim("Duration:")} ${duration}s\n` +
134
+ `${pc.dim("Lessons:")} ${db.lessons.length} in memory\n` +
135
+ `${pc.dim("Hits:")} ${totalHits} total`,
136
+ "Summary"
137
+ );
138
+
139
+ if (hasErrors) {
140
+ p.outro(pc.red("AUDIT FAILED: Please fix ERROR violations"));
141
+ } else {
142
+ p.outro(pc.green("AUDIT PASSED: Code is compliant"));
143
+ }
144
+ }
145
+
146
+ export default runAuditUI;