prjct-cli 0.44.1 → 0.45.3

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 (207) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/bin/prjct.ts +131 -10
  3. package/core/__tests__/agentic/memory-system.test.ts +39 -26
  4. package/core/__tests__/agentic/plan-mode.test.ts +64 -46
  5. package/core/__tests__/agentic/prompt-builder.test.ts +14 -14
  6. package/core/__tests__/services/project-index.test.ts +353 -0
  7. package/core/__tests__/types/fs.test.ts +3 -3
  8. package/core/__tests__/utils/date-helper.test.ts +10 -10
  9. package/core/__tests__/utils/output.test.ts +9 -6
  10. package/core/__tests__/utils/project-commands.test.ts +5 -6
  11. package/core/agentic/agent-router.ts +9 -10
  12. package/core/agentic/chain-of-thought.ts +16 -4
  13. package/core/agentic/command-executor.ts +66 -40
  14. package/core/agentic/context-builder.ts +8 -5
  15. package/core/agentic/ground-truth.ts +15 -9
  16. package/core/agentic/index.ts +145 -152
  17. package/core/agentic/loop-detector.ts +40 -11
  18. package/core/agentic/memory-system.ts +98 -35
  19. package/core/agentic/orchestrator-executor.ts +135 -71
  20. package/core/agentic/plan-mode.ts +46 -16
  21. package/core/agentic/prompt-builder.ts +108 -42
  22. package/core/agentic/services.ts +10 -9
  23. package/core/agentic/skill-loader.ts +9 -15
  24. package/core/agentic/smart-context.ts +129 -79
  25. package/core/agentic/template-executor.ts +13 -12
  26. package/core/agentic/template-loader.ts +7 -4
  27. package/core/agentic/tool-registry.ts +16 -13
  28. package/core/agents/index.ts +1 -1
  29. package/core/agents/performance.ts +10 -27
  30. package/core/ai-tools/formatters.ts +8 -6
  31. package/core/ai-tools/generator.ts +4 -4
  32. package/core/ai-tools/index.ts +1 -1
  33. package/core/ai-tools/registry.ts +21 -11
  34. package/core/bus/bus.ts +23 -16
  35. package/core/bus/index.ts +2 -2
  36. package/core/cli/linear.ts +3 -5
  37. package/core/cli/start.ts +28 -25
  38. package/core/commands/analysis.ts +287 -29
  39. package/core/commands/analytics.ts +52 -44
  40. package/core/commands/base.ts +15 -13
  41. package/core/commands/cleanup.ts +6 -13
  42. package/core/commands/command-data.ts +49 -8
  43. package/core/commands/commands.ts +60 -23
  44. package/core/commands/context.ts +4 -4
  45. package/core/commands/design.ts +3 -10
  46. package/core/commands/index.ts +5 -8
  47. package/core/commands/maintenance.ts +7 -4
  48. package/core/commands/planning.ts +179 -56
  49. package/core/commands/register.ts +14 -9
  50. package/core/commands/registry.ts +15 -14
  51. package/core/commands/setup.ts +26 -14
  52. package/core/commands/shipping.ts +11 -16
  53. package/core/commands/snapshots.ts +16 -32
  54. package/core/commands/uninstall.ts +541 -0
  55. package/core/commands/workflow.ts +24 -28
  56. package/core/constants/index.ts +10 -22
  57. package/core/context/generator.ts +82 -33
  58. package/core/context-tools/files-tool.ts +583 -0
  59. package/core/context-tools/imports-tool.ts +403 -0
  60. package/core/context-tools/index.ts +433 -0
  61. package/core/context-tools/recent-tool.ts +307 -0
  62. package/core/context-tools/signatures-tool.ts +501 -0
  63. package/core/context-tools/summary-tool.ts +307 -0
  64. package/core/context-tools/token-counter.ts +284 -0
  65. package/core/context-tools/types.ts +253 -0
  66. package/core/domain/agent-generator.ts +7 -5
  67. package/core/domain/agent-loader.ts +2 -2
  68. package/core/domain/analyzer.ts +19 -16
  69. package/core/domain/architecture-generator.ts +6 -3
  70. package/core/domain/context-estimator.ts +3 -4
  71. package/core/domain/snapshot-manager.ts +25 -22
  72. package/core/domain/task-stack.ts +24 -14
  73. package/core/errors.ts +1 -1
  74. package/core/events/events.ts +2 -4
  75. package/core/events/index.ts +1 -2
  76. package/core/index.ts +28 -12
  77. package/core/infrastructure/agent-detector.ts +3 -3
  78. package/core/infrastructure/ai-provider.ts +23 -20
  79. package/core/infrastructure/author-detector.ts +16 -10
  80. package/core/infrastructure/capability-installer.ts +2 -2
  81. package/core/infrastructure/claude-agent.ts +6 -6
  82. package/core/infrastructure/command-installer.ts +22 -17
  83. package/core/infrastructure/config-manager.ts +18 -14
  84. package/core/infrastructure/editors-config.ts +8 -4
  85. package/core/infrastructure/path-manager.ts +8 -6
  86. package/core/infrastructure/permission-manager.ts +20 -17
  87. package/core/infrastructure/setup.ts +42 -38
  88. package/core/infrastructure/update-checker.ts +5 -5
  89. package/core/integrations/issue-tracker/enricher.ts +8 -19
  90. package/core/integrations/issue-tracker/index.ts +2 -2
  91. package/core/integrations/issue-tracker/manager.ts +15 -15
  92. package/core/integrations/issue-tracker/types.ts +5 -22
  93. package/core/integrations/jira/client.ts +67 -59
  94. package/core/integrations/jira/index.ts +11 -14
  95. package/core/integrations/jira/mcp-adapter.ts +5 -10
  96. package/core/integrations/jira/service.ts +10 -10
  97. package/core/integrations/linear/client.ts +27 -18
  98. package/core/integrations/linear/index.ts +9 -12
  99. package/core/integrations/linear/service.ts +11 -11
  100. package/core/integrations/linear/sync.ts +8 -8
  101. package/core/outcomes/analyzer.ts +5 -18
  102. package/core/outcomes/index.ts +2 -2
  103. package/core/outcomes/recorder.ts +3 -3
  104. package/core/plugin/builtin/webhook.ts +19 -15
  105. package/core/plugin/hooks.ts +29 -21
  106. package/core/plugin/index.ts +7 -7
  107. package/core/plugin/loader.ts +19 -19
  108. package/core/plugin/registry.ts +12 -23
  109. package/core/schemas/agents.ts +1 -1
  110. package/core/schemas/analysis.ts +1 -1
  111. package/core/schemas/enriched-task.ts +62 -49
  112. package/core/schemas/ideas.ts +13 -13
  113. package/core/schemas/index.ts +17 -27
  114. package/core/schemas/issues.ts +40 -25
  115. package/core/schemas/metrics.ts +143 -0
  116. package/core/schemas/outcomes.ts +70 -62
  117. package/core/schemas/permissions.ts +15 -12
  118. package/core/schemas/prd.ts +27 -14
  119. package/core/schemas/project.ts +3 -3
  120. package/core/schemas/roadmap.ts +47 -34
  121. package/core/schemas/schemas.ts +3 -4
  122. package/core/schemas/shipped.ts +3 -3
  123. package/core/schemas/state.ts +43 -29
  124. package/core/server/index.ts +5 -6
  125. package/core/server/routes-extended.ts +68 -72
  126. package/core/server/routes.ts +3 -3
  127. package/core/server/server.ts +31 -26
  128. package/core/services/agent-generator.ts +237 -0
  129. package/core/services/agent-service.ts +2 -2
  130. package/core/services/breakdown-service.ts +2 -4
  131. package/core/services/context-generator.ts +299 -0
  132. package/core/services/context-selector.ts +420 -0
  133. package/core/services/doctor-service.ts +426 -0
  134. package/core/services/file-categorizer.ts +448 -0
  135. package/core/services/file-scorer.ts +270 -0
  136. package/core/services/git-analyzer.ts +267 -0
  137. package/core/services/index.ts +27 -10
  138. package/core/services/memory-service.ts +3 -4
  139. package/core/services/project-index.ts +911 -0
  140. package/core/services/project-service.ts +4 -4
  141. package/core/services/skill-installer.ts +14 -17
  142. package/core/services/skill-lock.ts +3 -3
  143. package/core/services/skill-service.ts +12 -6
  144. package/core/services/stack-detector.ts +245 -0
  145. package/core/services/sync-service.ts +170 -329
  146. package/core/services/watch-service.ts +294 -0
  147. package/core/session/compaction.ts +23 -31
  148. package/core/session/index.ts +11 -5
  149. package/core/session/log-migration.ts +3 -3
  150. package/core/session/metrics.ts +19 -14
  151. package/core/session/session-log-manager.ts +12 -17
  152. package/core/session/task-session-manager.ts +25 -25
  153. package/core/session/utils.ts +1 -1
  154. package/core/storage/ideas-storage.ts +41 -57
  155. package/core/storage/index-storage.ts +514 -0
  156. package/core/storage/index.ts +41 -13
  157. package/core/storage/metrics-storage.ts +320 -0
  158. package/core/storage/queue-storage.ts +35 -45
  159. package/core/storage/shipped-storage.ts +17 -20
  160. package/core/storage/state-storage.ts +50 -30
  161. package/core/storage/storage-manager.ts +6 -6
  162. package/core/storage/storage.ts +18 -15
  163. package/core/sync/auth-config.ts +3 -3
  164. package/core/sync/index.ts +13 -19
  165. package/core/sync/oauth-handler.ts +3 -3
  166. package/core/sync/sync-client.ts +4 -9
  167. package/core/sync/sync-manager.ts +12 -14
  168. package/core/types/commands.ts +42 -7
  169. package/core/types/index.ts +284 -302
  170. package/core/types/integrations.ts +3 -3
  171. package/core/types/storage.ts +49 -0
  172. package/core/types/utils.ts +3 -3
  173. package/core/utils/agent-stream.ts +3 -1
  174. package/core/utils/animations.ts +14 -11
  175. package/core/utils/branding.ts +7 -7
  176. package/core/utils/cache.ts +1 -3
  177. package/core/utils/collection-filters.ts +3 -15
  178. package/core/utils/date-helper.ts +2 -7
  179. package/core/utils/file-helper.ts +13 -8
  180. package/core/utils/jsonl-helper.ts +13 -10
  181. package/core/utils/keychain.ts +4 -8
  182. package/core/utils/logger.ts +1 -1
  183. package/core/utils/next-steps.ts +3 -3
  184. package/core/utils/output.ts +58 -11
  185. package/core/utils/project-commands.ts +6 -6
  186. package/core/utils/project-credentials.ts +5 -12
  187. package/core/utils/runtime.ts +2 -2
  188. package/core/utils/session-helper.ts +3 -4
  189. package/core/utils/version.ts +3 -3
  190. package/core/wizard/index.ts +13 -0
  191. package/core/wizard/onboarding.ts +633 -0
  192. package/core/workflow/state-machine.ts +7 -7
  193. package/dist/bin/prjct.mjs +18907 -13189
  194. package/dist/core/infrastructure/command-installer.js +96 -111
  195. package/dist/core/infrastructure/editors-config.js +6 -6
  196. package/dist/core/infrastructure/setup.js +256 -257
  197. package/dist/core/utils/version.js +9 -9
  198. package/package.json +11 -12
  199. package/scripts/build.js +3 -3
  200. package/scripts/postinstall.js +2 -2
  201. package/templates/mcp-config.json +6 -1
  202. package/templates/permissions/permissive.jsonc +1 -1
  203. package/templates/permissions/strict.jsonc +5 -9
  204. package/templates/global/docs/agents.md +0 -88
  205. package/templates/global/docs/architecture.md +0 -103
  206. package/templates/global/docs/commands.md +0 -96
  207. package/templates/global/docs/validation.md +0 -95
