@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,308 +1,308 @@
1
- import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync } from 'fs';
2
- import { join } from 'path';
3
- import {
4
- getKnowledgeRegistry,
5
- saveKnowledgeRegistry,
6
- getAllKnowledge,
7
- changeStatus
8
- } from './lifecycle.js';
9
- import { findBrokenReferences } from './references.js';
10
-
11
- /**
12
- * GC Result
13
- * @typedef {Object} GCResult
14
- * @property {string[]} marked - Items marked for deletion
15
- * @property {string[]} archived - Items archived
16
- * @property {string[]} deleted - Items removed
17
- * @property {string[]} cleaned - Broken references cleaned
18
- */
19
-
20
- /**
21
- * Identify items that should be garbage collected
22
- * Criteria:
23
- * - No related files exist
24
- * - No references from other decisions
25
- * - Status is 'garbage' or orphaned
26
- * @param {string} cwd - Working directory
27
- * @returns {Object[]} Items to collect
28
- */
29
- export function identifyGarbage(cwd = process.cwd()) {
30
- const registry = getKnowledgeRegistry(cwd);
31
- if (!registry) {
32
- return [];
33
- }
34
-
35
- const garbage = [];
36
-
37
- for (const [id, item] of Object.entries(registry.items)) {
38
- // Already marked as garbage
39
- if (item.status === 'garbage') {
40
- garbage.push({
41
- id,
42
- title: item.title,
43
- reason: 'marked_garbage',
44
- relatedFiles: item.relatedFiles || [],
45
- relatedDecisions: item.relatedDecisions || []
46
- });
47
- continue;
48
- }
49
-
50
- // Check if orphaned (no references)
51
- const hasFileRefs = (item.relatedFiles || []).length > 0;
52
- const hasDecisionRefs = (item.relatedDecisions || []).length > 0;
53
- const hasTaskRefs = (item.relatedTasks || []).length > 0;
54
-
55
- if (!hasFileRefs && !hasDecisionRefs && !hasTaskRefs) {
56
- // Check if it's been accessed recently (last 30 days)
57
- const lastAccessed = item.lastAccessed ? new Date(item.lastAccessed) : null;
58
- const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
59
-
60
- if (!lastAccessed || lastAccessed < thirtyDaysAgo) {
61
- garbage.push({
62
- id,
63
- title: item.title,
64
- reason: 'orphaned',
65
- lastAccessed: item.lastAccessed,
66
- relatedFiles: [],
67
- relatedDecisions: []
68
- });
69
- }
70
- }
71
- }
72
-
73
- return garbage;
74
- }
75
-
76
- /**
77
- * Run garbage collection in dry-run mode
78
- * @param {string} cwd - Working directory
79
- * @returns {GCResult}
80
- */
81
- export function dryRunGC(cwd = process.cwd()) {
82
- const garbage = identifyGarbage(cwd);
83
- const broken = findBrokenReferences(cwd);
84
-
85
- return {
86
- marked: garbage.map(g => g.id),
87
- archived: [],
88
- deleted: [],
89
- cleaned: broken.map(b => b.id),
90
- details: {
91
- garbage,
92
- broken
93
- }
94
- };
95
- }
96
-
97
- /**
98
- * Execute garbage collection
99
- * @param {Object} options - Options
100
- * @param {boolean} options.archive - Archive instead of delete
101
- * @param {boolean} options.cleanBroken - Clean broken references
102
- * @param {string} cwd - Working directory
103
- * @returns {GCResult}
104
- */
105
- export function executeGC(options = {}, cwd = process.cwd()) {
106
- const { archive = true, cleanBroken = true } = options;
107
- const registry = getKnowledgeRegistry(cwd);
108
-
109
- if (!registry) {
110
- return { marked: [], archived: [], deleted: [], cleaned: [] };
111
- }
112
-
113
- const result = {
114
- marked: [],
115
- archived: [],
116
- deleted: [],
117
- cleaned: []
118
- };
119
-
120
- // Identify garbage
121
- const garbage = identifyGarbage(cwd);
122
-
123
- for (const item of garbage) {
124
- if (archive) {
125
- // Archive to memory/archive
126
- archiveKnowledgeItem(item.id, cwd);
127
- // Update local registry reference to reflect archived status
128
- registry.items[item.id].status = 'archived';
129
- result.archived.push(item.id);
130
- } else {
131
- // Delete from registry
132
- delete registry.items[item.id];
133
- result.deleted.push(item.id);
134
- }
135
- result.marked.push(item.id);
136
- }
137
-
138
- // Clean broken references if requested
139
- if (cleanBroken) {
140
- const broken = findBrokenReferences(cwd);
141
- for (const { id, brokenFiles, brokenDecisions } of broken) {
142
- const item = registry.items[id];
143
- if (item) {
144
- // Remove broken file references
145
- item.relatedFiles = (item.relatedFiles || []).filter(
146
- f => !brokenFiles.includes(f)
147
- );
148
- // Remove broken decision references
149
- item.relatedDecisions = (item.relatedDecisions || []).filter(
150
- d => !brokenDecisions.includes(d)
151
- );
152
- result.cleaned.push(id);
153
- }
154
- }
155
- }
156
-
157
- // Update last GC timestamp
158
- registry.lastGC = new Date().toISOString();
159
- saveKnowledgeRegistry(registry, cwd);
160
-
161
- return result;
162
- }
163
-
164
- /**
165
- * Archive a knowledge item to memory/archive
166
- * @param {string} id - Item ID
167
- * @param {string} cwd - Working directory
168
- */
169
- function archiveKnowledgeItem(id, cwd = process.cwd()) {
170
- const registry = getKnowledgeRegistry(cwd);
171
- if (!registry || !registry.items[id]) return;
172
-
173
- const item = registry.items[id];
174
- const archiveDir = join(cwd, '.ai', 'memory', 'archive');
175
-
176
- // Ensure archive directory exists
177
- if (!existsSync(archiveDir)) {
178
- mkdirSync(archiveDir, { recursive: true });
179
- }
180
-
181
- // Append to archive file
182
- const archiveFile = join(archiveDir, 'archived-knowledge.md');
183
- const archiveEntry = formatArchiveEntry(item);
184
-
185
- appendFileSync(archiveFile, archiveEntry);
186
-
187
- // Update status in registry
188
- registry.items[id].status = 'archived';
189
- registry.items[id].archivedAt = new Date().toISOString();
190
- saveKnowledgeRegistry(registry, cwd);
191
- }
192
-
193
- /**
194
- * Format a knowledge item for archive
195
- * @param {Object} item - Knowledge item
196
- * @returns {string}
197
- */
198
- function formatArchiveEntry(item) {
199
- const divider = '\n---\n\n';
200
- const header = `## ${item.id}: ${item.title} [ARCHIVED]\n`;
201
- const metadata = [
202
- `**Type:** ${item.type}`,
203
- `**Created:** ${item.createdAt || 'Unknown'}`,
204
- `**Archived:** ${new Date().toISOString().split('T')[0]}`,
205
- `**Access Count:** ${item.accessCount || 0}`,
206
- `**Applied Count:** ${item.appliedCount || 0}`,
207
- `**Last Accessed:** ${item.lastAccessed || 'Never'}`
208
- ].join('\n');
209
-
210
- const refs = [];
211
- if (item.relatedFiles?.length) {
212
- refs.push(`**Related Files:** ${item.relatedFiles.join(', ')}`);
213
- }
214
- if (item.relatedDecisions?.length) {
215
- refs.push(`**Related Decisions:** ${item.relatedDecisions.join(', ')}`);
216
- }
217
- if (item.appliedContexts?.length) {
218
- refs.push(`**Applied Contexts:** ${item.appliedContexts.join(', ')}`);
219
- }
220
-
221
- return header + metadata + '\n' + refs.join('\n') + divider;
222
- }
223
-
224
- /**
225
- * Get items that are candidates for archival (inactive but not orphaned)
226
- * @param {number} inactiveDays - Days of inactivity
227
- * @param {string} cwd - Working directory
228
- * @returns {Object[]}
229
- */
230
- export function getArchivalCandidates(inactiveDays = 60, cwd = process.cwd()) {
231
- const registry = getKnowledgeRegistry(cwd);
232
- if (!registry) return [];
233
-
234
- const candidates = [];
235
- const cutoff = new Date(Date.now() - inactiveDays * 24 * 60 * 60 * 1000);
236
-
237
- for (const [id, item] of Object.entries(registry.items)) {
238
- if (item.status !== 'active') continue;
239
-
240
- const lastAccessed = item.lastAccessed ? new Date(item.lastAccessed) : null;
241
-
242
- // Has references but hasn't been accessed
243
- const hasRefs = (item.relatedFiles || []).length > 0 ||
244
- (item.relatedDecisions || []).length > 0;
245
-
246
- if (hasRefs && (!lastAccessed || lastAccessed < cutoff)) {
247
- candidates.push({
248
- id,
249
- title: item.title,
250
- lastAccessed: item.lastAccessed,
251
- daysSinceAccess: lastAccessed
252
- ? Math.floor((Date.now() - lastAccessed) / (24 * 60 * 60 * 1000))
253
- : 'never',
254
- refCount: (item.relatedFiles || []).length + (item.relatedDecisions || []).length
255
- });
256
- }
257
- }
258
-
259
- return candidates.sort((a, b) => {
260
- const aDate = a.lastAccessed ? new Date(a.lastAccessed) : new Date(0);
261
- const bDate = b.lastAccessed ? new Date(b.lastAccessed) : new Date(0);
262
- return aDate - bDate;
263
- });
264
- }
265
-
266
- /**
267
- * Restore an archived item back to active
268
- * @param {string} id - Item ID
269
- * @param {string} cwd - Working directory
270
- * @returns {Object|null}
271
- */
272
- export function restoreFromArchive(id, cwd = process.cwd()) {
273
- const registry = getKnowledgeRegistry(cwd);
274
- if (!registry || !registry.items[id]) return null;
275
-
276
- const item = registry.items[id];
277
- if (item.status !== 'archived') {
278
- return { error: `Item ${id} is not archived (status: ${item.status})` };
279
- }
280
-
281
- item.status = 'active';
282
- item.restoredAt = new Date().toISOString();
283
- delete item.archivedAt;
284
-
285
- saveKnowledgeRegistry(registry, cwd);
286
- return item;
287
- }
288
-
289
- /**
290
- * Get GC statistics
291
- * @param {string} cwd - Working directory
292
- * @returns {Object}
293
- */
294
- export function getGCStats(cwd = process.cwd()) {
295
- const registry = getKnowledgeRegistry(cwd);
296
- const garbage = identifyGarbage(cwd);
297
- const archivalCandidates = getArchivalCandidates(60, cwd);
298
- const broken = findBrokenReferences(cwd);
299
-
300
- return {
301
- lastGC: registry?.lastGC || 'Never',
302
- garbageCount: garbage.length,
303
- archivalCandidates: archivalCandidates.length,
304
- brokenReferences: broken.length,
305
- garbage: garbage.map(g => ({ id: g.id, reason: g.reason })),
306
- candidates: archivalCandidates.slice(0, 5)
307
- };
308
- }
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import {
4
+ getKnowledgeRegistry,
5
+ saveKnowledgeRegistry,
6
+ getAllKnowledge,
7
+ changeStatus
8
+ } from './lifecycle.js';
9
+ import { findBrokenReferences } from './references.js';
10
+
11
+ /**
12
+ * GC Result
13
+ * @typedef {Object} GCResult
14
+ * @property {string[]} marked - Items marked for deletion
15
+ * @property {string[]} archived - Items archived
16
+ * @property {string[]} deleted - Items removed
17
+ * @property {string[]} cleaned - Broken references cleaned
18
+ */
19
+
20
+ /**
21
+ * Identify items that should be garbage collected
22
+ * Criteria:
23
+ * - No related files exist
24
+ * - No references from other decisions
25
+ * - Status is 'garbage' or orphaned
26
+ * @param {string} cwd - Working directory
27
+ * @returns {Object[]} Items to collect
28
+ */
29
+ export function identifyGarbage(cwd = process.cwd()) {
30
+ const registry = getKnowledgeRegistry(cwd);
31
+ if (!registry) {
32
+ return [];
33
+ }
34
+
35
+ const garbage = [];
36
+
37
+ for (const [id, item] of Object.entries(registry.items)) {
38
+ // Already marked as garbage
39
+ if (item.status === 'garbage') {
40
+ garbage.push({
41
+ id,
42
+ title: item.title,
43
+ reason: 'marked_garbage',
44
+ relatedFiles: item.relatedFiles || [],
45
+ relatedDecisions: item.relatedDecisions || []
46
+ });
47
+ continue;
48
+ }
49
+
50
+ // Check if orphaned (no references)
51
+ const hasFileRefs = (item.relatedFiles || []).length > 0;
52
+ const hasDecisionRefs = (item.relatedDecisions || []).length > 0;
53
+ const hasTaskRefs = (item.relatedTasks || []).length > 0;
54
+
55
+ if (!hasFileRefs && !hasDecisionRefs && !hasTaskRefs) {
56
+ // Check if it's been accessed recently (last 30 days)
57
+ const lastAccessed = item.lastAccessed ? new Date(item.lastAccessed) : null;
58
+ const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
59
+
60
+ if (!lastAccessed || lastAccessed < thirtyDaysAgo) {
61
+ garbage.push({
62
+ id,
63
+ title: item.title,
64
+ reason: 'orphaned',
65
+ lastAccessed: item.lastAccessed,
66
+ relatedFiles: [],
67
+ relatedDecisions: []
68
+ });
69
+ }
70
+ }
71
+ }
72
+
73
+ return garbage;
74
+ }
75
+
76
+ /**
77
+ * Run garbage collection in dry-run mode
78
+ * @param {string} cwd - Working directory
79
+ * @returns {GCResult}
80
+ */
81
+ export function dryRunGC(cwd = process.cwd()) {
82
+ const garbage = identifyGarbage(cwd);
83
+ const broken = findBrokenReferences(cwd);
84
+
85
+ return {
86
+ marked: garbage.map(g => g.id),
87
+ archived: [],
88
+ deleted: [],
89
+ cleaned: broken.map(b => b.id),
90
+ details: {
91
+ garbage,
92
+ broken
93
+ }
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Execute garbage collection
99
+ * @param {Object} options - Options
100
+ * @param {boolean} options.archive - Archive instead of delete
101
+ * @param {boolean} options.cleanBroken - Clean broken references
102
+ * @param {string} cwd - Working directory
103
+ * @returns {GCResult}
104
+ */
105
+ export function executeGC(options = {}, cwd = process.cwd()) {
106
+ const { archive = true, cleanBroken = true } = options;
107
+ const registry = getKnowledgeRegistry(cwd);
108
+
109
+ if (!registry) {
110
+ return { marked: [], archived: [], deleted: [], cleaned: [] };
111
+ }
112
+
113
+ const result = {
114
+ marked: [],
115
+ archived: [],
116
+ deleted: [],
117
+ cleaned: []
118
+ };
119
+
120
+ // Identify garbage
121
+ const garbage = identifyGarbage(cwd);
122
+
123
+ for (const item of garbage) {
124
+ if (archive) {
125
+ // Archive to memory/archive
126
+ archiveKnowledgeItem(item.id, cwd);
127
+ // Update local registry reference to reflect archived status
128
+ registry.items[item.id].status = 'archived';
129
+ result.archived.push(item.id);
130
+ } else {
131
+ // Delete from registry
132
+ delete registry.items[item.id];
133
+ result.deleted.push(item.id);
134
+ }
135
+ result.marked.push(item.id);
136
+ }
137
+
138
+ // Clean broken references if requested
139
+ if (cleanBroken) {
140
+ const broken = findBrokenReferences(cwd);
141
+ for (const { id, brokenFiles, brokenDecisions } of broken) {
142
+ const item = registry.items[id];
143
+ if (item) {
144
+ // Remove broken file references
145
+ item.relatedFiles = (item.relatedFiles || []).filter(
146
+ f => !brokenFiles.includes(f)
147
+ );
148
+ // Remove broken decision references
149
+ item.relatedDecisions = (item.relatedDecisions || []).filter(
150
+ d => !brokenDecisions.includes(d)
151
+ );
152
+ result.cleaned.push(id);
153
+ }
154
+ }
155
+ }
156
+
157
+ // Update last GC timestamp
158
+ registry.lastGC = new Date().toISOString();
159
+ saveKnowledgeRegistry(registry, cwd);
160
+
161
+ return result;
162
+ }
163
+
164
+ /**
165
+ * Archive a knowledge item to memory/archive
166
+ * @param {string} id - Item ID
167
+ * @param {string} cwd - Working directory
168
+ */
169
+ function archiveKnowledgeItem(id, cwd = process.cwd()) {
170
+ const registry = getKnowledgeRegistry(cwd);
171
+ if (!registry || !registry.items[id]) return;
172
+
173
+ const item = registry.items[id];
174
+ const archiveDir = join(cwd, '.ai', 'memory', 'archive');
175
+
176
+ // Ensure archive directory exists
177
+ if (!existsSync(archiveDir)) {
178
+ mkdirSync(archiveDir, { recursive: true });
179
+ }
180
+
181
+ // Append to archive file
182
+ const archiveFile = join(archiveDir, 'archived-knowledge.md');
183
+ const archiveEntry = formatArchiveEntry(item);
184
+
185
+ appendFileSync(archiveFile, archiveEntry);
186
+
187
+ // Update status in registry
188
+ registry.items[id].status = 'archived';
189
+ registry.items[id].archivedAt = new Date().toISOString();
190
+ saveKnowledgeRegistry(registry, cwd);
191
+ }
192
+
193
+ /**
194
+ * Format a knowledge item for archive
195
+ * @param {Object} item - Knowledge item
196
+ * @returns {string}
197
+ */
198
+ function formatArchiveEntry(item) {
199
+ const divider = '\n---\n\n';
200
+ const header = `## ${item.id}: ${item.title} [ARCHIVED]\n`;
201
+ const metadata = [
202
+ `**Type:** ${item.type}`,
203
+ `**Created:** ${item.createdAt || 'Unknown'}`,
204
+ `**Archived:** ${new Date().toISOString().split('T')[0]}`,
205
+ `**Access Count:** ${item.accessCount || 0}`,
206
+ `**Applied Count:** ${item.appliedCount || 0}`,
207
+ `**Last Accessed:** ${item.lastAccessed || 'Never'}`
208
+ ].join('\n');
209
+
210
+ const refs = [];
211
+ if (item.relatedFiles?.length) {
212
+ refs.push(`**Related Files:** ${item.relatedFiles.join(', ')}`);
213
+ }
214
+ if (item.relatedDecisions?.length) {
215
+ refs.push(`**Related Decisions:** ${item.relatedDecisions.join(', ')}`);
216
+ }
217
+ if (item.appliedContexts?.length) {
218
+ refs.push(`**Applied Contexts:** ${item.appliedContexts.join(', ')}`);
219
+ }
220
+
221
+ return header + metadata + '\n' + refs.join('\n') + divider;
222
+ }
223
+
224
+ /**
225
+ * Get items that are candidates for archival (inactive but not orphaned)
226
+ * @param {number} inactiveDays - Days of inactivity
227
+ * @param {string} cwd - Working directory
228
+ * @returns {Object[]}
229
+ */
230
+ export function getArchivalCandidates(inactiveDays = 60, cwd = process.cwd()) {
231
+ const registry = getKnowledgeRegistry(cwd);
232
+ if (!registry) return [];
233
+
234
+ const candidates = [];
235
+ const cutoff = new Date(Date.now() - inactiveDays * 24 * 60 * 60 * 1000);
236
+
237
+ for (const [id, item] of Object.entries(registry.items)) {
238
+ if (item.status !== 'active') continue;
239
+
240
+ const lastAccessed = item.lastAccessed ? new Date(item.lastAccessed) : null;
241
+
242
+ // Has references but hasn't been accessed
243
+ const hasRefs = (item.relatedFiles || []).length > 0 ||
244
+ (item.relatedDecisions || []).length > 0;
245
+
246
+ if (hasRefs && (!lastAccessed || lastAccessed < cutoff)) {
247
+ candidates.push({
248
+ id,
249
+ title: item.title,
250
+ lastAccessed: item.lastAccessed,
251
+ daysSinceAccess: lastAccessed
252
+ ? Math.floor((Date.now() - lastAccessed) / (24 * 60 * 60 * 1000))
253
+ : 'never',
254
+ refCount: (item.relatedFiles || []).length + (item.relatedDecisions || []).length
255
+ });
256
+ }
257
+ }
258
+
259
+ return candidates.sort((a, b) => {
260
+ const aDate = a.lastAccessed ? new Date(a.lastAccessed) : new Date(0);
261
+ const bDate = b.lastAccessed ? new Date(b.lastAccessed) : new Date(0);
262
+ return aDate - bDate;
263
+ });
264
+ }
265
+
266
+ /**
267
+ * Restore an archived item back to active
268
+ * @param {string} id - Item ID
269
+ * @param {string} cwd - Working directory
270
+ * @returns {Object|null}
271
+ */
272
+ export function restoreFromArchive(id, cwd = process.cwd()) {
273
+ const registry = getKnowledgeRegistry(cwd);
274
+ if (!registry || !registry.items[id]) return null;
275
+
276
+ const item = registry.items[id];
277
+ if (item.status !== 'archived') {
278
+ return { error: `Item ${id} is not archived (status: ${item.status})` };
279
+ }
280
+
281
+ item.status = 'active';
282
+ item.restoredAt = new Date().toISOString();
283
+ delete item.archivedAt;
284
+
285
+ saveKnowledgeRegistry(registry, cwd);
286
+ return item;
287
+ }
288
+
289
+ /**
290
+ * Get GC statistics
291
+ * @param {string} cwd - Working directory
292
+ * @returns {Object}
293
+ */
294
+ export function getGCStats(cwd = process.cwd()) {
295
+ const registry = getKnowledgeRegistry(cwd);
296
+ const garbage = identifyGarbage(cwd);
297
+ const archivalCandidates = getArchivalCandidates(60, cwd);
298
+ const broken = findBrokenReferences(cwd);
299
+
300
+ return {
301
+ lastGC: registry?.lastGC || 'Never',
302
+ garbageCount: garbage.length,
303
+ archivalCandidates: archivalCandidates.length,
304
+ brokenReferences: broken.length,
305
+ garbage: garbage.map(g => ({ id: g.id, reason: g.reason })),
306
+ candidates: archivalCandidates.slice(0, 5)
307
+ };
308
+ }