pikakit 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +8 -8
  2. package/lib/agent-cli/bin/agent.js +187 -0
  3. package/lib/agent-cli/lib/audit.js +154 -0
  4. package/lib/agent-cli/lib/audit.test.js +100 -0
  5. package/lib/agent-cli/lib/auto-learn.js +319 -0
  6. package/lib/agent-cli/lib/backup.js +138 -0
  7. package/lib/agent-cli/lib/backup.test.js +78 -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/icons.js +93 -0
  22. package/lib/agent-cli/lib/ignore.js +116 -0
  23. package/lib/agent-cli/lib/ignore.test.js +58 -0
  24. package/lib/agent-cli/lib/init.js +124 -0
  25. package/lib/agent-cli/lib/knowledge-index.js +326 -0
  26. package/lib/agent-cli/lib/knowledge-metrics.js +335 -0
  27. package/lib/agent-cli/lib/knowledge-retention.js +398 -0
  28. package/lib/agent-cli/lib/knowledge-validator.js +312 -0
  29. package/lib/agent-cli/lib/learn.js +255 -0
  30. package/lib/agent-cli/lib/learn.test.js +70 -0
  31. package/lib/agent-cli/lib/proposals.js +199 -0
  32. package/lib/agent-cli/lib/proposals.test.js +56 -0
  33. package/lib/agent-cli/lib/recall.js +826 -0
  34. package/lib/agent-cli/lib/recall.test.js +107 -0
  35. package/lib/agent-cli/lib/selfevolution-bridge.js +167 -0
  36. package/lib/agent-cli/lib/settings.js +203 -0
  37. package/lib/agent-cli/lib/skill-learn.js +296 -0
  38. package/lib/agent-cli/lib/stats.js +132 -0
  39. package/lib/agent-cli/lib/stats.test.js +94 -0
  40. package/lib/agent-cli/lib/types.js +33 -0
  41. package/lib/agent-cli/lib/ui/audit-ui.js +146 -0
  42. package/lib/agent-cli/lib/ui/backup-ui.js +107 -0
  43. package/lib/agent-cli/lib/ui/clack-helpers.js +317 -0
  44. package/lib/agent-cli/lib/ui/common.js +83 -0
  45. package/lib/agent-cli/lib/ui/completion-ui.js +126 -0
  46. package/lib/agent-cli/lib/ui/custom-select.js +69 -0
  47. package/lib/agent-cli/lib/ui/dashboard-ui.js +222 -0
  48. package/lib/agent-cli/lib/ui/evolution-signals-ui.js +107 -0
  49. package/lib/agent-cli/lib/ui/export-ui.js +94 -0
  50. package/lib/agent-cli/lib/ui/fix-all-ui.js +191 -0
  51. package/lib/agent-cli/lib/ui/help-ui.js +49 -0
  52. package/lib/agent-cli/lib/ui/index.js +199 -0
  53. package/lib/agent-cli/lib/ui/init-ui.js +56 -0
  54. package/lib/agent-cli/lib/ui/knowledge-ui.js +55 -0
  55. package/lib/agent-cli/lib/ui/learn-ui.js +706 -0
  56. package/lib/agent-cli/lib/ui/lessons-ui.js +148 -0
  57. package/lib/agent-cli/lib/ui/pretty.js +145 -0
  58. package/lib/agent-cli/lib/ui/proposals-ui.js +99 -0
  59. package/lib/agent-cli/lib/ui/recall-ui.js +342 -0
  60. package/lib/agent-cli/lib/ui/routing-demo.js +79 -0
  61. package/lib/agent-cli/lib/ui/routing-ui.js +325 -0
  62. package/lib/agent-cli/lib/ui/settings-ui.js +381 -0
  63. package/lib/agent-cli/lib/ui/stats-ui.js +123 -0
  64. package/lib/agent-cli/lib/ui/watch-ui.js +236 -0
  65. package/lib/agent-cli/lib/watcher.js +181 -0
  66. package/lib/agent-cli/lib/watcher.test.js +85 -0
  67. package/lib/agent-cli/src/MIGRATION.md +418 -0
  68. package/lib/agent-cli/src/README.md +367 -0
  69. package/lib/agent-cli/src/core/evolution/evolution-signal.js +42 -0
  70. package/lib/agent-cli/src/core/evolution/index.js +17 -0
  71. package/lib/agent-cli/src/core/evolution/review-gate.js +40 -0
  72. package/lib/agent-cli/src/core/evolution/signal-detector.js +137 -0
  73. package/lib/agent-cli/src/core/evolution/signal-queue.js +79 -0
  74. package/lib/agent-cli/src/core/evolution/threshold-checker.js +79 -0
  75. package/lib/agent-cli/src/core/index.js +15 -0
  76. package/lib/agent-cli/src/core/learning/cognitive-enhancer.js +282 -0
  77. package/lib/agent-cli/src/core/learning/index.js +12 -0
  78. package/lib/agent-cli/src/core/learning/lesson-synthesizer.js +83 -0
  79. package/lib/agent-cli/src/core/scanning/index.js +14 -0
  80. package/lib/agent-cli/src/data/index.js +13 -0
  81. package/lib/agent-cli/src/data/repositories/index.js +8 -0
  82. package/lib/agent-cli/src/data/repositories/lesson-repository.js +130 -0
  83. package/lib/agent-cli/src/data/repositories/signal-repository.js +119 -0
  84. package/lib/agent-cli/src/data/storage/index.js +8 -0
  85. package/lib/agent-cli/src/data/storage/json-storage.js +64 -0
  86. package/lib/agent-cli/src/data/storage/yaml-storage.js +66 -0
  87. package/lib/agent-cli/src/infrastructure/index.js +13 -0
  88. package/lib/agent-cli/src/presentation/formatters/skill-formatter.js +232 -0
  89. package/lib/agent-cli/src/services/export-service.js +162 -0
  90. package/lib/agent-cli/src/services/index.js +13 -0
  91. package/lib/agent-cli/src/services/learning-service.js +99 -0
  92. package/package.json +5 -3