@@ -0,0 +1,514 @@
1
+ /**
2
+ * IndexStorage - Persistent storage for ProjectIndex
3
+ *
4
+ * Stores:
5
+ * - project-index.json: Main index data
6
+ * - file-scores.json: Calculated scores
7
+ * - checksums.json: For detecting file changes
8
+ *
9
+ * Location: ~/.prjct-cli/projects/{projectId}/index/
10
+ */
11
+
12
+ import crypto from 'node:crypto'
13
+ import fs from 'node:fs/promises'
14
+ import path from 'node:path'
15
+ import pathManager from '../infrastructure/path-manager'
16
+ import { isNotFoundError } from '../types/fs'
17
+ import { getTimestamp } from '../utils/date-helper'
18
+
19
+ // ============================================================================
20
+ // TYPES
21
+ // ============================================================================
22
+
23
+ export interface LanguageStats {
24
+ count: number // Number of files
25
+ totalLines: number // Total lines of code
26
+ totalSize: number // Total bytes
27
+ }
28
+
29
+ export interface ConfigFileEntry {
30
+ path: string
31
+ type: string // "package.json", "Cargo.toml", etc.
32
+ checksum: string
33
+ parsed?: Record<string, unknown>
34
+ }
35
+
36
+ export interface DirectoryEntry {
37
+ path: string
38
+ type: 'source' | 'test' | 'config' | 'build' | 'vendor' | 'docs' | 'unknown'
39
+ fileCount: number
40
+ }
41
+
42
+ export interface ScoredFile {
43
+ path: string
44
+ score: number
45
+ size: number
46
+ mtime: string // ISO timestamp
47
+ categories?: string[] // Domain categories: ['payments', 'api', 'backend']
48
+ }
49
+
50
+ export interface DetectedPattern {
51
+ name: string // "monorepo", "api-first", "component-based"
52
+ confidence: number // 0-1
53
+ evidence: string[] // Files/dirs that evidence this pattern
54
+ }
55
+
56
+ export interface DetectedStack {
57
+ ecosystem: string // "JavaScript", "Python", "Rust", etc.
58
+ frameworks: string[] // Detected frameworks
59
+ hasTests: boolean
60
+ hasDocker: boolean
61
+ hasCi: boolean
62
+ buildTool: string | null
63
+ }
64
+
65
+ export interface ProjectIndex {
66
+ version: string
67
+ projectPath: string
68
+ lastFullScan: string // ISO timestamp
69
+ lastIncrementalUpdate: string // ISO timestamp
70
+
71
+ // Language detection by extension
72
+ languages: Record<string, LanguageStats>
73
+
74
+ // Config files found
75
+ configFiles: ConfigFileEntry[]
76
+
77
+ // Directory structure (top-level relevant)
78
+ directories: DirectoryEntry[]
79
+
80
+ // Files with score > threshold
81
+ relevantFiles: ScoredFile[]
82
+
83
+ // Detected patterns
84
+ patterns: DetectedPattern[]
85
+
86
+ // Stack detection
87
+ detectedStack: DetectedStack
88
+
89
+ // Metrics
90
+ totalFiles: number
91
+ totalSize: number // Total bytes
92
+ totalLines: number // Total LOC
93
+ scanDuration: number // ms
94
+ }
95
+
96
+ export interface FileChecksums {
97
+ version: string
98
+ lastUpdated: string
99
+ checksums: Record<string, string> // path -> checksum
100
+ }
101
+
102
+ // ============================================================================
103
+ // DOMAIN & CATEGORY TYPES (for Smart Context Selection)
104
+ // ============================================================================
105
+
106
+ /**
107
+ * A domain discovered by LLM analysis of the project
108
+ */
109
+ export interface DomainDefinition {
110
+ name: string // "payments", "shipping", "inventory"
111
+ description: string // "Handles payment processing, Stripe integration"
112
+ keywords: string[] // ["stripe", "checkout", "billing"]
113
+ filePatterns: string[] // ["**/payments/**", "**/billing/**"]
114
+ fileCount: number // Number of files in this domain
115
+ }
116
+
117
+ /**
118
+ * Discovered domains for a project
119
+ */
120
+ export interface DiscoveredDomains {
121
+ version: string
122
+ projectId: string
123
+ domains: DomainDefinition[]
124
+ discoveredAt: string // ISO timestamp
125
+ }
126
+
127
+ /**
128
+ * Category assignment for a single file
129
+ */
130
+ export interface FileCategory {
131
+ path: string
132
+ categories: string[] // ['payments', 'users', 'api']
133
+ primaryDomain: string // 'payments'
134
+ confidence: number // 0-1
135
+ categorizedAt: string // ISO timestamp
136
+ method: 'llm' | 'heuristic' // How it was categorized
137
+ }
138
+
139
+ /**
140
+ * Cache of file categorizations
141
+ */
142
+ export interface CategoriesCache {
143
+ version: string
144
+ lastUpdate: string
145
+ fileCategories: FileCategory[]
146
+ domainIndex: Record<string, string[]> // domain -> file paths
147
+ }
148
+
149
+ // ============================================================================
150
+ // DEFAULTS
151
+ // ============================================================================
152
+
153
+ export const INDEX_VERSION = '1.0.0'
154
+
155
+ export function getDefaultIndex(projectPath: string): ProjectIndex {
156
+ return {
157
+ version: INDEX_VERSION,
158
+ projectPath,
159
+ lastFullScan: '',
160
+ lastIncrementalUpdate: '',
161
+ languages: {},
162
+ configFiles: [],
163
+ directories: [],
164
+ relevantFiles: [],
165
+ patterns: [],
166
+ detectedStack: {
167
+ ecosystem: 'unknown',
168
+ frameworks: [],
169
+ hasTests: false,
170
+ hasDocker: false,
171
+ hasCi: false,
172
+ buildTool: null,
173
+ },
174
+ totalFiles: 0,
175
+ totalSize: 0,
176
+ totalLines: 0,
177
+ scanDuration: 0,
178
+ }
179
+ }
180
+
181
+ export function getDefaultChecksums(): FileChecksums {
182
+ return {
183
+ version: INDEX_VERSION,
184
+ lastUpdated: '',
185
+ checksums: {},
186
+ }
187
+ }
188
+
189
+ // ============================================================================
190
+ // INDEX STORAGE CLASS
191
+ // ============================================================================
192
+
193
+ class IndexStorage {
194
+ /**
195
+ * Get the index directory path for a project
196
+ */
197
+ getIndexPath(projectId: string): string {
198
+ return path.join(pathManager.getGlobalProjectPath(projectId), 'index')
199
+ }
200
+
201
+ /**
202
+ * Ensure index directory exists
203
+ */
204
+ async ensureIndexDir(projectId: string): Promise<string> {
205
+ const indexPath = this.getIndexPath(projectId)
206
+ await fs.mkdir(indexPath, { recursive: true })
207
+ return indexPath
208
+ }
209
+
210
+ // ==========================================================================
211
+ // PROJECT INDEX
212
+ // ==========================================================================
213
+
214
+ /**
215
+ * Read the project index
216
+ */
217
+ async readIndex(projectId: string): Promise<ProjectIndex | null> {
218
+ const filePath = path.join(this.getIndexPath(projectId), 'project-index.json')
219
+
220
+ try {
221
+ const content = await fs.readFile(filePath, 'utf-8')
222
+ const index = JSON.parse(content) as ProjectIndex
223
+
224
+ // Version check
225
+ if (index.version !== INDEX_VERSION) {
226
+ // Index is outdated, return null to trigger full rescan
227
+ return null
228
+ }
229
+
230
+ return index
231
+ } catch (error) {
232
+ if (isNotFoundError(error)) {
233
+ return null
234
+ }
235
+ throw error
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Write the project index
241
+ */
242
+ async writeIndex(projectId: string, index: ProjectIndex): Promise<void> {
243
+ await this.ensureIndexDir(projectId)
244
+ const filePath = path.join(this.getIndexPath(projectId), 'project-index.json')
245
+ await fs.writeFile(filePath, JSON.stringify(index, null, 2), 'utf-8')
246
+ }
247
+
248
+ /**
249
+ * Check if index exists and is valid
250
+ */
251
+ async hasValidIndex(projectId: string): Promise<boolean> {
252
+ const index = await this.readIndex(projectId)
253
+ return index !== null && index.lastFullScan !== ''
254
+ }
255
+
256
+ // ==========================================================================
257
+ // FILE CHECKSUMS
258
+ // ==========================================================================
259
+
260
+ /**
261
+ * Read file checksums
262
+ */
263
+ async readChecksums(projectId: string): Promise<FileChecksums> {
264
+ const filePath = path.join(this.getIndexPath(projectId), 'checksums.json')
265
+
266
+ try {
267
+ const content = await fs.readFile(filePath, 'utf-8')
268
+ return JSON.parse(content) as FileChecksums
269
+ } catch (error) {
270
+ if (isNotFoundError(error)) {
271
+ return getDefaultChecksums()
272
+ }
273
+ throw error
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Write file checksums
279
+ */
280
+ async writeChecksums(projectId: string, checksums: FileChecksums): Promise<void> {
281
+ await this.ensureIndexDir(projectId)
282
+ const filePath = path.join(this.getIndexPath(projectId), 'checksums.json')
283
+ await fs.writeFile(filePath, JSON.stringify(checksums, null, 2), 'utf-8')
284
+ }
285
+
286
+ /**
287
+ * Calculate checksum for a file
288
+ */
289
+ async calculateChecksum(filePath: string): Promise<string> {
290
+ try {
291
+ const content = await fs.readFile(filePath)
292
+ return crypto.createHash('md5').update(content).digest('hex')
293
+ } catch {
294
+ return ''
295
+ }
296
+ }
297
+
298
+ /**
299
+ * Detect changed files by comparing checksums
300
+ */
301
+ async detectChangedFiles(
302
+ projectId: string,
303
+ currentFiles: Map<string, string> // path -> checksum
304
+ ): Promise<{
305
+ added: string[]
306
+ modified: string[]
307
+ deleted: string[]
308
+ }> {
309
+ const stored = await this.readChecksums(projectId)
310
+ const storedChecksums = stored.checksums
311
+
312
+ const added: string[] = []
313
+ const modified: string[] = []
314
+ const deleted: string[] = []
315
+
316
+ // Check current files against stored
317
+ for (const [filePath, checksum] of currentFiles) {
318
+ if (!(filePath in storedChecksums)) {
319
+ added.push(filePath)
320
+ } else if (storedChecksums[filePath] !== checksum) {
321
+ modified.push(filePath)
322
+ }
323
+ }
324
+
325
+ // Check for deleted files
326
+ for (const filePath of Object.keys(storedChecksums)) {
327
+ if (!currentFiles.has(filePath)) {
328
+ deleted.push(filePath)
329
+ }
330
+ }
331
+
332
+ return { added, modified, deleted }
333
+ }
334
+
335
+ // ==========================================================================
336
+ // FILE SCORES
337
+ // ==========================================================================
338
+
339
+ /**
340
+ * Read file scores
341
+ */
342
+ async readScores(projectId: string): Promise<ScoredFile[]> {
343
+ const filePath = path.join(this.getIndexPath(projectId), 'file-scores.json')
344
+
345
+ try {
346
+ const content = await fs.readFile(filePath, 'utf-8')
347
+ const data = JSON.parse(content) as { scores: ScoredFile[] }
348
+ return data.scores || []
349
+ } catch (error) {
350
+ if (isNotFoundError(error)) {
351
+ return []
352
+ }
353
+ throw error
354
+ }
355
+ }
356
+
357
+ /**
358
+ * Write file scores
359
+ */
360
+ async writeScores(projectId: string, scores: ScoredFile[]): Promise<void> {
361
+ await this.ensureIndexDir(projectId)
362
+ const filePath = path.join(this.getIndexPath(projectId), 'file-scores.json')
363
+ const data = {
364
+ version: INDEX_VERSION,
365
+ lastUpdated: getTimestamp(),
366
+ scores,
367
+ }
368
+ await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8')
369
+ }
370
+
371
+ // ==========================================================================
372
+ // UTILITY METHODS
373
+ // ==========================================================================
374
+
375
+ /**
376
+ * Clear all index data for a project
377
+ */
378
+ async clearIndex(projectId: string): Promise<void> {
379
+ const indexPath = this.getIndexPath(projectId)
380
+
381
+ try {
382
+ const files = await fs.readdir(indexPath)
383
+ await Promise.all(files.map((file) => fs.unlink(path.join(indexPath, file))))
384
+ } catch (error) {
385
+ if (!isNotFoundError(error)) {
386
+ throw error
387
+ }
388
+ }
389
+ }
390
+
391
+ /**
392
+ * Get index age in hours
393
+ */
394
+ async getIndexAge(projectId: string): Promise<number> {
395
+ const index = await this.readIndex(projectId)
396
+ if (!index || !index.lastFullScan) {
397
+ return Infinity
398
+ }
399
+
400
+ const lastScan = new Date(index.lastFullScan)
401
+ const now = new Date()
402
+ return (now.getTime() - lastScan.getTime()) / (1000 * 60 * 60)
403
+ }
404
+
405
+ // ==========================================================================
406
+ // DISCOVERED DOMAINS
407
+ // ==========================================================================
408
+
409
+ /**
410
+ * Read discovered domains for a project
411
+ */
412
+ async readDomains(projectId: string): Promise<DiscoveredDomains | null> {
413
+ const filePath = path.join(this.getIndexPath(projectId), 'domains.json')
414
+
415
+ try {
416
+ const content = await fs.readFile(filePath, 'utf-8')
417
+ const domains = JSON.parse(content) as DiscoveredDomains
418
+
419
+ // Version check
420
+ if (domains.version !== INDEX_VERSION) {
421
+ return null
422
+ }
423
+
424
+ return domains
425
+ } catch (error) {
426
+ if (isNotFoundError(error)) {
427
+ return null
428
+ }
429
+ throw error
430
+ }
431
+ }
432
+
433
+ /**
434
+ * Write discovered domains
435
+ */
436
+ async writeDomains(projectId: string, domains: DiscoveredDomains): Promise<void> {
437
+ await this.ensureIndexDir(projectId)
438
+ const filePath = path.join(this.getIndexPath(projectId), 'domains.json')
439
+ await fs.writeFile(filePath, JSON.stringify(domains, null, 2), 'utf-8')
440
+ }
441
+
442
+ // ==========================================================================
443
+ // CATEGORIES CACHE
444
+ // ==========================================================================
445
+
446
+ /**
447
+ * Read categories cache
448
+ */
449
+ async readCategories(projectId: string): Promise<CategoriesCache | null> {
450
+ const filePath = path.join(this.getIndexPath(projectId), 'categories-cache.json')
451
+
452
+ try {
453
+ const content = await fs.readFile(filePath, 'utf-8')
454
+ const cache = JSON.parse(content) as CategoriesCache
455
+
456
+ // Version check
457
+ if (cache.version !== INDEX_VERSION) {
458
+ return null
459
+ }
460
+
461
+ return cache
462
+ } catch (error) {
463
+ if (isNotFoundError(error)) {
464
+ return null
465
+ }
466
+ throw error
467
+ }
468
+ }
469
+
470
+ /**
471
+ * Write categories cache
472
+ */
473
+ async writeCategories(projectId: string, cache: CategoriesCache): Promise<void> {
474
+ await this.ensureIndexDir(projectId)
475
+ const filePath = path.join(this.getIndexPath(projectId), 'categories-cache.json')
476
+ await fs.writeFile(filePath, JSON.stringify(cache, null, 2), 'utf-8')
477
+ }
478
+
479
+ /**
480
+ * Get file categories for specific paths
481
+ */
482
+ async getFileCategories(projectId: string, paths: string[]): Promise<Map<string, string[]>> {
483
+ const cache = await this.readCategories(projectId)
484
+ const result = new Map<string, string[]>()
485
+
486
+ if (!cache) {
487
+ return result
488
+ }
489
+
490
+ const pathSet = new Set(paths)
491
+ for (const fc of cache.fileCategories) {
492
+ if (pathSet.has(fc.path)) {
493
+ result.set(fc.path, fc.categories)
494
+ }
495
+ }
496
+
497
+ return result
498
+ }
499
+
500
+ /**
501
+ * Get files by domain
502
+ */
503
+ async getFilesByDomain(projectId: string, domain: string): Promise<string[]> {
504
+ const cache = await this.readCategories(projectId)
505
+ if (!cache) {
506
+ return []
507
+ }
508
+
509
+ return cache.domainIndex[domain] || []
510
+ }
511
+ }
512
+
513
+ export const indexStorage = new IndexStorage()
514
+ export default indexStorage
@@ -14,6 +14,10 @@
14
14
  * For future per-entity storage
15
15
  * - getStorage(projectId): data/{entity}s/{id}.json
16
16
  *
17
+ * 3. INDEX STORAGE (New)
18
+ * For persistent project scanning with scoring
19
+ * - indexStorage: index/project-index.json, index/checksums.json
20
+ *
17
21
  * Structure:
18
22
  * ~/.prjct-cli/projects/{projectId}/
19
23
  * ├── storage/ # Aggregate JSON (source of truth)
@@ -27,6 +31,10 @@
27
31
  * │ ├── next.md
28
32
  * │ ├── ideas.md
29
33
  * │ └── shipped.md
34
+ * ├── index/ # Project index (persistent scan)
35
+ * │ ├── project-index.json
36
+ * │ ├── file-scores.json
37
+ * │ └── checksums.json
30
38
  * ├── data/ # Granular JSON (legacy/future)
31
39
  * │ └── ...
32
40
  * └── sync/ # Backend sync
@@ -34,23 +42,43 @@
34
42
  * └── last-sync.json
35
43
  */
36
44
 
37
- // ========== AGGREGATE STORAGE (Recommended) ==========
38
- export { StorageManager } from './storage-manager'
39
- export { stateStorage } from './state-storage'
40
- export { queueStorage } from './queue-storage'
41
- export { ideasStorage } from './ideas-storage'
42
- export { shippedStorage } from './shipped-storage'
43
-
44
- // ========== GRANULAR STORAGE (Legacy) ==========
45
- export { getStorage, default } from './storage'
46
-
47
45
  // Re-export types from canonical location
48
46
  export type {
49
- Storage,
47
+ AgentUsage,
48
+ DailyStats,
50
49
  Idea,
51
- IdeasJson,
52
- IdeaStatus,
53
50
  IdeaPriority,
51
+ IdeaStatus,
52
+ IdeasJson,
53
+ MetricsJson,
54
54
  ShippedFeature,
55
55
  ShippedJson,
56
+ Storage,
56
57
  } from '../types'
58
+ export { ideasStorage } from './ideas-storage'
59
+ export type {
60
+ CategoriesCache,
61
+ ConfigFileEntry,
62
+ DetectedPattern,
63
+ DetectedStack,
64
+ DirectoryEntry,
65
+ DiscoveredDomains,
66
+ // Smart Context Selection types (PRJ-85)
67
+ DomainDefinition,
68
+ FileCategory,
69
+ FileChecksums,
70
+ LanguageStats,
71
+ ProjectIndex,
72
+ ScoredFile,
73
+ } from './index-storage'
74
+ // ========== INDEX STORAGE (Project scanning) ==========
75
+ export { getDefaultChecksums, getDefaultIndex, INDEX_VERSION, indexStorage } from './index-storage'
76
+ export { metricsStorage } from './metrics-storage'
77
+ export { queueStorage } from './queue-storage'
78
+ export { shippedStorage } from './shipped-storage'
79
+ export { stateStorage } from './state-storage'
80
+
81
+ // ========== GRANULAR STORAGE (Legacy) ==========
82
+ export { default, getStorage } from './storage'
83
+ // ========== AGGREGATE STORAGE (Recommended) ==========
84
+ export { StorageManager } from './storage-manager'