@fermindi/pwn-cli 0.1.1 → 0.2.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 (46) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +265 -251
  3. package/cli/batch.js +333 -333
  4. package/cli/codespaces.js +303 -303
  5. package/cli/index.js +98 -91
  6. package/cli/inject.js +78 -67
  7. package/cli/knowledge.js +531 -531
  8. package/cli/migrate.js +466 -0
  9. package/cli/notify.js +135 -135
  10. package/cli/patterns.js +665 -665
  11. package/cli/status.js +91 -91
  12. package/cli/validate.js +61 -61
  13. package/package.json +70 -70
  14. package/src/core/inject.js +208 -204
  15. package/src/core/state.js +91 -91
  16. package/src/core/validate.js +202 -202
  17. package/src/core/workspace.js +176 -176
  18. package/src/index.js +20 -20
  19. package/src/knowledge/gc.js +308 -308
  20. package/src/knowledge/lifecycle.js +401 -401
  21. package/src/knowledge/promote.js +364 -364
  22. package/src/knowledge/references.js +342 -342
  23. package/src/patterns/matcher.js +218 -218
  24. package/src/patterns/registry.js +375 -375
  25. package/src/patterns/triggers.js +423 -423
  26. package/src/services/batch-service.js +849 -849
  27. package/src/services/notification-service.js +342 -342
  28. package/templates/codespaces/devcontainer.json +52 -52
  29. package/templates/codespaces/setup.sh +70 -70
  30. package/templates/workspace/.ai/README.md +164 -164
  31. package/templates/workspace/.ai/agents/README.md +204 -204
  32. package/templates/workspace/.ai/agents/claude.md +625 -625
  33. package/templates/workspace/.ai/config/README.md +79 -79
  34. package/templates/workspace/.ai/config/notifications.template.json +20 -20
  35. package/templates/workspace/.ai/memory/deadends.md +79 -79
  36. package/templates/workspace/.ai/memory/decisions.md +58 -58
  37. package/templates/workspace/.ai/memory/patterns.md +65 -65
  38. package/templates/workspace/.ai/patterns/backend/README.md +126 -126
  39. package/templates/workspace/.ai/patterns/frontend/README.md +103 -103
  40. package/templates/workspace/.ai/patterns/index.md +256 -256
  41. package/templates/workspace/.ai/patterns/triggers.json +1087 -1087
  42. package/templates/workspace/.ai/patterns/universal/README.md +141 -141
  43. package/templates/workspace/.ai/state.template.json +8 -8
  44. package/templates/workspace/.ai/tasks/active.md +77 -77
  45. package/templates/workspace/.ai/tasks/backlog.md +95 -95
  46. package/templates/workspace/.ai/workflows/batch-task.md +356 -356