@@ -0,0 +1,826 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Smart Recall Script - ESM Version (Production-Ready)
4
+ *
5
+ * The "Memory" script. Checks code against learned lessons.
6
+ * Features:
7
+ * - Streaming file scanner (multiple files)
8
+ * - Context-aware pattern matching (shows line numbers)
9
+ * - Hit tracking for frequency analysis
10
+ *
11
+ * Usage:
12
+ * node recall.js <file_path>
13
+ * node recall.js <directory> --recursive
14
+ */
15
+
16
+ import fs from "fs";
17
+ import path from "path";
18
+ import yaml from "js-yaml";
19
+ import ora from "ora";
20
+ import { KNOWLEDGE_DIR, LESSONS_PATH, DEBUG, cwd, VERSION } from "./config.js";
21
+ import { icons, safeText } from "./icons.js";
22
+ import { loadIgnorePatterns, isIgnored } from "./ignore.js";
23
+ import pretty from "./ui/pretty.js";
24
+ import * as p from "@clack/prompts";
25
+ import pc from "picocolors";
26
+ import { checkEvolutionThreshold, queueEvolutionSignal } from "./evolution-signal.js";
27
+ import { isAutoUpdatingEnabled, getUpdateThreshold } from "./settings.js";
28
+
29
+ // ============================================================================
30
+ // PERCEPTION LAYER - Cognitive Enhancements
31
+ // ============================================================================
32
+
33
+ /**
34
+ * Infer intent from lesson metadata
35
+ * @param {object} lesson - Raw lesson from YAML
36
+ * @returns {string} - Intent: 'prevent', 'warn', 'optimize'
37
+ */
38
+ function inferIntent(lesson) {
39
+ if (lesson.type === 'mistake') {
40
+ return lesson.severity === 'ERROR' ? 'prevent' : 'warn';
41
+ }
42
+ return 'optimize'; // improvements
43
+ }
44
+
45
+ /**
46
+ * Classify pattern type for better context
47
+ * @param {string} pattern - Regex pattern
48
+ * @returns {string} - Pattern type: 'dependency', 'structure', 'quality', 'security', 'performance', 'general'
49
+ */
50
+ function classifyPattern(pattern) {
51
+ if (!pattern) return 'general';
52
+
53
+ // Security patterns
54
+ if (pattern.includes('eval') || pattern.includes('innerHTML') || pattern.includes('dangerouslySetInnerHTML')) {
55
+ return 'security';
56
+ }
57
+
58
+ // Dependency patterns
59
+ if (pattern.includes('import') || pattern.includes('require') || pattern.includes('from')) {
60
+ return 'dependency';
61
+ }
62
+
63
+ // Structure patterns
64
+ if (pattern.includes('function') || pattern.includes('class') || pattern.includes('const') || pattern.includes('let')) {
65
+ return 'structure';
66
+ }
67
+
68
+ // Quality patterns
69
+ if (pattern.includes('console') || pattern.includes('debugger') || pattern.includes('TODO')) {
70
+ return 'quality';
71
+ }
72
+
73
+ // Performance patterns
74
+ if (pattern.includes('loop') || pattern.includes('forEach') || pattern.includes('map')) {
75
+ return 'performance';
76
+ }
77
+
78
+ return 'general';
79
+ }
80
+
81
+ /**
82
+ * Infer applicability context from tags
83
+ * @param {Array<string>} tags - Lesson tags
84
+ * @returns {object} - Context: { scope, appliesTo }
85
+ */
86
+ function inferApplicability(tags = []) {
87
+ return {
88
+ scope: tags.includes('global') ? 'global' : 'local',
89
+ appliesTo: tags.filter(t =>
90
+ t.includes('file') ||
91
+ t.includes('component') ||
92
+ t.includes('module')
93
+ )
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Calculate confidence score based on lesson metadata
99
+ * @param {object} lesson - Lesson with hitCount
100
+ * @returns {number} - Confidence: 0.0 to 1.0
101
+ */
102
+ function calculateConfidence(lesson) {
103
+ const hitCount = lesson.hitCount || 0;
104
+
105
+ // Confidence increases with hits (logarithmic scale)
106
+ if (hitCount === 0) return 0.3; // Default for new lessons
107
+ if (hitCount < 3) return 0.5;
108
+ if (hitCount < 10) return 0.7;
109
+ if (hitCount < 30) return 0.85;
110
+ return 0.95; // High confidence for well-established patterns
111
+ }
112
+
113
+ /**
114
+ * Transform raw lesson into cognitive lesson with intent & context
115
+ * @param {object} rawLesson - Raw lesson from YAML
116
+ * @returns {object} - Cognitive lesson with enhanced metadata
117
+ */
118
+ function createCognitiveLesson(rawLesson) {
119
+ return {
120
+ ...rawLesson,
121
+
122
+ // Intent inference
123
+ intent: inferIntent(rawLesson),
124
+
125
+ // Pattern classification
126
+ patternType: classifyPattern(rawLesson.pattern),
127
+
128
+ // Context awareness
129
+ context: {
130
+ appliesTo: inferApplicability(rawLesson.tags),
131
+ scope: (rawLesson.tags || []).includes('global') ? 'global' : 'local'
132
+ },
133
+
134
+ // Cognitive metadata
135
+ cognitive: {
136
+ maturity: (rawLesson.hitCount || 0) > 10 ? 'stable' : 'learning',
137
+ confidence: calculateConfidence(rawLesson)
138
+ }
139
+ };
140
+ }
141
+
142
+ // ============================================================================
143
+ // KNOWLEDGE BASE
144
+ // ============================================================================
145
+
146
+ /**
147
+ * Load knowledge base from YAML file
148
+ * Supports both v3.x (lessons-learned.yaml) and v4.x (cognitive lessons)
149
+ * @returns {{ lessons: Array<{ id: string, pattern: string, message: string, severity: string, hitCount?: number, lastHit?: string }>, version?: number }}
150
+ */
151
+ export function loadKnowledge() {
152
+ try {
153
+ // Check for v4.x cognitive structure first
154
+ const mistakesPath = path.join(KNOWLEDGE_DIR, 'mistakes.yaml');
155
+ const improvementsPath = path.join(KNOWLEDGE_DIR, 'improvements.yaml');
156
+
157
+ const hasV4 = fs.existsSync(mistakesPath) || fs.existsSync(improvementsPath);
158
+
159
+ if (hasV4) {
160
+ // v4.x: Load cognitive lessons and flatten for scanning
161
+ const mistakes = fs.existsSync(mistakesPath)
162
+ ? yaml.load(fs.readFileSync(mistakesPath, 'utf8'))
163
+ : { mistakes: [] };
164
+ const improvements = fs.existsSync(improvementsPath)
165
+ ? yaml.load(fs.readFileSync(improvementsPath, 'utf8'))
166
+ : { improvements: [] };
167
+
168
+ // Flatten and apply cognitive transformation
169
+ const rawLessons = [
170
+ ...(mistakes.mistakes || []).map(m => ({
171
+ id: m.id,
172
+ pattern: m.pattern,
173
+ message: m.message,
174
+ severity: m.severity || 'WARNING',
175
+ hitCount: m.hitCount || 0,
176
+ lastHit: m.lastHit,
177
+ excludePaths: m.excludePaths,
178
+ tags: m.tags,
179
+ autoFix: m.autoFix, // Preserve autoFix for Fix All
180
+ type: 'mistake', // Mark for tracking
181
+ })),
182
+ ...(improvements.improvements || []).map(i => ({
183
+ id: i.id,
184
+ pattern: i.pattern,
185
+ message: i.message,
186
+ severity: 'INFO', // Improvements are informational
187
+ hitCount: i.appliedCount || 0,
188
+ lastHit: i.lastApplied,
189
+ excludePaths: i.excludePaths, // PRESERVE excludePaths!
190
+ tags: i.tags,
191
+ type: 'improvement', // Mark for tracking
192
+ }))
193
+ ];
194
+
195
+ // Apply cognitive transformation to create agent-level lessons
196
+ const lessons = rawLessons.map(createCognitiveLesson);
197
+
198
+ return { lessons, version: 4.0 };
199
+ }
200
+
201
+ // Fallback to v3.x
202
+ if (!fs.existsSync(LESSONS_PATH)) {
203
+ return { lessons: [], version: 1 };
204
+ }
205
+ const content = fs.readFileSync(LESSONS_PATH, "utf8");
206
+ return yaml.load(content) || { lessons: [], version: 1 };
207
+ } catch (error) {
208
+ if (DEBUG) console.error("Error loading knowledge:", error.message);
209
+ return { lessons: [], version: 1 };
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Save knowledge base to YAML file
215
+ * Supports both v3.x and v4.x formats
216
+ * @param {{ lessons: Array, version?: number }} data
217
+ */
218
+ export function saveKnowledge(data) {
219
+ try {
220
+ fs.mkdirSync(KNOWLEDGE_DIR, { recursive: true });
221
+
222
+ // v4.x: Save to mistakes.yaml and improvements.yaml
223
+ if (data.version === 4.0) {
224
+ const mistakes = data.lessons.filter(l => l.type === 'mistake').map(m => {
225
+ const { type, ...rest } = m; // Remove type field
226
+ return rest;
227
+ });
228
+ const improvements = data.lessons.filter(l => l.type === 'improvement').map(i => {
229
+ const { type, severity, ...rest } = i; // Remove type and severity
230
+ return {
231
+ ...rest,
232
+ excludePaths: i.excludePaths || [], // PRESERVE excludePaths explicitly
233
+ appliedCount: rest.hitCount,
234
+ lastApplied: rest.lastHit,
235
+ };
236
+ });
237
+
238
+ // Save mistakes
239
+ if (mistakes.length > 0) {
240
+ const mistakesPath = path.join(KNOWLEDGE_DIR, 'mistakes.yaml');
241
+ const mistakesData = yaml.dump({ version: 4.0, mistakes }, { lineWidth: -1 });
242
+ fs.writeFileSync(mistakesPath, mistakesData, 'utf8');
243
+ }
244
+
245
+ // Save improvements
246
+ if (improvements.length > 0) {
247
+ const improvementsPath = path.join(KNOWLEDGE_DIR, 'improvements.yaml');
248
+ const improvementsData = yaml.dump({ version: 4.0, improvements }, { lineWidth: -1 });
249
+ fs.writeFileSync(improvementsPath, improvementsData, 'utf8');
250
+ }
251
+ } else {
252
+ // v3.x: Save to lessons-learned.yaml
253
+ const str = yaml.dump(data, { lineWidth: -1 });
254
+ fs.writeFileSync(LESSONS_PATH, str, "utf8");
255
+ }
256
+ } catch (error) {
257
+ console.error("❌ Failed to save knowledge base:", error.message);
258
+ }
259
+ }
260
+
261
+ // ============================================================================
262
+ // FILE SCANNING
263
+ // ============================================================================
264
+
265
+ /**
266
+ * Scan a single file against learned patterns
267
+ * @param {string} filePath - Path to file to scan
268
+ * @param {{ lessons: Array }} db - Knowledge base
269
+ * @param {boolean} updateHits - Whether to update hit counts
270
+ * @returns {{ file: string, violations: Array<{ lesson: object, matches: Array<{ line: number, content: string }> }> }}
271
+ */
272
+ export function scanFile(filePath, db, updateHits = false) {
273
+ const violations = [];
274
+
275
+ if (!fs.existsSync(filePath)) {
276
+ return { file: filePath, violations: [], error: "File not found" };
277
+ }
278
+
279
+ const content = fs.readFileSync(filePath, "utf8");
280
+ const lines = content.split("\n");
281
+
282
+ if (!db.lessons || db.lessons.length === 0) {
283
+ return { file: filePath, violations: [] };
284
+ }
285
+
286
+ db.lessons.forEach(lesson => {
287
+ // Skip if no valid pattern
288
+ if (!lesson.pattern) return;
289
+
290
+ // Check excludePaths - skip this lesson for excluded paths using glob patterns
291
+ if (lesson.excludePaths && Array.isArray(lesson.excludePaths) && lesson.excludePaths.length > 0) {
292
+ if (isIgnored(filePath, lesson.excludePaths)) {
293
+ return; // Skip this lesson for this file
294
+ }
295
+ }
296
+
297
+ try {
298
+ const regex = new RegExp(lesson.pattern, "g");
299
+ const matches = [];
300
+
301
+ lines.forEach((line, idx) => {
302
+ if (regex.test(line)) {
303
+ matches.push({
304
+ line: idx + 1,
305
+ content: line.trim().substring(0, 80)
306
+ });
307
+ regex.lastIndex = 0; // Reset for next test
308
+ }
309
+ });
310
+
311
+ if (matches.length > 0) {
312
+ violations.push({ lesson, matches });
313
+
314
+ // Track hit count
315
+ if (updateHits) {
316
+ lesson.hitCount = (lesson.hitCount || 0) + matches.length;
317
+ lesson.lastHit = new Date().toISOString();
318
+
319
+ // Check evolution threshold (Phase 2: Signal Layer)
320
+ const threshold = getUpdateThreshold(); // Get from settings
321
+ const evolutionCheck = checkEvolutionThreshold(lesson, threshold);
322
+
323
+ if (evolutionCheck.ready) {
324
+ // Queue evolution signal (doesn't evolve yet, just signals)
325
+ queueEvolutionSignal(lesson.id, evolutionCheck, {
326
+ triggerEvent: 'violation',
327
+ file: filePath,
328
+ matchCount: matches.length,
329
+ timestamp: Date.now()
330
+ });
331
+
332
+ if (DEBUG) {
333
+ console.log(`📡 Evolution signal queued for [${lesson.id}]: ${evolutionCheck.reason} (confidence: ${evolutionCheck.confidence.toFixed(2)})`);
334
+ }
335
+ }
336
+
337
+ // Auto-escalation: WARNING → ERROR after threshold violations
338
+ // ONLY if Auto-Updating is ENABLED in settings
339
+ if (isAutoUpdatingEnabled() && lesson.severity === "WARNING" && lesson.hitCount >= threshold && !lesson.autoEscalated) {
340
+ lesson.severity = "ERROR";
341
+ lesson.autoEscalated = true;
342
+ console.log(`⚡ Auto-escalated [${lesson.id}] to ERROR (${lesson.hitCount} hits, threshold: ${threshold})`);
343
+ }
344
+ }
345
+ }
346
+ } catch (e) {
347
+ if (DEBUG) console.error(`Invalid regex in lesson ${lesson.id}:`, e.message);
348
+ }
349
+ });
350
+
351
+ return { file: filePath, violations };
352
+ }
353
+
354
+ /**
355
+ * Scan multiple files in a directory recursively
356
+ * @param {string} dirPath - Directory to scan
357
+ * @param {{ lessons: Array }} db - Knowledge base
358
+ * @param {string[]} extensions - File extensions to scan
359
+ * @returns {{ results: Array<{ file: string, violations: Array }>, ignoredCount: number, totalScanned: number }}
360
+ */
361
+ export function scanDirectory(dirPath, db, extensions = [".js", ".ts", ".tsx", ".jsx"]) {
362
+ const results = [];
363
+ const ignorePatterns = loadIgnorePatterns(dirPath);
364
+ let ignoredCount = 0;
365
+ let totalScanned = 0;
366
+
367
+ function walk(dir) {
368
+ let entries;
369
+ try {
370
+ entries = fs.readdirSync(dir, { withFileTypes: true });
371
+ } catch (e) {
372
+ return; // Skip unreadable directories
373
+ }
374
+
375
+ for (const entry of entries) {
376
+ const fullPath = path.join(dir, entry.name);
377
+ const relativePath = path.relative(dirPath, fullPath);
378
+
379
+ // Check .agentignore patterns
380
+ if (isIgnored(relativePath, ignorePatterns)) {
381
+ ignoredCount++;
382
+ continue;
383
+ }
384
+
385
+ if (entry.isDirectory()) {
386
+ walk(fullPath);
387
+ } else if (entry.isFile()) {
388
+ const ext = path.extname(entry.name);
389
+ if (extensions.includes(ext)) {
390
+ totalScanned++;
391
+ const result = scanFile(fullPath, db, true);
392
+ if (result.violations.length > 0) {
393
+ results.push(result);
394
+ }
395
+ }
396
+ }
397
+ }
398
+ }
399
+
400
+ if (fs.statSync(dirPath).isDirectory()) {
401
+ walk(dirPath);
402
+ } else {
403
+ totalScanned++;
404
+ results.push(scanFile(dirPath, db, true));
405
+ }
406
+
407
+ return { results, ignoredCount, totalScanned };
408
+ }
409
+
410
+ // ============================================================================
411
+ // REPORTING
412
+ // ============================================================================
413
+
414
+ /**
415
+ * Print scan results in a readable format
416
+ * @param {Array} results - Scan results
417
+ */
418
+ export function printResults(results) {
419
+ let totalViolations = 0;
420
+ let errorCount = 0;
421
+ let warningCount = 0;
422
+
423
+ results.forEach(result => {
424
+ if (result.violations.length === 0) return;
425
+
426
+ console.log(`\n📄 ${path.relative(process.cwd(), result.file)}`);
427
+
428
+ result.violations.forEach(({ lesson, matches }) => {
429
+ totalViolations += matches.length;
430
+ if (lesson.severity === "ERROR") errorCount += matches.length;
431
+ else warningCount += matches.length;
432
+
433
+ const icon = lesson.severity === "ERROR" ? "❌" : "⚠️";
434
+ console.log(` ${icon} [${lesson.id}] ${lesson.message}`);
435
+
436
+ matches.forEach(m => {
437
+ console.log(` L${m.line}: ${m.content}`);
438
+ });
439
+ });
440
+ });
441
+
442
+ // Return stats only - summary is handled by pretty.showScanSummary
443
+ return { total: totalViolations, errors: errorCount, warnings: warningCount };
444
+ }
445
+
446
+ // ============================================================================
447
+ // CLI
448
+ // ============================================================================
449
+
450
+ function main() {
451
+ const args = process.argv.slice(2);
452
+ const jsonMode = args.includes("--json");
453
+
454
+ if (args.length === 0 || args.includes("--help")) {
455
+ console.log(`
456
+ 🧠 Smart Recall - Memory Check Tool
457
+
458
+ Usage:
459
+ recall <file> Check single file
460
+ recall <directory> Check all files in directory
461
+ recall --staged Check git staged files only
462
+
463
+ Options:
464
+ --json Output JSON for CI/CD
465
+ --help Show this help
466
+ `);
467
+ process.exit(0);
468
+ }
469
+
470
+ const target = args.find(a => !a.startsWith("--")) || ".";
471
+ const db = loadKnowledge();
472
+
473
+ if (!db.lessons || db.lessons.length === 0) {
474
+ console.log("ℹ️ No lessons learned yet. Use 'agent learn' to add patterns.");
475
+ process.exit(0);
476
+ }
477
+
478
+ // Scan first
479
+ const { results, ignoredCount, totalScanned } = scanDirectory(target, db);
480
+ const stats = printResults(results);
481
+
482
+ // JSON output mode for CI/CD
483
+ if (jsonMode) {
484
+ const output = {
485
+ version: VERSION,
486
+ filesScanned: totalScanned,
487
+ ignored: ignoredCount,
488
+ violations: stats.total,
489
+ errors: stats.errors,
490
+ warnings: stats.warnings,
491
+ passed: stats.errors === 0,
492
+ details: results.filter(r => r.violations.length > 0).map(r => ({
493
+ file: r.file,
494
+ violations: r.violations.map(v => ({
495
+ id: v.lesson.id,
496
+ severity: v.lesson.severity,
497
+ message: v.lesson.message,
498
+ matches: v.matches.length
499
+ }))
500
+ }))
501
+ };
502
+ console.log(JSON.stringify(output, null, 2));
503
+ process.exit(stats.errors > 0 ? 1 : 0);
504
+ }
505
+
506
+ // Show Clack-based summary (consistent with CLI)
507
+ p.intro(pc.cyan(`${icons.brain} PikaKit v${VERSION}`));
508
+
509
+ // Save updated hit counts
510
+ saveKnowledge(db);
511
+
512
+ // Summary using Clack
513
+ const summaryLines = [
514
+ `${pc.green("✓")} ${totalScanned} file(s) scanned`,
515
+ `${pc.dim("›")} ${ignoredCount} paths ignored`,
516
+ stats.total > 0
517
+ ? `${pc.red("✗")} ${stats.total} violation(s) found`
518
+ : `${pc.green("✓")} No violations found`
519
+ ];
520
+
521
+ if (stats.total === 0) {
522
+ summaryLines.push("");
523
+ summaryLines.push(pc.green(`All clear! Your code looks great. ${icons.party}`));
524
+ }
525
+
526
+ p.note(summaryLines.join("\n"), pc.dim("Memory check completed ✓"));
527
+
528
+ if (stats.errors > 0) {
529
+ process.exit(1);
530
+ } else {
531
+ process.exit(0);
532
+ }
533
+ }
534
+
535
+ // ============================================================================
536
+ // STRUCTURED SCAN (for Scan All / Fix All architecture)
537
+ // ============================================================================
538
+
539
+ /**
540
+ * Sanitize filename for use in issue ID
541
+ * @param {string} filepath - File path
542
+ * @returns {string} Sanitized filename
543
+ */
544
+ function sanitizeFilename(filepath) {
545
+ return filepath
546
+ .replace(/[^a-zA-Z0-9]/g, '_')
547
+ .replace(/_+/g, '_')
548
+ .substring(0, 50);
549
+ }
550
+
551
+ /**
552
+ * Scan directory and generate structured output with issue IDs
553
+ * This is the read-only analyzer for Scan All
554
+ * @param {string} targetPath - Path to scan
555
+ * @param {{ lessons: Array }} db - Knowledge base
556
+ * @returns {{ scanId: string, timestamp: number, issues: Array, summary: object }}
557
+ */
558
+ export function scanDirectoryStructured(targetPath, db) {
559
+ // Run standard scan
560
+ const { results, ignoredCount, totalScanned } = scanDirectory(targetPath, db);
561
+
562
+ // Generate structured issues with unique IDs
563
+ const issues = [];
564
+ results.forEach(fileResult => {
565
+ fileResult.violations.forEach(({ lesson, matches }) => {
566
+ matches.forEach(match => {
567
+ const issueId = `${lesson.id}_${sanitizeFilename(fileResult.file)}_L${match.line}`;
568
+ issues.push({
569
+ id: issueId,
570
+ lessonId: lesson.id,
571
+ type: lesson.type || 'unknown',
572
+ severity: lesson.severity || 'WARNING',
573
+ file: fileResult.file,
574
+ line: match.line,
575
+ message: lesson.message,
576
+ pattern: lesson.pattern,
577
+ matchedCode: match.content,
578
+ fixable: !!lesson.autoFix, // Check if lesson has autoFix strategy
579
+ confidence: 0.95
580
+ });
581
+ });
582
+ });
583
+ });
584
+
585
+ const scanResult = {
586
+ scanId: `scan_${Date.now()}`,
587
+ timestamp: Date.now(),
588
+ projectPath: path.resolve(targetPath),
589
+ totalIssues: issues.length,
590
+ summary: {
591
+ errors: issues.filter(i => i.severity === 'ERROR').length,
592
+ warnings: issues.filter(i => i.severity !== 'ERROR').length,
593
+ filesScanned: totalScanned,
594
+ ignoredFiles: ignoredCount
595
+ },
596
+ issues
597
+ };
598
+
599
+ return scanResult;
600
+ }
601
+
602
+ /**
603
+ * Save scan result to disk
604
+ * @param {object} scanResult - Scan result object
605
+ */
606
+ export function saveScanResult(scanResult) {
607
+ const scansDir = path.join(KNOWLEDGE_DIR, 'scans');
608
+
609
+ // Create scans directory if it doesn't exist
610
+ if (!fs.existsSync(scansDir)) {
611
+ fs.mkdirSync(scansDir, { recursive: true });
612
+ }
613
+
614
+ const filename = `${scanResult.scanId}.json`;
615
+ const filepath = path.join(scansDir, filename);
616
+
617
+ fs.writeFileSync(filepath, JSON.stringify(scanResult, null, 2));
618
+
619
+ // Keep only last 10 scans
620
+ cleanOldScans(scansDir, 10);
621
+ }
622
+
623
+ /**
624
+ * Load scan result from disk
625
+ * @param {string} scanId - Scan ID to load
626
+ * @returns {object|null} Scan result or null if not found
627
+ */
628
+ export function loadScanResult(scanId) {
629
+ const filepath = path.join(KNOWLEDGE_DIR, 'scans', `${scanId}.json`);
630
+
631
+ if (!fs.existsSync(filepath)) {
632
+ return null;
633
+ }
634
+
635
+ return JSON.parse(fs.readFileSync(filepath, 'utf8'));
636
+ }
637
+
638
+ /**
639
+ * Load most recent scan result
640
+ * @returns {object|null} Most recent scan result or null
641
+ */
642
+ export function loadMostRecentScan() {
643
+ const scansDir = path.join(KNOWLEDGE_DIR, 'scans');
644
+
645
+ if (!fs.existsSync(scansDir)) {
646
+ return null;
647
+ }
648
+
649
+ const files = fs.readdirSync(scansDir)
650
+ .filter(f => f.endsWith('.json'))
651
+ .map(f => ({
652
+ name: f,
653
+ path: path.join(scansDir, f),
654
+ mtime: fs.statSync(path.join(scansDir, f)).mtime
655
+ }))
656
+ .sort((a, b) => b.mtime - a.mtime);
657
+
658
+ if (files.length === 0) {
659
+ return null;
660
+ }
661
+
662
+ return JSON.parse(fs.readFileSync(files[0].path, 'utf8'));
663
+ }
664
+
665
+ /**
666
+ * Clean old scan files, keeping only the most recent N
667
+ * @param {string} scansDir - Scans directory path
668
+ * @param {number} keepCount - Number of scans to keep
669
+ */
670
+ function cleanOldScans(scansDir, keepCount) {
671
+ const files = fs.readdirSync(scansDir)
672
+ .filter(f => f.endsWith('.json'))
673
+ .map(f => ({
674
+ name: f,
675
+ path: path.join(scansDir, f),
676
+ mtime: fs.statSync(path.join(scansDir, f)).mtime
677
+ }))
678
+ .sort((a, b) => b.mtime - a.mtime);
679
+
680
+ // Delete files beyond keepCount
681
+ files.slice(keepCount).forEach(file => {
682
+ try {
683
+ fs.unlinkSync(file.path);
684
+ } catch (e) {
685
+ // Ignore errors
686
+ }
687
+ });
688
+ }
689
+
690
+ // ============================================================================
691
+ // AUTO-FIX LOGIC (for Fix All automation)
692
+ // ============================================================================
693
+
694
+ /**
695
+ * Apply auto-fix to a file based on issue and lesson strategy
696
+ * @param {object} issue - Issue object from scan with file, line, etc.
697
+ * @param {object} lesson - Lesson with autoFix strategy
698
+ * @returns {{ success: boolean, backup?: string, error?: string, details?: string }}
699
+ */
700
+ export function applyFix(issue, lesson) {
701
+ if (!lesson.autoFix) {
702
+ return { success: false, error: 'No auto-fix strategy defined' };
703
+ }
704
+
705
+ const { file, line } = issue;
706
+ const { autoFix } = lesson;
707
+
708
+ try {
709
+ // 1. Create backup
710
+ const backup = createBackupFile(file);
711
+
712
+ // 2. Read file
713
+ const content = fs.readFileSync(file, 'utf8');
714
+ const lines = content.split('\n');
715
+
716
+ // 3. Apply fix based on type
717
+ let fixed = false;
718
+ let details = '';
719
+
720
+ switch (autoFix.type) {
721
+ case 'replace':
722
+ const searchRegex = new RegExp(autoFix.search, 'g');
723
+ const originalLine = lines[line - 1];
724
+ lines[line - 1] = lines[line - 1].replace(
725
+ searchRegex,
726
+ autoFix.replace
727
+ );
728
+ fixed = lines[line - 1] !== originalLine;
729
+ details = `Replaced: ${originalLine.trim()} → ${lines[line - 1].trim()}`;
730
+ break;
731
+
732
+ case 'regex-replace':
733
+ const findRegex = new RegExp(autoFix.find, 'g');
734
+ const origLine = lines[line - 1];
735
+ lines[line - 1] = lines[line - 1].replace(
736
+ findRegex,
737
+ autoFix.replace
738
+ );
739
+ fixed = lines[line - 1] !== origLine;
740
+ details = `Replaced: ${origLine.trim()} → ${lines[line - 1].trim()}`;
741
+ break;
742
+
743
+ case 'comment-warning':
744
+ const targetLine = line - 1;
745
+ const indent = lines[targetLine].match(/^\s*/)[0];
746
+ lines.splice(targetLine, 0, indent + autoFix.comment);
747
+ fixed = true;
748
+ details = `Added warning comment above line ${line}`;
749
+ break;
750
+
751
+ case 'insert':
752
+ const pos = autoFix.position === 'before' ? line - 1 : line;
753
+ lines.splice(pos, 0, autoFix.content);
754
+ fixed = true;
755
+ details = `Inserted: ${autoFix.content}`;
756
+ break;
757
+
758
+ case 'delete':
759
+ const deletedLine = lines[line - 1];
760
+ lines.splice(line - 1, 1);
761
+ fixed = true;
762
+ details = `Deleted: ${deletedLine.trim()}`;
763
+ break;
764
+
765
+ default:
766
+ return { success: false, error: `Unknown fix type: ${autoFix.type}` };
767
+ }
768
+
769
+ if (!fixed) {
770
+ return { success: false, error: 'Fix did not modify file' };
771
+ }
772
+
773
+ // 4. Write back
774
+ fs.writeFileSync(file, lines.join('\n'), 'utf8');
775
+
776
+ return { success: true, backup, details };
777
+
778
+ } catch (e) {
779
+ return { success: false, error: e.message };
780
+ }
781
+ }
782
+
783
+ /**
784
+ * Create backup of file before fixing
785
+ * @param {string} filepath - File to backup
786
+ * @returns {string} Backup filepath
787
+ */
788
+ function createBackupFile(filepath) {
789
+ const backupDir = path.join(KNOWLEDGE_DIR, 'fix-backups');
790
+ if (!fs.existsSync(backupDir)) {
791
+ fs.mkdirSync(backupDir, { recursive: true });
792
+ }
793
+
794
+ const timestamp = Date.now();
795
+ const filename = path.basename(filepath);
796
+ const backupPath = path.join(backupDir, `${filename}.${timestamp}.bak`);
797
+
798
+ fs.copyFileSync(filepath, backupPath);
799
+
800
+ return backupPath;
801
+ }
802
+
803
+ /**
804
+ * Restore file from backup
805
+ * @param {string} originalPath - Original file path
806
+ * @param {string} backupPath - Backup file path
807
+ * @returns {boolean} True if restored successfully
808
+ */
809
+ export function restoreFromBackup(originalPath, backupPath) {
810
+ try {
811
+ if (fs.existsSync(backupPath)) {
812
+ fs.copyFileSync(backupPath, originalPath);
813
+ fs.unlinkSync(backupPath);
814
+ return true;
815
+ }
816
+ return false;
817
+ } catch (e) {
818
+ console.error(`Failed to restore from backup: ${e.message}`);
819
+ return false;
820
+ }
821
+ }
822
+
823
+ // Run if executed directly
824
+ if (process.argv[1].includes("recall")) {
825
+ main();
826
+ }