@fermindi/pwn-cli 0.1.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 -0
  2. package/README.md +251 -0
  3. package/cli/batch.js +333 -0
  4. package/cli/codespaces.js +303 -0
  5. package/cli/index.js +91 -0
  6. package/cli/inject.js +53 -0
  7. package/cli/knowledge.js +531 -0
  8. package/cli/notify.js +135 -0
  9. package/cli/patterns.js +665 -0
  10. package/cli/status.js +91 -0
  11. package/cli/validate.js +61 -0
  12. package/package.json +70 -0
  13. package/src/core/inject.js +128 -0
  14. package/src/core/state.js +91 -0
  15. package/src/core/validate.js +202 -0
  16. package/src/core/workspace.js +176 -0
  17. package/src/index.js +20 -0
  18. package/src/knowledge/gc.js +308 -0
  19. package/src/knowledge/lifecycle.js +401 -0
  20. package/src/knowledge/promote.js +364 -0
  21. package/src/knowledge/references.js +342 -0
  22. package/src/patterns/matcher.js +218 -0
  23. package/src/patterns/registry.js +375 -0
  24. package/src/patterns/triggers.js +423 -0
  25. package/src/services/batch-service.js +849 -0
  26. package/src/services/notification-service.js +342 -0
  27. package/templates/codespaces/devcontainer.json +52 -0
  28. package/templates/codespaces/setup.sh +70 -0
  29. package/templates/workspace/.ai/README.md +164 -0
  30. package/templates/workspace/.ai/agents/README.md +204 -0
  31. package/templates/workspace/.ai/agents/claude.md +625 -0
  32. package/templates/workspace/.ai/config/.gitkeep +0 -0
  33. package/templates/workspace/.ai/config/README.md +79 -0
  34. package/templates/workspace/.ai/config/notifications.template.json +20 -0
  35. package/templates/workspace/.ai/memory/deadends.md +79 -0
  36. package/templates/workspace/.ai/memory/decisions.md +58 -0
  37. package/templates/workspace/.ai/memory/patterns.md +65 -0
  38. package/templates/workspace/.ai/patterns/backend/README.md +126 -0
  39. package/templates/workspace/.ai/patterns/frontend/README.md +103 -0
  40. package/templates/workspace/.ai/patterns/index.md +256 -0
  41. package/templates/workspace/.ai/patterns/triggers.json +1087 -0
  42. package/templates/workspace/.ai/patterns/universal/README.md +141 -0
  43. package/templates/workspace/.ai/state.template.json +8 -0
  44. package/templates/workspace/.ai/tasks/active.md +77 -0
  45. package/templates/workspace/.ai/tasks/backlog.md +95 -0
  46. package/templates/workspace/.ai/workflows/batch-task.md +356 -0
