@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.
- package/LICENSE +21 -0
- package/README.md +251 -0
- package/cli/batch.js +333 -0
- package/cli/codespaces.js +303 -0
- package/cli/index.js +91 -0
- package/cli/inject.js +53 -0
- package/cli/knowledge.js +531 -0
- package/cli/notify.js +135 -0
- package/cli/patterns.js +665 -0
- package/cli/status.js +91 -0
- package/cli/validate.js +61 -0
- package/package.json +70 -0
- package/src/core/inject.js +128 -0
- package/src/core/state.js +91 -0
- package/src/core/validate.js +202 -0
- package/src/core/workspace.js +176 -0
- package/src/index.js +20 -0
- package/src/knowledge/gc.js +308 -0
- package/src/knowledge/lifecycle.js +401 -0
- package/src/knowledge/promote.js +364 -0
- package/src/knowledge/references.js +342 -0
- package/src/patterns/matcher.js +218 -0
- package/src/patterns/registry.js +375 -0
- package/src/patterns/triggers.js +423 -0
- package/src/services/batch-service.js +849 -0
- package/src/services/notification-service.js +342 -0
- package/templates/codespaces/devcontainer.json +52 -0
- package/templates/codespaces/setup.sh +70 -0
- package/templates/workspace/.ai/README.md +164 -0
- package/templates/workspace/.ai/agents/README.md +204 -0
- package/templates/workspace/.ai/agents/claude.md +625 -0
- package/templates/workspace/.ai/config/.gitkeep +0 -0
- package/templates/workspace/.ai/config/README.md +79 -0
- package/templates/workspace/.ai/config/notifications.template.json +20 -0
- package/templates/workspace/.ai/memory/deadends.md +79 -0
- package/templates/workspace/.ai/memory/decisions.md +58 -0
- package/templates/workspace/.ai/memory/patterns.md +65 -0
- package/templates/workspace/.ai/patterns/backend/README.md +126 -0
- package/templates/workspace/.ai/patterns/frontend/README.md +103 -0
- package/templates/workspace/.ai/patterns/index.md +256 -0
- package/templates/workspace/.ai/patterns/triggers.json +1087 -0
- package/templates/workspace/.ai/patterns/universal/README.md +141 -0
- package/templates/workspace/.ai/state.template.json +8 -0
- package/templates/workspace/.ai/tasks/active.md +77 -0
- package/templates/workspace/.ai/tasks/backlog.md +95 -0
- 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 };
|