@fermindi/pwn-cli 0.1.0 → 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.
- package/LICENSE +21 -21
- package/README.md +265 -251
- package/cli/batch.js +333 -333
- package/cli/codespaces.js +303 -303
- package/cli/index.js +98 -91
- package/cli/inject.js +78 -53
- package/cli/knowledge.js +531 -531
- package/cli/migrate.js +466 -0
- package/cli/notify.js +135 -135
- package/cli/patterns.js +665 -665
- package/cli/status.js +91 -91
- package/cli/validate.js +61 -61
- package/package.json +70 -70
- package/src/core/inject.js +208 -128
- package/src/core/state.js +91 -91
- package/src/core/validate.js +202 -202
- package/src/core/workspace.js +176 -176
- package/src/index.js +20 -20
- package/src/knowledge/gc.js +308 -308
- package/src/knowledge/lifecycle.js +401 -401
- package/src/knowledge/promote.js +364 -364
- package/src/knowledge/references.js +342 -342
- package/src/patterns/matcher.js +218 -218
- package/src/patterns/registry.js +375 -375
- package/src/patterns/triggers.js +423 -423
- package/src/services/batch-service.js +849 -849
- package/src/services/notification-service.js +342 -342
- package/templates/codespaces/devcontainer.json +52 -52
- package/templates/codespaces/setup.sh +70 -70
- package/templates/workspace/.ai/README.md +164 -164
- package/templates/workspace/.ai/agents/README.md +204 -204
- package/templates/workspace/.ai/agents/claude.md +625 -625
- package/templates/workspace/.ai/config/README.md +79 -79
- package/templates/workspace/.ai/config/notifications.template.json +20 -20
- package/templates/workspace/.ai/memory/deadends.md +79 -79
- package/templates/workspace/.ai/memory/decisions.md +58 -58
- package/templates/workspace/.ai/memory/patterns.md +65 -65
- package/templates/workspace/.ai/patterns/backend/README.md +126 -126
- package/templates/workspace/.ai/patterns/frontend/README.md +103 -103
- package/templates/workspace/.ai/patterns/index.md +256 -256
- package/templates/workspace/.ai/patterns/triggers.json +1087 -1087
- package/templates/workspace/.ai/patterns/universal/README.md +141 -141
- package/templates/workspace/.ai/state.template.json +8 -8
- package/templates/workspace/.ai/tasks/active.md +77 -77
- package/templates/workspace/.ai/tasks/backlog.md +95 -95
- package/templates/workspace/.ai/workflows/batch-task.md +356 -356
|
@@ -1,401 +1,401 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
2
|
-
import { join } from 'path';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Knowledge Item Status
|
|
6
|
-
* @typedef {'active' | 'candidate_pattern' | 'archived' | 'garbage'} KnowledgeStatus
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Knowledge Item
|
|
11
|
-
* @typedef {Object} KnowledgeItem
|
|
12
|
-
* @property {string} id - Item ID (DEC-001, DE-001)
|
|
13
|
-
* @property {string} type - 'decision' | 'deadend'
|
|
14
|
-
* @property {string} title - Item title
|
|
15
|
-
* @property {string} status - Current status
|
|
16
|
-
* @property {string[]} relatedFiles - Code files that reference this
|
|
17
|
-
* @property {string[]} relatedDecisions - Other decisions that reference
|
|
18
|
-
* @property {string[]} relatedTasks - Tasks that depend on this
|
|
19
|
-
* @property {number} accessCount - Times consulted
|
|
20
|
-
* @property {number} appliedCount - Times applied in code
|
|
21
|
-
* @property {string[]} appliedContexts - Different contexts applied
|
|
22
|
-
* @property {string} createdAt - ISO date string
|
|
23
|
-
* @property {string} lastAccessed - ISO date string
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Get the path to knowledge.json registry
|
|
28
|
-
* @param {string} cwd - Working directory
|
|
29
|
-
* @returns {string}
|
|
30
|
-
*/
|
|
31
|
-
export function getKnowledgePath(cwd = process.cwd()) {
|
|
32
|
-
return join(cwd, '.ai', 'config', 'knowledge.json');
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Initialize knowledge registry if it doesn't exist
|
|
37
|
-
* @param {string} cwd - Working directory
|
|
38
|
-
* @returns {Object} Knowledge registry
|
|
39
|
-
*/
|
|
40
|
-
export function initKnowledgeRegistry(cwd = process.cwd()) {
|
|
41
|
-
const knowledgePath = getKnowledgePath(cwd);
|
|
42
|
-
const configDir = join(cwd, '.ai', 'config');
|
|
43
|
-
|
|
44
|
-
if (!existsSync(configDir)) {
|
|
45
|
-
mkdirSync(configDir, { recursive: true });
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (!existsSync(knowledgePath)) {
|
|
49
|
-
const initial = {
|
|
50
|
-
version: '1.0.0',
|
|
51
|
-
items: {},
|
|
52
|
-
lastGC: null,
|
|
53
|
-
lastSync: new Date().toISOString()
|
|
54
|
-
};
|
|
55
|
-
writeFileSync(knowledgePath, JSON.stringify(initial, null, 2));
|
|
56
|
-
return initial;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return JSON.parse(readFileSync(knowledgePath, 'utf8'));
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Get knowledge registry
|
|
64
|
-
* @param {string} cwd - Working directory
|
|
65
|
-
* @returns {Object|null}
|
|
66
|
-
*/
|
|
67
|
-
export function getKnowledgeRegistry(cwd = process.cwd()) {
|
|
68
|
-
const knowledgePath = getKnowledgePath(cwd);
|
|
69
|
-
|
|
70
|
-
if (!existsSync(knowledgePath)) {
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
return JSON.parse(readFileSync(knowledgePath, 'utf8'));
|
|
76
|
-
} catch {
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Save knowledge registry
|
|
83
|
-
* @param {Object} registry - Registry to save
|
|
84
|
-
* @param {string} cwd - Working directory
|
|
85
|
-
*/
|
|
86
|
-
export function saveKnowledgeRegistry(registry, cwd = process.cwd()) {
|
|
87
|
-
const knowledgePath = getKnowledgePath(cwd);
|
|
88
|
-
registry.lastSync = new Date().toISOString();
|
|
89
|
-
writeFileSync(knowledgePath, JSON.stringify(registry, null, 2));
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Parse decisions.md to extract decision items
|
|
94
|
-
* @param {string} cwd - Working directory
|
|
95
|
-
* @returns {KnowledgeItem[]}
|
|
96
|
-
*/
|
|
97
|
-
export function parseDecisions(cwd = process.cwd()) {
|
|
98
|
-
const decisionsPath = join(cwd, '.ai', 'memory', 'decisions.md');
|
|
99
|
-
|
|
100
|
-
if (!existsSync(decisionsPath)) {
|
|
101
|
-
return [];
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const content = readFileSync(decisionsPath, 'utf8');
|
|
105
|
-
const items = [];
|
|
106
|
-
|
|
107
|
-
// Match pattern: ## DEC-XXX: Title
|
|
108
|
-
const decisionRegex = /^## (DEC-\d+): (.+)$/gm;
|
|
109
|
-
let match;
|
|
110
|
-
|
|
111
|
-
while ((match = decisionRegex.exec(content)) !== null) {
|
|
112
|
-
const id = match[1];
|
|
113
|
-
const title = match[2];
|
|
114
|
-
|
|
115
|
-
// Extract status from the block
|
|
116
|
-
const blockStart = match.index;
|
|
117
|
-
const nextMatch = content.indexOf('\n## ', blockStart + 1);
|
|
118
|
-
const blockEnd = nextMatch === -1 ? content.length : nextMatch;
|
|
119
|
-
const block = content.substring(blockStart, blockEnd);
|
|
120
|
-
|
|
121
|
-
const statusMatch = block.match(/\*\*Status:\*\* (\w+)/);
|
|
122
|
-
const dateMatch = block.match(/\*\*Date:\*\* ([\d-]+)/);
|
|
123
|
-
|
|
124
|
-
items.push({
|
|
125
|
-
id,
|
|
126
|
-
type: 'decision',
|
|
127
|
-
title,
|
|
128
|
-
status: statusMatch ? statusMatch[1].toLowerCase() : 'active',
|
|
129
|
-
relatedFiles: [],
|
|
130
|
-
relatedDecisions: [],
|
|
131
|
-
relatedTasks: [],
|
|
132
|
-
accessCount: 0,
|
|
133
|
-
appliedCount: 0,
|
|
134
|
-
appliedContexts: [],
|
|
135
|
-
createdAt: dateMatch ? dateMatch[1] : null,
|
|
136
|
-
lastAccessed: null
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return items;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Parse deadends.md to extract dead-end items
|
|
145
|
-
* @param {string} cwd - Working directory
|
|
146
|
-
* @returns {KnowledgeItem[]}
|
|
147
|
-
*/
|
|
148
|
-
export function parseDeadends(cwd = process.cwd()) {
|
|
149
|
-
const deadendsPath = join(cwd, '.ai', 'memory', 'deadends.md');
|
|
150
|
-
|
|
151
|
-
if (!existsSync(deadendsPath)) {
|
|
152
|
-
return [];
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const content = readFileSync(deadendsPath, 'utf8');
|
|
156
|
-
const items = [];
|
|
157
|
-
|
|
158
|
-
// Match pattern: ## DE-XXX: Title
|
|
159
|
-
const deadendRegex = /^## (DE-\d+): (.+)$/gm;
|
|
160
|
-
let match;
|
|
161
|
-
|
|
162
|
-
while ((match = deadendRegex.exec(content)) !== null) {
|
|
163
|
-
const id = match[1];
|
|
164
|
-
const title = match[2];
|
|
165
|
-
|
|
166
|
-
// Extract date from the block
|
|
167
|
-
const blockStart = match.index;
|
|
168
|
-
const nextMatch = content.indexOf('\n## ', blockStart + 1);
|
|
169
|
-
const blockEnd = nextMatch === -1 ? content.length : nextMatch;
|
|
170
|
-
const block = content.substring(blockStart, blockEnd);
|
|
171
|
-
|
|
172
|
-
const dateMatch = block.match(/\*\*Date:\*\* ([\d-]+)/);
|
|
173
|
-
|
|
174
|
-
items.push({
|
|
175
|
-
id,
|
|
176
|
-
type: 'deadend',
|
|
177
|
-
title,
|
|
178
|
-
status: 'active',
|
|
179
|
-
relatedFiles: [],
|
|
180
|
-
relatedDecisions: [],
|
|
181
|
-
relatedTasks: [],
|
|
182
|
-
accessCount: 0,
|
|
183
|
-
appliedCount: 0,
|
|
184
|
-
appliedContexts: [],
|
|
185
|
-
createdAt: dateMatch ? dateMatch[1] : null,
|
|
186
|
-
lastAccessed: null
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
return items;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Sync knowledge registry with markdown files
|
|
195
|
-
* Discovers new items and preserves existing metadata
|
|
196
|
-
* @param {string} cwd - Working directory
|
|
197
|
-
* @returns {Object} Sync result
|
|
198
|
-
*/
|
|
199
|
-
export function syncKnowledge(cwd = process.cwd()) {
|
|
200
|
-
const registry = initKnowledgeRegistry(cwd);
|
|
201
|
-
const decisions = parseDecisions(cwd);
|
|
202
|
-
const deadends = parseDeadends(cwd);
|
|
203
|
-
|
|
204
|
-
const allItems = [...decisions, ...deadends];
|
|
205
|
-
const result = { added: [], updated: [], removed: [] };
|
|
206
|
-
|
|
207
|
-
// Track existing IDs
|
|
208
|
-
const existingIds = new Set(Object.keys(registry.items));
|
|
209
|
-
const currentIds = new Set(allItems.map(item => item.id));
|
|
210
|
-
|
|
211
|
-
// Add or update items
|
|
212
|
-
for (const item of allItems) {
|
|
213
|
-
if (registry.items[item.id]) {
|
|
214
|
-
// Preserve existing metadata, update title
|
|
215
|
-
registry.items[item.id].title = item.title;
|
|
216
|
-
if (item.createdAt && !registry.items[item.id].createdAt) {
|
|
217
|
-
registry.items[item.id].createdAt = item.createdAt;
|
|
218
|
-
}
|
|
219
|
-
result.updated.push(item.id);
|
|
220
|
-
} else {
|
|
221
|
-
// New item
|
|
222
|
-
registry.items[item.id] = {
|
|
223
|
-
...item,
|
|
224
|
-
lastAccessed: new Date().toISOString()
|
|
225
|
-
};
|
|
226
|
-
result.added.push(item.id);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// Mark removed items
|
|
231
|
-
for (const id of existingIds) {
|
|
232
|
-
if (!currentIds.has(id)) {
|
|
233
|
-
// Item was removed from markdown
|
|
234
|
-
result.removed.push(id);
|
|
235
|
-
// Don't delete, mark for GC
|
|
236
|
-
registry.items[id].status = 'garbage';
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
saveKnowledgeRegistry(registry, cwd);
|
|
241
|
-
return result;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Get a knowledge item
|
|
246
|
-
* @param {string} id - Item ID
|
|
247
|
-
* @param {string} cwd - Working directory
|
|
248
|
-
* @returns {KnowledgeItem|null}
|
|
249
|
-
*/
|
|
250
|
-
export function getKnowledgeItem(id, cwd = process.cwd()) {
|
|
251
|
-
const registry = getKnowledgeRegistry(cwd);
|
|
252
|
-
if (!registry) return null;
|
|
253
|
-
return registry.items[id] || null;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Update a knowledge item
|
|
258
|
-
* @param {string} id - Item ID
|
|
259
|
-
* @param {Object} updates - Fields to update
|
|
260
|
-
* @param {string} cwd - Working directory
|
|
261
|
-
* @returns {KnowledgeItem|null}
|
|
262
|
-
*/
|
|
263
|
-
export function updateKnowledgeItem(id, updates, cwd = process.cwd()) {
|
|
264
|
-
const registry = getKnowledgeRegistry(cwd);
|
|
265
|
-
if (!registry || !registry.items[id]) return null;
|
|
266
|
-
|
|
267
|
-
registry.items[id] = {
|
|
268
|
-
...registry.items[id],
|
|
269
|
-
...updates
|
|
270
|
-
};
|
|
271
|
-
|
|
272
|
-
saveKnowledgeRegistry(registry, cwd);
|
|
273
|
-
return registry.items[id];
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* Record an access to a knowledge item
|
|
278
|
-
* @param {string} id - Item ID
|
|
279
|
-
* @param {string} cwd - Working directory
|
|
280
|
-
* @returns {KnowledgeItem|null}
|
|
281
|
-
*/
|
|
282
|
-
export function recordAccess(id, cwd = process.cwd()) {
|
|
283
|
-
const registry = getKnowledgeRegistry(cwd);
|
|
284
|
-
if (!registry || !registry.items[id]) return null;
|
|
285
|
-
|
|
286
|
-
registry.items[id].accessCount = (registry.items[id].accessCount || 0) + 1;
|
|
287
|
-
registry.items[id].lastAccessed = new Date().toISOString();
|
|
288
|
-
|
|
289
|
-
saveKnowledgeRegistry(registry, cwd);
|
|
290
|
-
return registry.items[id];
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* Record that a knowledge item was applied in a context
|
|
295
|
-
* @param {string} id - Item ID
|
|
296
|
-
* @param {string} context - Context description (e.g., file path, feature name)
|
|
297
|
-
* @param {string} cwd - Working directory
|
|
298
|
-
* @returns {KnowledgeItem|null}
|
|
299
|
-
*/
|
|
300
|
-
export function recordApplication(id, context, cwd = process.cwd()) {
|
|
301
|
-
const registry = getKnowledgeRegistry(cwd);
|
|
302
|
-
if (!registry || !registry.items[id]) return null;
|
|
303
|
-
|
|
304
|
-
const item = registry.items[id];
|
|
305
|
-
item.appliedCount = (item.appliedCount || 0) + 1;
|
|
306
|
-
item.appliedContexts = item.appliedContexts || [];
|
|
307
|
-
|
|
308
|
-
if (!item.appliedContexts.includes(context)) {
|
|
309
|
-
item.appliedContexts.push(context);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
item.lastAccessed = new Date().toISOString();
|
|
313
|
-
|
|
314
|
-
// Check for promotion candidate
|
|
315
|
-
if (item.type === 'decision' &&
|
|
316
|
-
item.appliedCount >= 3 &&
|
|
317
|
-
item.appliedContexts.length >= 2 &&
|
|
318
|
-
item.status === 'active') {
|
|
319
|
-
item.status = 'candidate_pattern';
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
saveKnowledgeRegistry(registry, cwd);
|
|
323
|
-
return item;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
/**
|
|
327
|
-
* Get all knowledge items
|
|
328
|
-
* @param {Object} options - Filter options
|
|
329
|
-
* @param {string} options.type - Filter by type
|
|
330
|
-
* @param {string} options.status - Filter by status
|
|
331
|
-
* @param {string} cwd - Working directory
|
|
332
|
-
* @returns {KnowledgeItem[]}
|
|
333
|
-
*/
|
|
334
|
-
export function getAllKnowledge(options = {}, cwd = process.cwd()) {
|
|
335
|
-
const registry = getKnowledgeRegistry(cwd);
|
|
336
|
-
if (!registry) return [];
|
|
337
|
-
|
|
338
|
-
let items = Object.values(registry.items);
|
|
339
|
-
|
|
340
|
-
if (options.type) {
|
|
341
|
-
items = items.filter(item => item.type === options.type);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
if (options.status) {
|
|
345
|
-
items = items.filter(item => item.status === options.status);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
return items;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
/**
|
|
352
|
-
* Get knowledge statistics
|
|
353
|
-
* @param {string} cwd - Working directory
|
|
354
|
-
* @returns {Object}
|
|
355
|
-
*/
|
|
356
|
-
export function getKnowledgeStats(cwd = process.cwd()) {
|
|
357
|
-
const registry = getKnowledgeRegistry(cwd);
|
|
358
|
-
if (!registry) {
|
|
359
|
-
return {
|
|
360
|
-
total: 0,
|
|
361
|
-
decisions: 0,
|
|
362
|
-
deadends: 0,
|
|
363
|
-
active: 0,
|
|
364
|
-
candidatePatterns: 0,
|
|
365
|
-
archived: 0,
|
|
366
|
-
garbage: 0,
|
|
367
|
-
lastSync: null,
|
|
368
|
-
lastGC: null
|
|
369
|
-
};
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
const items = Object.values(registry.items);
|
|
373
|
-
|
|
374
|
-
return {
|
|
375
|
-
total: items.length,
|
|
376
|
-
decisions: items.filter(i => i.type === 'decision').length,
|
|
377
|
-
deadends: items.filter(i => i.type === 'deadend').length,
|
|
378
|
-
active: items.filter(i => i.status === 'active').length,
|
|
379
|
-
candidatePatterns: items.filter(i => i.status === 'candidate_pattern').length,
|
|
380
|
-
archived: items.filter(i => i.status === 'archived').length,
|
|
381
|
-
garbage: items.filter(i => i.status === 'garbage').length,
|
|
382
|
-
lastSync: registry.lastSync,
|
|
383
|
-
lastGC: registry.lastGC
|
|
384
|
-
};
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
/**
|
|
388
|
-
* Change status of a knowledge item
|
|
389
|
-
* @param {string} id - Item ID
|
|
390
|
-
* @param {KnowledgeStatus} newStatus - New status
|
|
391
|
-
* @param {string} cwd - Working directory
|
|
392
|
-
* @returns {KnowledgeItem|null}
|
|
393
|
-
*/
|
|
394
|
-
export function changeStatus(id, newStatus, cwd = process.cwd()) {
|
|
395
|
-
const validStatuses = ['active', 'candidate_pattern', 'archived', 'garbage'];
|
|
396
|
-
if (!validStatuses.includes(newStatus)) {
|
|
397
|
-
throw new Error(`Invalid status: ${newStatus}. Valid: ${validStatuses.join(', ')}`);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
return updateKnowledgeItem(id, { status: newStatus }, cwd);
|
|
401
|
-
}
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Knowledge Item Status
|
|
6
|
+
* @typedef {'active' | 'candidate_pattern' | 'archived' | 'garbage'} KnowledgeStatus
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Knowledge Item
|
|
11
|
+
* @typedef {Object} KnowledgeItem
|
|
12
|
+
* @property {string} id - Item ID (DEC-001, DE-001)
|
|
13
|
+
* @property {string} type - 'decision' | 'deadend'
|
|
14
|
+
* @property {string} title - Item title
|
|
15
|
+
* @property {string} status - Current status
|
|
16
|
+
* @property {string[]} relatedFiles - Code files that reference this
|
|
17
|
+
* @property {string[]} relatedDecisions - Other decisions that reference
|
|
18
|
+
* @property {string[]} relatedTasks - Tasks that depend on this
|
|
19
|
+
* @property {number} accessCount - Times consulted
|
|
20
|
+
* @property {number} appliedCount - Times applied in code
|
|
21
|
+
* @property {string[]} appliedContexts - Different contexts applied
|
|
22
|
+
* @property {string} createdAt - ISO date string
|
|
23
|
+
* @property {string} lastAccessed - ISO date string
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get the path to knowledge.json registry
|
|
28
|
+
* @param {string} cwd - Working directory
|
|
29
|
+
* @returns {string}
|
|
30
|
+
*/
|
|
31
|
+
export function getKnowledgePath(cwd = process.cwd()) {
|
|
32
|
+
return join(cwd, '.ai', 'config', 'knowledge.json');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Initialize knowledge registry if it doesn't exist
|
|
37
|
+
* @param {string} cwd - Working directory
|
|
38
|
+
* @returns {Object} Knowledge registry
|
|
39
|
+
*/
|
|
40
|
+
export function initKnowledgeRegistry(cwd = process.cwd()) {
|
|
41
|
+
const knowledgePath = getKnowledgePath(cwd);
|
|
42
|
+
const configDir = join(cwd, '.ai', 'config');
|
|
43
|
+
|
|
44
|
+
if (!existsSync(configDir)) {
|
|
45
|
+
mkdirSync(configDir, { recursive: true });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!existsSync(knowledgePath)) {
|
|
49
|
+
const initial = {
|
|
50
|
+
version: '1.0.0',
|
|
51
|
+
items: {},
|
|
52
|
+
lastGC: null,
|
|
53
|
+
lastSync: new Date().toISOString()
|
|
54
|
+
};
|
|
55
|
+
writeFileSync(knowledgePath, JSON.stringify(initial, null, 2));
|
|
56
|
+
return initial;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return JSON.parse(readFileSync(knowledgePath, 'utf8'));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get knowledge registry
|
|
64
|
+
* @param {string} cwd - Working directory
|
|
65
|
+
* @returns {Object|null}
|
|
66
|
+
*/
|
|
67
|
+
export function getKnowledgeRegistry(cwd = process.cwd()) {
|
|
68
|
+
const knowledgePath = getKnowledgePath(cwd);
|
|
69
|
+
|
|
70
|
+
if (!existsSync(knowledgePath)) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
return JSON.parse(readFileSync(knowledgePath, 'utf8'));
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Save knowledge registry
|
|
83
|
+
* @param {Object} registry - Registry to save
|
|
84
|
+
* @param {string} cwd - Working directory
|
|
85
|
+
*/
|
|
86
|
+
export function saveKnowledgeRegistry(registry, cwd = process.cwd()) {
|
|
87
|
+
const knowledgePath = getKnowledgePath(cwd);
|
|
88
|
+
registry.lastSync = new Date().toISOString();
|
|
89
|
+
writeFileSync(knowledgePath, JSON.stringify(registry, null, 2));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Parse decisions.md to extract decision items
|
|
94
|
+
* @param {string} cwd - Working directory
|
|
95
|
+
* @returns {KnowledgeItem[]}
|
|
96
|
+
*/
|
|
97
|
+
export function parseDecisions(cwd = process.cwd()) {
|
|
98
|
+
const decisionsPath = join(cwd, '.ai', 'memory', 'decisions.md');
|
|
99
|
+
|
|
100
|
+
if (!existsSync(decisionsPath)) {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const content = readFileSync(decisionsPath, 'utf8');
|
|
105
|
+
const items = [];
|
|
106
|
+
|
|
107
|
+
// Match pattern: ## DEC-XXX: Title
|
|
108
|
+
const decisionRegex = /^## (DEC-\d+): (.+)$/gm;
|
|
109
|
+
let match;
|
|
110
|
+
|
|
111
|
+
while ((match = decisionRegex.exec(content)) !== null) {
|
|
112
|
+
const id = match[1];
|
|
113
|
+
const title = match[2];
|
|
114
|
+
|
|
115
|
+
// Extract status from the block
|
|
116
|
+
const blockStart = match.index;
|
|
117
|
+
const nextMatch = content.indexOf('\n## ', blockStart + 1);
|
|
118
|
+
const blockEnd = nextMatch === -1 ? content.length : nextMatch;
|
|
119
|
+
const block = content.substring(blockStart, blockEnd);
|
|
120
|
+
|
|
121
|
+
const statusMatch = block.match(/\*\*Status:\*\* (\w+)/);
|
|
122
|
+
const dateMatch = block.match(/\*\*Date:\*\* ([\d-]+)/);
|
|
123
|
+
|
|
124
|
+
items.push({
|
|
125
|
+
id,
|
|
126
|
+
type: 'decision',
|
|
127
|
+
title,
|
|
128
|
+
status: statusMatch ? statusMatch[1].toLowerCase() : 'active',
|
|
129
|
+
relatedFiles: [],
|
|
130
|
+
relatedDecisions: [],
|
|
131
|
+
relatedTasks: [],
|
|
132
|
+
accessCount: 0,
|
|
133
|
+
appliedCount: 0,
|
|
134
|
+
appliedContexts: [],
|
|
135
|
+
createdAt: dateMatch ? dateMatch[1] : null,
|
|
136
|
+
lastAccessed: null
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return items;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Parse deadends.md to extract dead-end items
|
|
145
|
+
* @param {string} cwd - Working directory
|
|
146
|
+
* @returns {KnowledgeItem[]}
|
|
147
|
+
*/
|
|
148
|
+
export function parseDeadends(cwd = process.cwd()) {
|
|
149
|
+
const deadendsPath = join(cwd, '.ai', 'memory', 'deadends.md');
|
|
150
|
+
|
|
151
|
+
if (!existsSync(deadendsPath)) {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const content = readFileSync(deadendsPath, 'utf8');
|
|
156
|
+
const items = [];
|
|
157
|
+
|
|
158
|
+
// Match pattern: ## DE-XXX: Title
|
|
159
|
+
const deadendRegex = /^## (DE-\d+): (.+)$/gm;
|
|
160
|
+
let match;
|
|
161
|
+
|
|
162
|
+
while ((match = deadendRegex.exec(content)) !== null) {
|
|
163
|
+
const id = match[1];
|
|
164
|
+
const title = match[2];
|
|
165
|
+
|
|
166
|
+
// Extract date from the block
|
|
167
|
+
const blockStart = match.index;
|
|
168
|
+
const nextMatch = content.indexOf('\n## ', blockStart + 1);
|
|
169
|
+
const blockEnd = nextMatch === -1 ? content.length : nextMatch;
|
|
170
|
+
const block = content.substring(blockStart, blockEnd);
|
|
171
|
+
|
|
172
|
+
const dateMatch = block.match(/\*\*Date:\*\* ([\d-]+)/);
|
|
173
|
+
|
|
174
|
+
items.push({
|
|
175
|
+
id,
|
|
176
|
+
type: 'deadend',
|
|
177
|
+
title,
|
|
178
|
+
status: 'active',
|
|
179
|
+
relatedFiles: [],
|
|
180
|
+
relatedDecisions: [],
|
|
181
|
+
relatedTasks: [],
|
|
182
|
+
accessCount: 0,
|
|
183
|
+
appliedCount: 0,
|
|
184
|
+
appliedContexts: [],
|
|
185
|
+
createdAt: dateMatch ? dateMatch[1] : null,
|
|
186
|
+
lastAccessed: null
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return items;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Sync knowledge registry with markdown files
|
|
195
|
+
* Discovers new items and preserves existing metadata
|
|
196
|
+
* @param {string} cwd - Working directory
|
|
197
|
+
* @returns {Object} Sync result
|
|
198
|
+
*/
|
|
199
|
+
export function syncKnowledge(cwd = process.cwd()) {
|
|
200
|
+
const registry = initKnowledgeRegistry(cwd);
|
|
201
|
+
const decisions = parseDecisions(cwd);
|
|
202
|
+
const deadends = parseDeadends(cwd);
|
|
203
|
+
|
|
204
|
+
const allItems = [...decisions, ...deadends];
|
|
205
|
+
const result = { added: [], updated: [], removed: [] };
|
|
206
|
+
|
|
207
|
+
// Track existing IDs
|
|
208
|
+
const existingIds = new Set(Object.keys(registry.items));
|
|
209
|
+
const currentIds = new Set(allItems.map(item => item.id));
|
|
210
|
+
|
|
211
|
+
// Add or update items
|
|
212
|
+
for (const item of allItems) {
|
|
213
|
+
if (registry.items[item.id]) {
|
|
214
|
+
// Preserve existing metadata, update title
|
|
215
|
+
registry.items[item.id].title = item.title;
|
|
216
|
+
if (item.createdAt && !registry.items[item.id].createdAt) {
|
|
217
|
+
registry.items[item.id].createdAt = item.createdAt;
|
|
218
|
+
}
|
|
219
|
+
result.updated.push(item.id);
|
|
220
|
+
} else {
|
|
221
|
+
// New item
|
|
222
|
+
registry.items[item.id] = {
|
|
223
|
+
...item,
|
|
224
|
+
lastAccessed: new Date().toISOString()
|
|
225
|
+
};
|
|
226
|
+
result.added.push(item.id);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Mark removed items
|
|
231
|
+
for (const id of existingIds) {
|
|
232
|
+
if (!currentIds.has(id)) {
|
|
233
|
+
// Item was removed from markdown
|
|
234
|
+
result.removed.push(id);
|
|
235
|
+
// Don't delete, mark for GC
|
|
236
|
+
registry.items[id].status = 'garbage';
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
saveKnowledgeRegistry(registry, cwd);
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Get a knowledge item
|
|
246
|
+
* @param {string} id - Item ID
|
|
247
|
+
* @param {string} cwd - Working directory
|
|
248
|
+
* @returns {KnowledgeItem|null}
|
|
249
|
+
*/
|
|
250
|
+
export function getKnowledgeItem(id, cwd = process.cwd()) {
|
|
251
|
+
const registry = getKnowledgeRegistry(cwd);
|
|
252
|
+
if (!registry) return null;
|
|
253
|
+
return registry.items[id] || null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Update a knowledge item
|
|
258
|
+
* @param {string} id - Item ID
|
|
259
|
+
* @param {Object} updates - Fields to update
|
|
260
|
+
* @param {string} cwd - Working directory
|
|
261
|
+
* @returns {KnowledgeItem|null}
|
|
262
|
+
*/
|
|
263
|
+
export function updateKnowledgeItem(id, updates, cwd = process.cwd()) {
|
|
264
|
+
const registry = getKnowledgeRegistry(cwd);
|
|
265
|
+
if (!registry || !registry.items[id]) return null;
|
|
266
|
+
|
|
267
|
+
registry.items[id] = {
|
|
268
|
+
...registry.items[id],
|
|
269
|
+
...updates
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
saveKnowledgeRegistry(registry, cwd);
|
|
273
|
+
return registry.items[id];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Record an access to a knowledge item
|
|
278
|
+
* @param {string} id - Item ID
|
|
279
|
+
* @param {string} cwd - Working directory
|
|
280
|
+
* @returns {KnowledgeItem|null}
|
|
281
|
+
*/
|
|
282
|
+
export function recordAccess(id, cwd = process.cwd()) {
|
|
283
|
+
const registry = getKnowledgeRegistry(cwd);
|
|
284
|
+
if (!registry || !registry.items[id]) return null;
|
|
285
|
+
|
|
286
|
+
registry.items[id].accessCount = (registry.items[id].accessCount || 0) + 1;
|
|
287
|
+
registry.items[id].lastAccessed = new Date().toISOString();
|
|
288
|
+
|
|
289
|
+
saveKnowledgeRegistry(registry, cwd);
|
|
290
|
+
return registry.items[id];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Record that a knowledge item was applied in a context
|
|
295
|
+
* @param {string} id - Item ID
|
|
296
|
+
* @param {string} context - Context description (e.g., file path, feature name)
|
|
297
|
+
* @param {string} cwd - Working directory
|
|
298
|
+
* @returns {KnowledgeItem|null}
|
|
299
|
+
*/
|
|
300
|
+
export function recordApplication(id, context, cwd = process.cwd()) {
|
|
301
|
+
const registry = getKnowledgeRegistry(cwd);
|
|
302
|
+
if (!registry || !registry.items[id]) return null;
|
|
303
|
+
|
|
304
|
+
const item = registry.items[id];
|
|
305
|
+
item.appliedCount = (item.appliedCount || 0) + 1;
|
|
306
|
+
item.appliedContexts = item.appliedContexts || [];
|
|
307
|
+
|
|
308
|
+
if (!item.appliedContexts.includes(context)) {
|
|
309
|
+
item.appliedContexts.push(context);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
item.lastAccessed = new Date().toISOString();
|
|
313
|
+
|
|
314
|
+
// Check for promotion candidate
|
|
315
|
+
if (item.type === 'decision' &&
|
|
316
|
+
item.appliedCount >= 3 &&
|
|
317
|
+
item.appliedContexts.length >= 2 &&
|
|
318
|
+
item.status === 'active') {
|
|
319
|
+
item.status = 'candidate_pattern';
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
saveKnowledgeRegistry(registry, cwd);
|
|
323
|
+
return item;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Get all knowledge items
|
|
328
|
+
* @param {Object} options - Filter options
|
|
329
|
+
* @param {string} options.type - Filter by type
|
|
330
|
+
* @param {string} options.status - Filter by status
|
|
331
|
+
* @param {string} cwd - Working directory
|
|
332
|
+
* @returns {KnowledgeItem[]}
|
|
333
|
+
*/
|
|
334
|
+
export function getAllKnowledge(options = {}, cwd = process.cwd()) {
|
|
335
|
+
const registry = getKnowledgeRegistry(cwd);
|
|
336
|
+
if (!registry) return [];
|
|
337
|
+
|
|
338
|
+
let items = Object.values(registry.items);
|
|
339
|
+
|
|
340
|
+
if (options.type) {
|
|
341
|
+
items = items.filter(item => item.type === options.type);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (options.status) {
|
|
345
|
+
items = items.filter(item => item.status === options.status);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return items;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Get knowledge statistics
|
|
353
|
+
* @param {string} cwd - Working directory
|
|
354
|
+
* @returns {Object}
|
|
355
|
+
*/
|
|
356
|
+
export function getKnowledgeStats(cwd = process.cwd()) {
|
|
357
|
+
const registry = getKnowledgeRegistry(cwd);
|
|
358
|
+
if (!registry) {
|
|
359
|
+
return {
|
|
360
|
+
total: 0,
|
|
361
|
+
decisions: 0,
|
|
362
|
+
deadends: 0,
|
|
363
|
+
active: 0,
|
|
364
|
+
candidatePatterns: 0,
|
|
365
|
+
archived: 0,
|
|
366
|
+
garbage: 0,
|
|
367
|
+
lastSync: null,
|
|
368
|
+
lastGC: null
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const items = Object.values(registry.items);
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
total: items.length,
|
|
376
|
+
decisions: items.filter(i => i.type === 'decision').length,
|
|
377
|
+
deadends: items.filter(i => i.type === 'deadend').length,
|
|
378
|
+
active: items.filter(i => i.status === 'active').length,
|
|
379
|
+
candidatePatterns: items.filter(i => i.status === 'candidate_pattern').length,
|
|
380
|
+
archived: items.filter(i => i.status === 'archived').length,
|
|
381
|
+
garbage: items.filter(i => i.status === 'garbage').length,
|
|
382
|
+
lastSync: registry.lastSync,
|
|
383
|
+
lastGC: registry.lastGC
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Change status of a knowledge item
|
|
389
|
+
* @param {string} id - Item ID
|
|
390
|
+
* @param {KnowledgeStatus} newStatus - New status
|
|
391
|
+
* @param {string} cwd - Working directory
|
|
392
|
+
* @returns {KnowledgeItem|null}
|
|
393
|
+
*/
|
|
394
|
+
export function changeStatus(id, newStatus, cwd = process.cwd()) {
|
|
395
|
+
const validStatuses = ['active', 'candidate_pattern', 'archived', 'garbage'];
|
|
396
|
+
if (!validStatuses.includes(newStatus)) {
|
|
397
|
+
throw new Error(`Invalid status: ${newStatus}. Valid: ${validStatuses.join(', ')}`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return updateKnowledgeItem(id, { status: newStatus }, cwd);
|
|
401
|
+
}
|