@@ -1,375 +1,375 @@
1
- /**
2
- * Pattern Registry - Manages pattern storage and retrieval
3
- *
4
- * Patterns are organized in categories:
5
- * - frontend/ (react, vue, svelte, styling, component-libs)
6
- * - backend/ (express, fastapi, database, auth)
7
- * - universal/ (typescript, testing, error-handling, git, build-tools)
8
- */
9
-
10
- import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, statSync } from 'fs';
11
- import { join, relative, basename, dirname } from 'path';
12
-
13
- /**
14
- * Pattern entry structure
15
- * @typedef {Object} Pattern
16
- * @property {string} id - Unique identifier (category/name)
17
- * @property {string} name - Human-readable name
18
- * @property {string} category - Pattern category (frontend, backend, universal)
19
- * @property {string} path - Relative path to pattern directory
20
- * @property {string} description - Pattern description
21
- * @property {string[]} triggers - Associated trigger IDs
22
- * @property {Object} metadata - Additional metadata
23
- * @property {number} metadata.usageCount - How many times pattern was applied
24
- * @property {string} metadata.lastUsed - ISO timestamp of last usage
25
- * @property {string} metadata.createdAt - ISO timestamp of creation
26
- */
27
-
28
- /**
29
- * PatternRegistry - Singleton for managing patterns
30
- */
31
- export class PatternRegistry {
32
- constructor(workspacePath) {
33
- this.workspacePath = workspacePath;
34
- this.aiPath = join(workspacePath, '.ai');
35
- this.patternsPath = join(this.aiPath, 'patterns');
36
- this.registryPath = join(this.patternsPath, 'registry.json');
37
- this.patterns = new Map();
38
- this.loaded = false;
39
- }
40
-
41
- /**
42
- * Initialize or load the registry
43
- */
44
- async load() {
45
- if (this.loaded) return;
46
-
47
- // Create patterns directory if it doesn't exist
48
- if (!existsSync(this.patternsPath)) {
49
- mkdirSync(this.patternsPath, { recursive: true });
50
- }
51
-
52
- // Load existing registry or scan for patterns
53
- if (existsSync(this.registryPath)) {
54
- try {
55
- const data = JSON.parse(readFileSync(this.registryPath, 'utf8'));
56
- for (const pattern of data.patterns || []) {
57
- this.patterns.set(pattern.id, pattern);
58
- }
59
- } catch (error) {
60
- console.warn('⚠️ Could not load registry, scanning patterns directory...');
61
- await this.scan();
62
- }
63
- } else {
64
- await this.scan();
65
- }
66
-
67
- this.loaded = true;
68
- }
69
-
70
- /**
71
- * Scan patterns directory and build registry
72
- */
73
- async scan() {
74
- this.patterns.clear();
75
-
76
- const categories = ['frontend', 'backend', 'universal'];
77
-
78
- for (const category of categories) {
79
- const categoryPath = join(this.patternsPath, category);
80
- if (!existsSync(categoryPath)) continue;
81
-
82
- const entries = readdirSync(categoryPath, { withFileTypes: true });
83
-
84
- for (const entry of entries) {
85
- if (!entry.isDirectory()) continue;
86
-
87
- const patternPath = join(categoryPath, entry.name);
88
- const readmePath = join(patternPath, 'README.md');
89
-
90
- const pattern = {
91
- id: `${category}/${entry.name}`,
92
- name: this.formatName(entry.name),
93
- category,
94
- path: relative(this.aiPath, patternPath),
95
- description: this.extractDescription(readmePath),
96
- triggers: [],
97
- metadata: {
98
- usageCount: 0,
99
- lastUsed: null,
100
- createdAt: new Date().toISOString()
101
- }
102
- };
103
-
104
- this.patterns.set(pattern.id, pattern);
105
- }
106
- }
107
-
108
- // Save the scanned registry
109
- await this.save();
110
- }
111
-
112
- /**
113
- * Extract description from README.md first line
114
- */
115
- extractDescription(readmePath) {
116
- if (!existsSync(readmePath)) {
117
- return 'No description available';
118
- }
119
-
120
- try {
121
- const content = readFileSync(readmePath, 'utf8');
122
- const lines = content.split('\n');
123
-
124
- // Find first non-empty, non-heading line
125
- for (const line of lines) {
126
- const trimmed = line.trim();
127
- if (trimmed && !trimmed.startsWith('#') && !trimmed.startsWith('---')) {
128
- return trimmed.slice(0, 100) + (trimmed.length > 100 ? '...' : '');
129
- }
130
- }
131
-
132
- return 'Pattern documentation available';
133
- } catch {
134
- return 'No description available';
135
- }
136
- }
137
-
138
- /**
139
- * Format pattern name from directory name
140
- */
141
- formatName(dirName) {
142
- return dirName
143
- .split(/[-_]/)
144
- .map(word => word.charAt(0).toUpperCase() + word.slice(1))
145
- .join(' ');
146
- }
147
-
148
- /**
149
- * Save registry to disk
150
- */
151
- async save() {
152
- const data = {
153
- version: '1.0.0',
154
- lastUpdated: new Date().toISOString(),
155
- patterns: Array.from(this.patterns.values())
156
- };
157
-
158
- writeFileSync(this.registryPath, JSON.stringify(data, null, 2));
159
- }
160
-
161
- /**
162
- * Get a pattern by ID
163
- * @param {string} id - Pattern ID (e.g., 'frontend/react')
164
- * @returns {Pattern|null}
165
- */
166
- get(id) {
167
- return this.patterns.get(id) || null;
168
- }
169
-
170
- /**
171
- * Get all patterns
172
- * @returns {Pattern[]}
173
- */
174
- getAll() {
175
- return Array.from(this.patterns.values());
176
- }
177
-
178
- /**
179
- * Get patterns by category
180
- * @param {string} category - Category name
181
- * @returns {Pattern[]}
182
- */
183
- getByCategory(category) {
184
- return this.getAll().filter(p => p.category === category);
185
- }
186
-
187
- /**
188
- * Search patterns by name or description
189
- * @param {string} query - Search query
190
- * @returns {Pattern[]}
191
- */
192
- search(query) {
193
- const lowerQuery = query.toLowerCase();
194
- return this.getAll().filter(p =>
195
- p.name.toLowerCase().includes(lowerQuery) ||
196
- p.description.toLowerCase().includes(lowerQuery) ||
197
- p.id.toLowerCase().includes(lowerQuery)
198
- );
199
- }
200
-
201
- /**
202
- * Register a new pattern
203
- * @param {Pattern} pattern - Pattern to register
204
- */
205
- async register(pattern) {
206
- // Ensure required fields
207
- if (!pattern.id || !pattern.name || !pattern.category) {
208
- throw new Error('Pattern must have id, name, and category');
209
- }
210
-
211
- // Create pattern directory if it doesn't exist
212
- const patternDir = join(this.patternsPath, pattern.path || pattern.id);
213
- if (!existsSync(patternDir)) {
214
- mkdirSync(patternDir, { recursive: true });
215
-
216
- // Create README.md with description
217
- const readmePath = join(patternDir, 'README.md');
218
- writeFileSync(readmePath, `# ${pattern.name}\n\n${pattern.description || ''}\n`);
219
- }
220
-
221
- // Set metadata
222
- pattern.metadata = pattern.metadata || {
223
- usageCount: 0,
224
- lastUsed: null,
225
- createdAt: new Date().toISOString()
226
- };
227
-
228
- this.patterns.set(pattern.id, pattern);
229
- await this.save();
230
- }
231
-
232
- /**
233
- * Remove a pattern
234
- * @param {string} id - Pattern ID
235
- */
236
- async remove(id) {
237
- this.patterns.delete(id);
238
- await this.save();
239
- }
240
-
241
- /**
242
- * Record pattern usage
243
- * @param {string} id - Pattern ID
244
- */
245
- async recordUsage(id) {
246
- const pattern = this.patterns.get(id);
247
- if (pattern) {
248
- pattern.metadata.usageCount++;
249
- pattern.metadata.lastUsed = new Date().toISOString();
250
- await this.save();
251
- }
252
- }
253
-
254
- /**
255
- * Get pattern content (README.md)
256
- * @param {string} id - Pattern ID
257
- * @returns {string|null}
258
- */
259
- getContent(id) {
260
- const pattern = this.patterns.get(id);
261
- if (!pattern) return null;
262
-
263
- // pattern.path is relative to aiPath (e.g., "patterns/frontend/react")
264
- const readmePath = join(this.aiPath, pattern.path, 'README.md');
265
- if (!existsSync(readmePath)) return null;
266
-
267
- return readFileSync(readmePath, 'utf8');
268
- }
269
-
270
- /**
271
- * Get all files in a pattern directory
272
- * @param {string} id - Pattern ID
273
- * @returns {Object[]} Array of {name, path, content}
274
- */
275
- getPatternFiles(id) {
276
- const pattern = this.patterns.get(id);
277
- if (!pattern) return [];
278
-
279
- // pattern.path is relative to aiPath (e.g., "patterns/frontend/react")
280
- const patternDir = join(this.aiPath, pattern.path);
281
- if (!existsSync(patternDir)) return [];
282
-
283
- const files = [];
284
- const entries = readdirSync(patternDir, { withFileTypes: true });
285
-
286
- for (const entry of entries) {
287
- if (entry.isFile()) {
288
- const filePath = join(patternDir, entry.name);
289
- files.push({
290
- name: entry.name,
291
- path: relative(this.aiPath, filePath),
292
- content: readFileSync(filePath, 'utf8')
293
- });
294
- }
295
- }
296
-
297
- return files;
298
- }
299
-
300
- /**
301
- * Get usage statistics
302
- * @returns {Object}
303
- */
304
- getStats() {
305
- const patterns = this.getAll();
306
- const byCategory = {};
307
- let totalUsage = 0;
308
- let mostUsed = null;
309
-
310
- for (const pattern of patterns) {
311
- // Count by category
312
- byCategory[pattern.category] = (byCategory[pattern.category] || 0) + 1;
313
-
314
- // Track total usage
315
- totalUsage += pattern.metadata.usageCount;
316
-
317
- // Track most used
318
- if (!mostUsed || pattern.metadata.usageCount > mostUsed.metadata.usageCount) {
319
- mostUsed = pattern;
320
- }
321
- }
322
-
323
- return {
324
- totalPatterns: patterns.length,
325
- byCategory,
326
- totalUsage,
327
- mostUsed: mostUsed ? { id: mostUsed.id, count: mostUsed.metadata.usageCount } : null
328
- };
329
- }
330
-
331
- /**
332
- * Associate trigger with pattern
333
- * @param {string} patternId - Pattern ID
334
- * @param {string} triggerId - Trigger ID
335
- */
336
- async addTrigger(patternId, triggerId) {
337
- const pattern = this.patterns.get(patternId);
338
- if (pattern && !pattern.triggers.includes(triggerId)) {
339
- pattern.triggers.push(triggerId);
340
- await this.save();
341
- }
342
- }
343
-
344
- /**
345
- * Remove trigger from pattern
346
- * @param {string} patternId - Pattern ID
347
- * @param {string} triggerId - Trigger ID
348
- */
349
- async removeTrigger(patternId, triggerId) {
350
- const pattern = this.patterns.get(patternId);
351
- if (pattern) {
352
- pattern.triggers = pattern.triggers.filter(t => t !== triggerId);
353
- await this.save();
354
- }
355
- }
356
- }
357
-
358
- /**
359
- * Create a new PatternRegistry instance for the given workspace
360
- * @param {string} workspacePath - Path to workspace root
361
- * @returns {PatternRegistry}
362
- */
363
- export function createRegistry(workspacePath) {
364
- return new PatternRegistry(workspacePath);
365
- }
366
-
367
- /**
368
- * Get pattern registry for current working directory
369
- * @returns {PatternRegistry}
370
- */
371
- export function getRegistry() {
372
- return createRegistry(process.cwd());
373
- }
374
-
375
- export default { PatternRegistry, createRegistry, getRegistry };
1
+ /**
2
+ * Pattern Registry - Manages pattern storage and retrieval
3
+ *
4
+ * Patterns are organized in categories:
5
+ * - frontend/ (react, vue, svelte, styling, component-libs)
6
+ * - backend/ (express, fastapi, database, auth)
7
+ * - universal/ (typescript, testing, error-handling, git, build-tools)
8
+ */
9
+
10
+ import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, statSync } from 'fs';
11
+ import { join, relative, basename, dirname } from 'path';
12
+
13
+ /**
14
+ * Pattern entry structure
15
+ * @typedef {Object} Pattern
16
+ * @property {string} id - Unique identifier (category/name)
17
+ * @property {string} name - Human-readable name
18
+ * @property {string} category - Pattern category (frontend, backend, universal)
19
+ * @property {string} path - Relative path to pattern directory
20
+ * @property {string} description - Pattern description
21
+ * @property {string[]} triggers - Associated trigger IDs
22
+ * @property {Object} metadata - Additional metadata
23
+ * @property {number} metadata.usageCount - How many times pattern was applied
24
+ * @property {string} metadata.lastUsed - ISO timestamp of last usage
25
+ * @property {string} metadata.createdAt - ISO timestamp of creation
26
+ */
27
+
28
+ /**
29
+ * PatternRegistry - Singleton for managing patterns
30
+ */
31
+ export class PatternRegistry {
32
+ constructor(workspacePath) {
33
+ this.workspacePath = workspacePath;
34
+ this.aiPath = join(workspacePath, '.ai');
35
+ this.patternsPath = join(this.aiPath, 'patterns');
36
+ this.registryPath = join(this.patternsPath, 'registry.json');
37
+ this.patterns = new Map();
38
+ this.loaded = false;
39
+ }
40
+
41
+ /**
42
+ * Initialize or load the registry
43
+ */
44
+ async load() {
45
+ if (this.loaded) return;
46
+
47
+ // Create patterns directory if it doesn't exist
48
+ if (!existsSync(this.patternsPath)) {
49
+ mkdirSync(this.patternsPath, { recursive: true });
50
+ }
51
+
52
+ // Load existing registry or scan for patterns
53
+ if (existsSync(this.registryPath)) {
54
+ try {
55
+ const data = JSON.parse(readFileSync(this.registryPath, 'utf8'));
56
+ for (const pattern of data.patterns || []) {
57
+ this.patterns.set(pattern.id, pattern);
58
+ }
59
+ } catch (error) {
60
+ console.warn('⚠️ Could not load registry, scanning patterns directory...');
61
+ await this.scan();
62
+ }
63
+ } else {
64
+ await this.scan();
65
+ }
66
+
67
+ this.loaded = true;
68
+ }
69
+
70
+ /**
71
+ * Scan patterns directory and build registry
72
+ */
73
+ async scan() {
74
+ this.patterns.clear();
75
+
76
+ const categories = ['frontend', 'backend', 'universal'];
77
+
78
+ for (const category of categories) {
79
+ const categoryPath = join(this.patternsPath, category);
80
+ if (!existsSync(categoryPath)) continue;
81
+
82
+ const entries = readdirSync(categoryPath, { withFileTypes: true });
83
+
84
+ for (const entry of entries) {
85
+ if (!entry.isDirectory()) continue;
86
+
87
+ const patternPath = join(categoryPath, entry.name);
88
+ const readmePath = join(patternPath, 'README.md');
89
+
90
+ const pattern = {
91
+ id: `${category}/${entry.name}`,
92
+ name: this.formatName(entry.name),
93
+ category,
94
+ path: relative(this.aiPath, patternPath),
95
+ description: this.extractDescription(readmePath),
96
+ triggers: [],
97
+ metadata: {
98
+ usageCount: 0,
99
+ lastUsed: null,
100
+ createdAt: new Date().toISOString()
101
+ }
102
+ };
103
+
104
+ this.patterns.set(pattern.id, pattern);
105
+ }
106
+ }
107
+
108
+ // Save the scanned registry
109
+ await this.save();
110
+ }
111
+
112
+ /**
113
+ * Extract description from README.md first line
114
+ */
115
+ extractDescription(readmePath) {
116
+ if (!existsSync(readmePath)) {
117
+ return 'No description available';
118
+ }
119
+
120
+ try {
121
+ const content = readFileSync(readmePath, 'utf8');
122
+ const lines = content.split('\n');
123
+
124
+ // Find first non-empty, non-heading line
125
+ for (const line of lines) {
126
+ const trimmed = line.trim();
127
+ if (trimmed && !trimmed.startsWith('#') && !trimmed.startsWith('---')) {
128
+ return trimmed.slice(0, 100) + (trimmed.length > 100 ? '...' : '');
129
+ }
130
+ }
131
+
132
+ return 'Pattern documentation available';
133
+ } catch {
134
+ return 'No description available';
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Format pattern name from directory name
140
+ */
141
+ formatName(dirName) {
142
+ return dirName
143
+ .split(/[-_]/)
144
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
145
+ .join(' ');
146
+ }
147
+
148
+ /**
149
+ * Save registry to disk
150
+ */
151
+ async save() {
152
+ const data = {
153
+ version: '1.0.0',
154
+ lastUpdated: new Date().toISOString(),
155
+ patterns: Array.from(this.patterns.values())
156
+ };
157
+
158
+ writeFileSync(this.registryPath, JSON.stringify(data, null, 2));
159
+ }
160
+
161
+ /**
162
+ * Get a pattern by ID
163
+ * @param {string} id - Pattern ID (e.g., 'frontend/react')
164
+ * @returns {Pattern|null}
165
+ */
166
+ get(id) {
167
+ return this.patterns.get(id) || null;
168
+ }
169
+
170
+ /**
171
+ * Get all patterns
172
+ * @returns {Pattern[]}
173
+ */
174
+ getAll() {
175
+ return Array.from(this.patterns.values());
176
+ }
177
+
178
+ /**
179
+ * Get patterns by category
180
+ * @param {string} category - Category name
181
+ * @returns {Pattern[]}
182
+ */
183
+ getByCategory(category) {
184
+ return this.getAll().filter(p => p.category === category);
185
+ }
186
+
187
+ /**
188
+ * Search patterns by name or description
189
+ * @param {string} query - Search query
190
+ * @returns {Pattern[]}
191
+ */
192
+ search(query) {
193
+ const lowerQuery = query.toLowerCase();
194
+ return this.getAll().filter(p =>
195
+ p.name.toLowerCase().includes(lowerQuery) ||
196
+ p.description.toLowerCase().includes(lowerQuery) ||
197
+ p.id.toLowerCase().includes(lowerQuery)
198
+ );
199
+ }
200
+
201
+ /**
202
+ * Register a new pattern
203
+ * @param {Pattern} pattern - Pattern to register
204
+ */
205
+ async register(pattern) {
206
+ // Ensure required fields
207
+ if (!pattern.id || !pattern.name || !pattern.category) {
208
+ throw new Error('Pattern must have id, name, and category');
209
+ }
210
+
211
+ // Create pattern directory if it doesn't exist
212
+ const patternDir = join(this.patternsPath, pattern.path || pattern.id);
213
+ if (!existsSync(patternDir)) {
214
+ mkdirSync(patternDir, { recursive: true });
215
+
216
+ // Create README.md with description
217
+ const readmePath = join(patternDir, 'README.md');
218
+ writeFileSync(readmePath, `# ${pattern.name}\n\n${pattern.description || ''}\n`);
219
+ }
220
+
221
+ // Set metadata
222
+ pattern.metadata = pattern.metadata || {
223
+ usageCount: 0,
224
+ lastUsed: null,
225
+ createdAt: new Date().toISOString()
226
+ };
227
+
228
+ this.patterns.set(pattern.id, pattern);
229
+ await this.save();
230
+ }
231
+
232
+ /**
233
+ * Remove a pattern
234
+ * @param {string} id - Pattern ID
235
+ */
236
+ async remove(id) {
237
+ this.patterns.delete(id);
238
+ await this.save();
239
+ }
240
+
241
+ /**
242
+ * Record pattern usage
243
+ * @param {string} id - Pattern ID
244
+ */
245
+ async recordUsage(id) {
246
+ const pattern = this.patterns.get(id);
247
+ if (pattern) {
248
+ pattern.metadata.usageCount++;
249
+ pattern.metadata.lastUsed = new Date().toISOString();
250
+ await this.save();
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Get pattern content (README.md)
256
+ * @param {string} id - Pattern ID
257
+ * @returns {string|null}
258
+ */
259
+ getContent(id) {
260
+ const pattern = this.patterns.get(id);
261
+ if (!pattern) return null;
262
+
263
+ // pattern.path is relative to aiPath (e.g., "patterns/frontend/react")
264
+ const readmePath = join(this.aiPath, pattern.path, 'README.md');
265
+ if (!existsSync(readmePath)) return null;
266
+
267
+ return readFileSync(readmePath, 'utf8');
268
+ }
269
+
270
+ /**
271
+ * Get all files in a pattern directory
272
+ * @param {string} id - Pattern ID
273
+ * @returns {Object[]} Array of {name, path, content}
274
+ */
275
+ getPatternFiles(id) {
276
+ const pattern = this.patterns.get(id);
277
+ if (!pattern) return [];
278
+
279
+ // pattern.path is relative to aiPath (e.g., "patterns/frontend/react")
280
+ const patternDir = join(this.aiPath, pattern.path);
281
+ if (!existsSync(patternDir)) return [];
282
+
283
+ const files = [];
284
+ const entries = readdirSync(patternDir, { withFileTypes: true });
285
+
286
+ for (const entry of entries) {
287
+ if (entry.isFile()) {
288
+ const filePath = join(patternDir, entry.name);
289
+ files.push({
290
+ name: entry.name,
291
+ path: relative(this.aiPath, filePath),
292
+ content: readFileSync(filePath, 'utf8')
293
+ });
294
+ }
295
+ }
296
+
297
+ return files;
298
+ }
299
+
300
+ /**
301
+ * Get usage statistics
302
+ * @returns {Object}
303
+ */
304
+ getStats() {
305
+ const patterns = this.getAll();
306
+ const byCategory = {};
307
+ let totalUsage = 0;
308
+ let mostUsed = null;
309
+
310
+ for (const pattern of patterns) {
311
+ // Count by category
312
+ byCategory[pattern.category] = (byCategory[pattern.category] || 0) + 1;
313
+
314
+ // Track total usage
315
+ totalUsage += pattern.metadata.usageCount;
316
+
317
+ // Track most used
318
+ if (!mostUsed || pattern.metadata.usageCount > mostUsed.metadata.usageCount) {
319
+ mostUsed = pattern;
320
+ }
321
+ }
322
+
323
+ return {
324
+ totalPatterns: patterns.length,
325
+ byCategory,
326
+ totalUsage,
327
+ mostUsed: mostUsed ? { id: mostUsed.id, count: mostUsed.metadata.usageCount } : null
328
+ };
329
+ }
330
+
331
+ /**
332
+ * Associate trigger with pattern
333
+ * @param {string} patternId - Pattern ID
334
+ * @param {string} triggerId - Trigger ID
335
+ */
336
+ async addTrigger(patternId, triggerId) {
337
+ const pattern = this.patterns.get(patternId);
338
+ if (pattern && !pattern.triggers.includes(triggerId)) {
339
+ pattern.triggers.push(triggerId);
340
+ await this.save();
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Remove trigger from pattern
346
+ * @param {string} patternId - Pattern ID
347
+ * @param {string} triggerId - Trigger ID
348
+ */
349
+ async removeTrigger(patternId, triggerId) {
350
+ const pattern = this.patterns.get(patternId);
351
+ if (pattern) {
352
+ pattern.triggers = pattern.triggers.filter(t => t !== triggerId);
353
+ await this.save();
354
+ }
355
+ }
356
+ }
357
+
358
+ /**
359
+ * Create a new PatternRegistry instance for the given workspace
360
+ * @param {string} workspacePath - Path to workspace root
361
+ * @returns {PatternRegistry}
362
+ */
363
+ export function createRegistry(workspacePath) {
364
+ return new PatternRegistry(workspacePath);
365
+ }
366
+
367
+ /**
368
+ * Get pattern registry for current working directory
369
+ * @returns {PatternRegistry}
370
+ */
371
+ export function getRegistry() {
372
+ return createRegistry(process.cwd());
373
+ }
374
+
375
+ export default { PatternRegistry, createRegistry, getRegistry };