@@ -0,0 +1,423 @@
1
+ /**
2
+ * Trigger Evaluation Engine
3
+ *
4
+ * Evaluates triggers against file paths, content, and commands
5
+ * to determine which patterns should be auto-loaded.
6
+ *
7
+ * Trigger Types:
8
+ * - fileExt: Match file extensions (*.tsx, *.py)
9
+ * - path: Match directory paths (src/components/*)
10
+ * - import: Match import statements (import.*react)
11
+ * - keyword: Match code keywords (interface, class)
12
+ * - command: Match CLI commands (npm test, pnpm dev)
13
+ */
14
+
15
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
16
+ import { join, extname, relative, basename, dirname } from 'path';
17
+ import { minimatch } from './matcher.js';
18
+
19
+ /**
20
+ * Trigger definition
21
+ * @typedef {Object} Trigger
22
+ * @property {string} id - Unique identifier
23
+ * @property {string} name - Human-readable name
24
+ * @property {string} type - Trigger type (fileExt, path, import, keyword, command)
25
+ * @property {string|string[]} value - Pattern(s) to match
26
+ * @property {string[]} patterns - Pattern IDs to load when triggered
27
+ * @property {string} description - Trigger description
28
+ * @property {number} priority - Evaluation priority (higher = first)
29
+ * @property {boolean} enabled - Whether trigger is active
30
+ */
31
+
32
+ /**
33
+ * Trigger type priorities (higher = evaluated first)
34
+ */
35
+ const TYPE_PRIORITY = {
36
+ fileExt: 100,
37
+ path: 80,
38
+ import: 60,
39
+ keyword: 40,
40
+ command: 20
41
+ };
42
+
43
+ /**
44
+ * TriggerEngine - Evaluates triggers against context
45
+ */
46
+ export class TriggerEngine {
47
+ constructor(workspacePath) {
48
+ this.workspacePath = workspacePath;
49
+ this.aiPath = join(workspacePath, '.ai');
50
+ this.triggersPath = join(this.aiPath, 'patterns', 'triggers.json');
51
+ this.triggers = new Map();
52
+ this.loaded = false;
53
+ }
54
+
55
+ /**
56
+ * Load triggers from configuration
57
+ */
58
+ async load() {
59
+ if (this.loaded) return;
60
+
61
+ if (existsSync(this.triggersPath)) {
62
+ try {
63
+ const data = JSON.parse(readFileSync(this.triggersPath, 'utf8'));
64
+ for (const trigger of data.triggers || []) {
65
+ this.triggers.set(trigger.id, trigger);
66
+ }
67
+ } catch (error) {
68
+ console.warn('⚠️ Could not load triggers:', error.message);
69
+ }
70
+ }
71
+
72
+ this.loaded = true;
73
+ }
74
+
75
+ /**
76
+ * Save triggers to configuration
77
+ */
78
+ async save() {
79
+ const dir = dirname(this.triggersPath);
80
+ if (!existsSync(dir)) {
81
+ mkdirSync(dir, { recursive: true });
82
+ }
83
+
84
+ const data = {
85
+ version: '1.0.0',
86
+ lastUpdated: new Date().toISOString(),
87
+ triggers: Array.from(this.triggers.values())
88
+ };
89
+
90
+ writeFileSync(this.triggersPath, JSON.stringify(data, null, 2));
91
+ }
92
+
93
+ /**
94
+ * Register a new trigger
95
+ * @param {Trigger} trigger
96
+ */
97
+ async register(trigger) {
98
+ // Generate ID if not provided
99
+ if (!trigger.id) {
100
+ trigger.id = `${trigger.type}-${Date.now()}`;
101
+ }
102
+
103
+ // Set defaults
104
+ trigger.priority = trigger.priority ?? TYPE_PRIORITY[trigger.type] ?? 0;
105
+ trigger.enabled = trigger.enabled ?? true;
106
+ trigger.patterns = trigger.patterns || [];
107
+
108
+ this.triggers.set(trigger.id, trigger);
109
+ await this.save();
110
+ }
111
+
112
+ /**
113
+ * Remove a trigger
114
+ * @param {string} id
115
+ */
116
+ async remove(id) {
117
+ this.triggers.delete(id);
118
+ await this.save();
119
+ }
120
+
121
+ /**
122
+ * Enable/disable a trigger
123
+ * @param {string} id
124
+ * @param {boolean} enabled
125
+ */
126
+ async setEnabled(id, enabled) {
127
+ const trigger = this.triggers.get(id);
128
+ if (trigger) {
129
+ trigger.enabled = enabled;
130
+ await this.save();
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Get all triggers sorted by priority
136
+ * @returns {Trigger[]}
137
+ */
138
+ getAll() {
139
+ return Array.from(this.triggers.values())
140
+ .filter(t => t.enabled)
141
+ .sort((a, b) => b.priority - a.priority);
142
+ }
143
+
144
+ /**
145
+ * Get triggers by type
146
+ * @param {string} type
147
+ * @returns {Trigger[]}
148
+ */
149
+ getByType(type) {
150
+ return this.getAll().filter(t => t.type === type);
151
+ }
152
+
153
+ /**
154
+ * Evaluate all triggers against a file path
155
+ * @param {string} filePath - Absolute or relative file path
156
+ * @returns {EvaluationResult}
157
+ */
158
+ evaluateFile(filePath) {
159
+ const relPath = relative(this.workspacePath, filePath);
160
+ const ext = extname(filePath);
161
+ const name = basename(filePath);
162
+
163
+ const matched = [];
164
+ const patterns = new Set();
165
+
166
+ for (const trigger of this.getAll()) {
167
+ if (trigger.type === 'fileExt') {
168
+ if (this.matchFileExt(ext, name, trigger.value)) {
169
+ matched.push({ trigger, reason: `File extension matches ${trigger.value}` });
170
+ trigger.patterns.forEach(p => patterns.add(p));
171
+ }
172
+ } else if (trigger.type === 'path') {
173
+ if (this.matchPath(relPath, trigger.value)) {
174
+ matched.push({ trigger, reason: `Path matches ${trigger.value}` });
175
+ trigger.patterns.forEach(p => patterns.add(p));
176
+ }
177
+ }
178
+ }
179
+
180
+ return {
181
+ filePath: relPath,
182
+ matchedTriggers: matched,
183
+ patterns: Array.from(patterns)
184
+ };
185
+ }
186
+
187
+ /**
188
+ * Evaluate triggers against file content
189
+ * @param {string} content - File content
190
+ * @param {string} [filePath] - Optional file path for context
191
+ * @returns {EvaluationResult}
192
+ */
193
+ evaluateContent(content, filePath = null) {
194
+ const matched = [];
195
+ const patterns = new Set();
196
+
197
+ // Start with file-based evaluation if path provided
198
+ if (filePath) {
199
+ const fileResult = this.evaluateFile(filePath);
200
+ fileResult.matchedTriggers.forEach(m => matched.push(m));
201
+ fileResult.patterns.forEach(p => patterns.add(p));
202
+ }
203
+
204
+ // Evaluate import triggers
205
+ for (const trigger of this.getByType('import')) {
206
+ if (this.matchImport(content, trigger.value)) {
207
+ matched.push({ trigger, reason: `Import matches ${trigger.value}` });
208
+ trigger.patterns.forEach(p => patterns.add(p));
209
+ }
210
+ }
211
+
212
+ // Evaluate keyword triggers
213
+ for (const trigger of this.getByType('keyword')) {
214
+ if (this.matchKeyword(content, trigger.value)) {
215
+ matched.push({ trigger, reason: `Keyword matches ${trigger.value}` });
216
+ trigger.patterns.forEach(p => patterns.add(p));
217
+ }
218
+ }
219
+
220
+ return {
221
+ filePath: filePath ? relative(this.workspacePath, filePath) : null,
222
+ matchedTriggers: matched,
223
+ patterns: Array.from(patterns)
224
+ };
225
+ }
226
+
227
+ /**
228
+ * Evaluate triggers against a command
229
+ * @param {string} command - CLI command
230
+ * @returns {EvaluationResult}
231
+ */
232
+ evaluateCommand(command) {
233
+ const matched = [];
234
+ const patterns = new Set();
235
+
236
+ for (const trigger of this.getByType('command')) {
237
+ if (this.matchCommand(command, trigger.value)) {
238
+ matched.push({ trigger, reason: `Command matches ${trigger.value}` });
239
+ trigger.patterns.forEach(p => patterns.add(p));
240
+ }
241
+ }
242
+
243
+ return {
244
+ command,
245
+ matchedTriggers: matched,
246
+ patterns: Array.from(patterns)
247
+ };
248
+ }
249
+
250
+ /**
251
+ * Match file extension pattern
252
+ * @private
253
+ */
254
+ matchFileExt(ext, fileName, patterns) {
255
+ const values = Array.isArray(patterns) ? patterns : [patterns];
256
+
257
+ for (const pattern of values) {
258
+ // Handle *.ext patterns
259
+ if (pattern.startsWith('*.')) {
260
+ const targetExt = '.' + pattern.slice(2);
261
+ if (ext === targetExt) return true;
262
+ }
263
+ // Handle exact file name patterns
264
+ else if (fileName === pattern) {
265
+ return true;
266
+ }
267
+ // Handle glob patterns
268
+ else if (minimatch(fileName, pattern)) {
269
+ return true;
270
+ }
271
+ }
272
+
273
+ return false;
274
+ }
275
+
276
+ /**
277
+ * Match path pattern
278
+ * @private
279
+ */
280
+ matchPath(filePath, patterns) {
281
+ const values = Array.isArray(patterns) ? patterns : [patterns];
282
+ const normalizedPath = filePath.replace(/\\/g, '/');
283
+
284
+ for (const pattern of values) {
285
+ const normalizedPattern = pattern.replace(/\\/g, '/');
286
+
287
+ // Exact path match
288
+ if (normalizedPath === normalizedPattern) return true;
289
+
290
+ // Glob pattern match
291
+ if (minimatch(normalizedPath, normalizedPattern)) return true;
292
+
293
+ // Prefix match for directory patterns
294
+ if (normalizedPattern.endsWith('/') &&
295
+ normalizedPath.startsWith(normalizedPattern.slice(0, -1))) {
296
+ return true;
297
+ }
298
+ }
299
+
300
+ return false;
301
+ }
302
+
303
+ /**
304
+ * Match import pattern (regex)
305
+ * @private
306
+ */
307
+ matchImport(content, patterns) {
308
+ const values = Array.isArray(patterns) ? patterns : [patterns];
309
+
310
+ for (const pattern of values) {
311
+ try {
312
+ const regex = new RegExp(pattern, 'm');
313
+ if (regex.test(content)) return true;
314
+ } catch {
315
+ // Invalid regex, try literal match
316
+ if (content.includes(pattern)) return true;
317
+ }
318
+ }
319
+
320
+ return false;
321
+ }
322
+
323
+ /**
324
+ * Match keyword pattern
325
+ * @private
326
+ */
327
+ matchKeyword(content, patterns) {
328
+ const values = Array.isArray(patterns) ? patterns : [patterns];
329
+
330
+ for (const pattern of values) {
331
+ try {
332
+ // Create word boundary regex
333
+ const regex = new RegExp(`\\b${pattern}\\b`, 'm');
334
+ if (regex.test(content)) return true;
335
+ } catch {
336
+ // Invalid regex, try literal match
337
+ if (content.includes(pattern)) return true;
338
+ }
339
+ }
340
+
341
+ return false;
342
+ }
343
+
344
+ /**
345
+ * Match command pattern
346
+ * @private
347
+ */
348
+ matchCommand(command, patterns) {
349
+ const values = Array.isArray(patterns) ? patterns : [patterns];
350
+ const normalizedCmd = command.toLowerCase().trim();
351
+
352
+ for (const pattern of values) {
353
+ const normalizedPattern = pattern.toLowerCase();
354
+
355
+ // Exact match
356
+ if (normalizedCmd === normalizedPattern) return true;
357
+
358
+ // Starts with match (for commands with args)
359
+ if (normalizedCmd.startsWith(normalizedPattern)) return true;
360
+
361
+ // Contains match
362
+ if (normalizedCmd.includes(normalizedPattern)) return true;
363
+
364
+ // Regex match
365
+ try {
366
+ const regex = new RegExp(normalizedPattern);
367
+ if (regex.test(normalizedCmd)) return true;
368
+ } catch {
369
+ // Not a valid regex, skip
370
+ }
371
+ }
372
+
373
+ return false;
374
+ }
375
+
376
+ /**
377
+ * Get evaluation summary
378
+ * @returns {Object}
379
+ */
380
+ getSummary() {
381
+ const triggers = this.getAll();
382
+ const byType = {};
383
+
384
+ for (const trigger of triggers) {
385
+ byType[trigger.type] = (byType[trigger.type] || 0) + 1;
386
+ }
387
+
388
+ return {
389
+ totalTriggers: triggers.length,
390
+ byType,
391
+ enabledCount: triggers.length,
392
+ disabledCount: Array.from(this.triggers.values()).filter(t => !t.enabled).length
393
+ };
394
+ }
395
+ }
396
+
397
+ /**
398
+ * Evaluation result
399
+ * @typedef {Object} EvaluationResult
400
+ * @property {string|null} filePath - File path if evaluated
401
+ * @property {string|null} command - Command if evaluated
402
+ * @property {Array<{trigger: Trigger, reason: string}>} matchedTriggers
403
+ * @property {string[]} patterns - Pattern IDs to load
404
+ */
405
+
406
+ /**
407
+ * Create a new TriggerEngine instance
408
+ * @param {string} workspacePath
409
+ * @returns {TriggerEngine}
410
+ */
411
+ export function createTriggerEngine(workspacePath) {
412
+ return new TriggerEngine(workspacePath);
413
+ }
414
+
415
+ /**
416
+ * Get trigger engine for current working directory
417
+ * @returns {TriggerEngine}
418
+ */
419
+ export function getTriggerEngine() {
420
+ return createTriggerEngine(process.cwd());
421
+ }
422
+
423
+ export default { TriggerEngine, createTriggerEngine, getTriggerEngine };