prjct-cli 0.15.1 → 0.18.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 (72) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/bin/dev.js +0 -1
  3. package/bin/serve.js +19 -20
  4. package/core/__tests__/agentic/memory-system.test.ts +2 -1
  5. package/core/__tests__/agentic/plan-mode.test.ts +2 -1
  6. package/core/agentic/agent-router.ts +79 -14
  7. package/core/agentic/command-executor/command-executor.ts +2 -74
  8. package/core/agentic/services.ts +0 -48
  9. package/core/agentic/template-loader.ts +35 -1
  10. package/core/command-registry/setup-commands.ts +15 -0
  11. package/core/commands/base.ts +96 -77
  12. package/core/commands/planning.ts +13 -2
  13. package/core/commands/setup.ts +3 -85
  14. package/core/domain/agent-generator.ts +9 -17
  15. package/core/errors.ts +209 -0
  16. package/core/infrastructure/config-manager.ts +22 -5
  17. package/core/infrastructure/path-manager.ts +23 -1
  18. package/core/infrastructure/setup.ts +5 -50
  19. package/core/storage/ideas-storage.ts +4 -0
  20. package/core/storage/queue-storage.ts +4 -0
  21. package/core/storage/shipped-storage.ts +4 -0
  22. package/core/storage/state-storage.ts +4 -0
  23. package/core/storage/storage-manager.ts +52 -13
  24. package/core/sync/auth-config.ts +145 -0
  25. package/core/sync/index.ts +30 -0
  26. package/core/sync/oauth-handler.ts +148 -0
  27. package/core/sync/sync-client.ts +252 -0
  28. package/core/sync/sync-manager.ts +358 -0
  29. package/core/utils/logger.ts +19 -12
  30. package/package.json +2 -4
  31. package/templates/agentic/subagent-generation.md +109 -0
  32. package/templates/commands/auth.md +234 -0
  33. package/templates/commands/sync.md +129 -13
  34. package/templates/subagents/domain/backend.md +105 -0
  35. package/templates/subagents/domain/database.md +118 -0
  36. package/templates/subagents/domain/devops.md +148 -0
  37. package/templates/subagents/domain/frontend.md +99 -0
  38. package/templates/subagents/domain/testing.md +169 -0
  39. package/templates/subagents/workflow/prjct-planner.md +158 -0
  40. package/templates/subagents/workflow/prjct-shipper.md +179 -0
  41. package/templates/subagents/workflow/prjct-workflow.md +98 -0
  42. package/bin/generate-views.js +0 -209
  43. package/bin/migrate-to-json.js +0 -742
  44. package/core/agentic/context-filter.ts +0 -365
  45. package/core/agentic/parallel-tools.ts +0 -165
  46. package/core/agentic/response-templates.ts +0 -164
  47. package/core/agentic/semantic-compression.ts +0 -273
  48. package/core/agentic/think-blocks.ts +0 -202
  49. package/core/agentic/validation-rules.ts +0 -313
  50. package/core/domain/agent-matcher.ts +0 -130
  51. package/core/domain/agent-validator.ts +0 -250
  52. package/core/domain/architect-session.ts +0 -315
  53. package/core/domain/product-standards.ts +0 -106
  54. package/core/domain/smart-cache.ts +0 -167
  55. package/core/domain/task-analyzer.ts +0 -296
  56. package/core/infrastructure/legacy-installer-detector/cleanup.ts +0 -216
  57. package/core/infrastructure/legacy-installer-detector/detection.ts +0 -95
  58. package/core/infrastructure/legacy-installer-detector/index.ts +0 -171
  59. package/core/infrastructure/legacy-installer-detector/migration.ts +0 -87
  60. package/core/infrastructure/legacy-installer-detector/types.ts +0 -42
  61. package/core/infrastructure/legacy-installer-detector.ts +0 -7
  62. package/core/infrastructure/migrator/file-operations.ts +0 -125
  63. package/core/infrastructure/migrator/index.ts +0 -288
  64. package/core/infrastructure/migrator/project-scanner.ts +0 -90
  65. package/core/infrastructure/migrator/reports.ts +0 -117
  66. package/core/infrastructure/migrator/types.ts +0 -124
  67. package/core/infrastructure/migrator/validation.ts +0 -94
  68. package/core/infrastructure/migrator/version-migration.ts +0 -117
  69. package/core/infrastructure/migrator.ts +0 -10
  70. package/core/infrastructure/uuid-migration.ts +0 -750
  71. package/templates/commands/migrate-all.md +0 -96
  72. package/templates/commands/migrate.md +0 -140
@@ -2,8 +2,6 @@
2
2
  * Base class and helpers for PrjctCommands
3
3
  */
4
4
 
5
- import path from 'path'
6
-
7
5
  import commandExecutor from '../agentic/command-executor'
8
6
  import contextBuilder from '../agentic/context-builder'
9
7
  import toolRegistry from '../agentic/tool-registry'
@@ -12,7 +10,6 @@ import pathManager from '../infrastructure/path-manager'
12
10
  import configManager from '../infrastructure/config-manager'
13
11
  import authorDetector from '../infrastructure/author-detector'
14
12
  import agentDetector from '../infrastructure/agent-detector'
15
- import migrator from '../infrastructure/migrator'
16
13
  import UpdateChecker from '../infrastructure/update-checker'
17
14
  import dateHelper from '../utils/date-helper'
18
15
  import jsonlHelper from '../utils/jsonl-helper'
@@ -24,10 +21,23 @@ import type {
24
21
  AgentInfo,
25
22
  Author,
26
23
  AgentAssignmentResult,
27
- ComplexityResult,
28
- HealthResult,
29
24
  Context
30
25
  } from './types'
26
+ import { ProjectError, AgentError } from '../errors'
27
+
28
+ // Valid agent types - whitelist for security (prevents path traversal)
29
+ const VALID_AGENT_TYPES = ['claude'] as const
30
+ type ValidAgentType = typeof VALID_AGENT_TYPES[number]
31
+
32
+ // Lazy-loaded to avoid circular dependencies
33
+ let _planningCommands: import('./planning').PlanningCommands | null = null
34
+ async function getPlanningCommands(): Promise<import('./planning').PlanningCommands> {
35
+ if (!_planningCommands) {
36
+ const { PlanningCommands } = await import('./planning')
37
+ _planningCommands = new PlanningCommands()
38
+ }
39
+ return _planningCommands
40
+ }
31
41
 
32
42
  /**
33
43
  * Base class with shared state and utilities
@@ -62,10 +72,16 @@ export class PrjctCommandsBase {
62
72
  this.agentInfo = await agentDetector.detect()
63
73
 
64
74
  if (!this.agentInfo.isSupported) {
65
- throw new Error('Unsupported agent. Please use Claude Code, Claude Desktop, or Terminal.')
75
+ throw AgentError.notSupported(this.agentInfo.type)
76
+ }
77
+
78
+ // Security: validate agent type against whitelist to prevent path traversal
79
+ const agentType = this.agentInfo.type as ValidAgentType
80
+ if (!VALID_AGENT_TYPES.includes(agentType)) {
81
+ throw AgentError.notSupported(this.agentInfo.type)
66
82
  }
67
83
 
68
- const { default: Agent } = await import(`../infrastructure/agents/${this.agentInfo.type}-agent`)
84
+ const { default: Agent } = await import(`../infrastructure/agents/${agentType}-agent`)
69
85
  this.agent = new Agent()
70
86
 
71
87
  return this.agent
@@ -80,8 +96,8 @@ export class PrjctCommandsBase {
80
96
  }
81
97
 
82
98
  out.spin('initializing project...')
83
- // Note: init() will be implemented in planning module
84
- const initResult = await (this as unknown as { init: (idea: string | null, projectPath: string) => Promise<CommandResult> }).init(null, projectPath)
99
+ const planning = await getPlanningCommands()
100
+ const initResult = await planning.init(null, projectPath)
85
101
  if (!initResult.success) {
86
102
  return initResult
87
103
  }
@@ -107,13 +123,12 @@ export class PrjctCommandsBase {
107
123
  * Get global project path
108
124
  */
109
125
  async getGlobalProjectPath(projectPath: string): Promise<string> {
110
- if (await migrator.needsMigration(projectPath)) {
111
- throw new Error('Project needs migration. Run /p:migrate first.')
112
- }
113
-
114
126
  const projectId = await configManager.getProjectId(projectPath)
115
- await pathManager.ensureProjectStructure(projectId!)
116
- return pathManager.getGlobalProjectPath(projectId!)
127
+ if (!projectId) {
128
+ throw ProjectError.notInitialized()
129
+ }
130
+ await pathManager.ensureProjectStructure(projectId)
131
+ return pathManager.getGlobalProjectPath(projectId)
117
132
  }
118
133
 
119
134
  /**
@@ -174,64 +189,6 @@ export class PrjctCommandsBase {
174
189
  }
175
190
  }
176
191
 
177
- /**
178
- * Assign agent for a task
179
- */
180
- async _assignAgentForTask(taskDescription: string, projectPath: string, _context: Context): Promise<AgentAssignmentResult> {
181
- try {
182
- const projectId = await configManager.getProjectId(projectPath)
183
- const agentsPath = pathManager.getFilePath(projectId!, 'agents', '')
184
- const agentFiles = await fileHelper.listFiles(agentsPath, { extension: '.md' })
185
- const agents = agentFiles.map(f => f.replace('.md', ''))
186
-
187
- return {
188
- agent: { name: agents[0] || 'generalist', domain: 'auto' },
189
- routing: {
190
- confidence: 0.8,
191
- reason: 'Claude assigns via templates/agent-assignment.md',
192
- availableAgents: agents
193
- },
194
- _agenticNote: 'Use templates/agent-assignment.md for actual assignment'
195
- }
196
- } catch {
197
- return {
198
- agent: { name: 'generalist', domain: 'general' },
199
- routing: { confidence: 0.5, reason: 'Fallback - no agents found' }
200
- }
201
- }
202
- }
203
-
204
- /**
205
- * Detect task complexity
206
- */
207
- _detectComplexity(_task: string): ComplexityResult {
208
- return { level: 'medium', hours: 4, type: 'feature' }
209
- }
210
-
211
- /**
212
- * Calculate project health score
213
- */
214
- _calculateHealth(stats: { activeTask: boolean; featuresShipped: number }): HealthResult {
215
- const hasActivity = stats.activeTask || stats.featuresShipped > 0
216
- return {
217
- score: hasActivity ? 70 : 50,
218
- message: hasActivity ? '🟢 Active' : '🟡 Ready to start',
219
- }
220
- }
221
-
222
- /**
223
- * Render ASCII progress bar
224
- */
225
- _renderProgressBar(label: string, value: number, max: number): void {
226
- const percentage = Math.min(100, Math.round((value / max) * 100))
227
- const barLength = 30
228
- const filled = Math.round((percentage / 100) * barLength)
229
- const empty = barLength - filled
230
-
231
- const bar = '█'.repeat(filled) + '░'.repeat(empty)
232
- console.log(` ${label}: [${bar}] ${percentage}%`)
233
- }
234
-
235
192
  /**
236
193
  * Breakdown feature into tasks
237
194
  */
@@ -252,11 +209,73 @@ export class PrjctCommandsBase {
252
209
  }
253
210
 
254
211
  /**
255
- * Auto-assign agent based on task (deprecated)
212
+ * Assign agent for a task using AgentRouter
213
+ * Returns agent info for Claude to delegate work
256
214
  */
257
- _autoAssignAgent(_task: string): string {
258
- console.warn('DEPRECATED: Use _assignAgentForTask() for proper agent routing')
259
- return 'generalist'
215
+ async _assignAgentForTask(
216
+ task: string,
217
+ projectPath: string,
218
+ _context: Context
219
+ ): Promise<AgentAssignmentResult> {
220
+ try {
221
+ await this.agentRouter.initialize(projectPath)
222
+ const agents = await this.agentRouter.getAgentNames()
223
+
224
+ if (agents.length === 0) {
225
+ return {
226
+ agent: { name: 'generalist' },
227
+ routing: {
228
+ confidence: 1.0,
229
+ reason: 'No specialized agents available',
230
+ availableAgents: [],
231
+ },
232
+ }
233
+ }
234
+
235
+ // Simple keyword matching for agent assignment
236
+ // Claude will make the final decision via templates
237
+ const taskLower = task.toLowerCase()
238
+ let bestMatch = 'generalist'
239
+
240
+ for (const agentName of agents) {
241
+ const nameLower = agentName.toLowerCase()
242
+ if (taskLower.includes(nameLower) || nameLower.includes('general')) {
243
+ bestMatch = agentName
244
+ break
245
+ }
246
+ // Common domain keywords
247
+ if ((nameLower.includes('fe') || nameLower.includes('frontend')) &&
248
+ (taskLower.includes('ui') || taskLower.includes('component') || taskLower.includes('react'))) {
249
+ bestMatch = agentName
250
+ break
251
+ }
252
+ if ((nameLower.includes('be') || nameLower.includes('backend')) &&
253
+ (taskLower.includes('api') || taskLower.includes('server') || taskLower.includes('database'))) {
254
+ bestMatch = agentName
255
+ break
256
+ }
257
+ }
258
+
259
+ await this.agentRouter.logUsage(task, bestMatch, projectPath)
260
+
261
+ return {
262
+ agent: { name: bestMatch },
263
+ routing: {
264
+ confidence: 0.7,
265
+ reason: 'Keyword-based agent matching',
266
+ availableAgents: agents,
267
+ },
268
+ _agenticNote: 'Claude should verify this assignment using agent context',
269
+ }
270
+ } catch {
271
+ return {
272
+ agent: { name: 'generalist' },
273
+ routing: {
274
+ confidence: 1.0,
275
+ reason: 'Agent routing unavailable',
276
+ },
277
+ }
278
+ }
260
279
  }
261
280
  }
262
281
 
@@ -22,6 +22,16 @@ import { queueStorage, ideasStorage } from '../storage'
22
22
  import authorDetector from '../infrastructure/author-detector'
23
23
  import commandInstaller from '../infrastructure/command-installer'
24
24
 
25
+ // Lazy-loaded to avoid circular dependencies
26
+ let _analysisCommands: import('./analysis').AnalysisCommands | null = null
27
+ async function getAnalysisCommands(): Promise<import('./analysis').AnalysisCommands> {
28
+ if (!_analysisCommands) {
29
+ const { AnalysisCommands } = await import('./analysis')
30
+ _analysisCommands = new AnalysisCommands()
31
+ }
32
+ return _analysisCommands
33
+ }
34
+
25
35
  export class PlanningCommands extends PrjctCommandsBase {
26
36
  /**
27
37
  * /p:init - Initialize prjct project
@@ -82,11 +92,12 @@ export class PlanningCommands extends PrjctCommandsBase {
82
92
 
83
93
  if (hasCode || !isEmpty) {
84
94
  out.spin('analyzing project...')
85
- const analysisResult = await (this as unknown as { analyze: (options: AnalyzeOptions, projectPath: string) => Promise<CommandResult> }).analyze({}, projectPath)
95
+ const analysis = await getAnalysisCommands()
96
+ const analysisResult = await analysis.analyze({}, projectPath)
86
97
 
87
98
  if (analysisResult.success) {
88
99
  out.spin('generating agents...')
89
- await (this as unknown as { sync: (projectPath: string) => Promise<CommandResult> }).sync(projectPath)
100
+ await analysis.sync(projectPath)
90
101
  out.done('initialized')
91
102
  return { success: true, mode: 'existing', projectId }
92
103
  }
@@ -1,21 +1,15 @@
1
1
  /**
2
- * Setup Commands: start, setup, migrateAll, installStatusLine, showAsciiArt
2
+ * Setup Commands: start, setup, installStatusLine, showAsciiArt
3
3
  */
4
4
 
5
5
  import path from 'path'
6
6
  import fs from 'fs'
7
- import { promises as fsPromises } from 'fs'
8
7
  import os from 'os'
9
8
  import chalk from 'chalk'
10
9
 
11
- import migrator from '../infrastructure/migrator'
12
10
  import commandInstaller from '../infrastructure/command-installer'
13
- import type { CommandResult, SetupOptions, MigrateOptions, GlobalConfig, MigrationResult } from './types'
14
- import {
15
- PrjctCommandsBase,
16
- configManager,
17
- dateHelper
18
- } from './base'
11
+ import type { CommandResult, SetupOptions } from './types'
12
+ import { PrjctCommandsBase } from './base'
19
13
  import { VERSION } from '../utils/version'
20
14
 
21
15
  export class SetupCommands extends PrjctCommandsBase {
@@ -225,80 +219,4 @@ fi
225
219
  console.log('')
226
220
  }
227
221
 
228
- /**
229
- * Migrate all legacy projects
230
- */
231
- async migrateAll(options: MigrateOptions = {}): Promise<CommandResult> {
232
- console.log('🔄 Scanning for legacy prjct projects...\n')
233
-
234
- const homeDir = os.homedir()
235
- const globalRoot = path.join(homeDir, '.prjct-cli', 'projects')
236
-
237
- let projectIds: string[] = []
238
- try {
239
- const dirs = await fsPromises.readdir(globalRoot)
240
- projectIds = dirs.filter((d: string) => !d.startsWith('.'))
241
- } catch {
242
- return {
243
- success: false,
244
- message: '❌ No prjct projects found',
245
- }
246
- }
247
-
248
- console.log(`📁 Found ${projectIds.length} projects in global storage\n`)
249
-
250
- const migrated: { projectId: string; path: string }[] = []
251
- const failed: { projectId: string; path: string; error: string }[] = []
252
- const skipped: { projectId: string; reason: string }[] = []
253
-
254
- for (const projectId of projectIds) {
255
- const globalConfig = await configManager.readGlobalConfig(projectId) as GlobalConfig | null
256
- if (!globalConfig || !globalConfig.projectPath) {
257
- skipped.push({ projectId, reason: 'No project path in config' })
258
- continue
259
- }
260
-
261
- const projectPath = globalConfig.projectPath!
262
-
263
- if (!(await migrator.needsMigration(projectPath))) {
264
- skipped.push({ projectId, reason: 'Already migrated' })
265
- continue
266
- }
267
-
268
- console.log(`🔄 Migrating: ${projectPath}`)
269
-
270
- try {
271
- const result = await migrator.migrate(projectPath, options) as MigrationResult
272
-
273
- if (result.success) {
274
- migrated.push({ projectId, path: projectPath })
275
- console.log(` ✅ Migrated successfully`)
276
- } else {
277
- const issues = result.issues?.join(', ') || 'Unknown error'
278
- failed.push({ projectId, path: projectPath, error: issues })
279
- console.log(` ❌ ${issues}`)
280
- }
281
- } catch (error) {
282
- failed.push({ projectId, path: projectPath, error: (error as Error).message })
283
- console.log(` ❌ ${(error as Error).message}`)
284
- }
285
-
286
- console.log('')
287
- }
288
-
289
- console.log('\n📊 Migration Summary:')
290
- console.log(` ✅ Migrated: ${migrated.length}`)
291
- console.log(` ⏭️ Skipped: ${skipped.length}`)
292
- console.log(` ❌ Failed: ${failed.length}`)
293
-
294
- if (failed.length > 0) {
295
- console.log('\n❌ Failed migrations:')
296
- failed.forEach((f) => console.log(` - ${f.path}: ${f.error}`))
297
- }
298
-
299
- return {
300
- success: failed.length === 0,
301
- message: '',
302
- }
303
- }
304
222
  }
@@ -38,9 +38,9 @@ class AgentGenerator {
38
38
 
39
39
  constructor(projectId: string | null = null) {
40
40
  this.projectId = projectId
41
- // NEW: Write to data/agents/ for JSON storage (OpenCode-style)
41
+ // Write to agents/ for MD storage (matches AgentLoader)
42
42
  this.outputDir = projectId
43
- ? path.join(os.homedir(), '.prjct-cli', 'projects', projectId, 'data', 'agents')
43
+ ? path.join(os.homedir(), '.prjct-cli', 'projects', projectId, 'agents')
44
44
  : path.join(os.homedir(), '.prjct-cli', 'agents')
45
45
  this.loader = new AgentLoader(projectId)
46
46
  }
@@ -54,18 +54,10 @@ class AgentGenerator {
54
54
  log.debug(`Generating ${agentName} agent...`)
55
55
  await fs.mkdir(this.outputDir, { recursive: true })
56
56
 
57
- // Write as JSON (OpenCode-style storage)
58
- const agent = {
59
- name: agentName,
60
- role: config.role || agentName,
61
- domain: config.domain || 'general',
62
- expertise: config.expertise || '',
63
- contextFilter: config.contextFilter || 'Only relevant files',
64
- createdAt: new Date().toISOString()
65
- }
66
-
67
- const outputPath = path.join(this.outputDir, `${agentName}.json`)
68
- await fs.writeFile(outputPath, JSON.stringify(agent, null, 2), 'utf-8')
57
+ // Write as MD (matches AgentLoader which reads .md files)
58
+ const content = this.buildAgentPrompt(agentName, config)
59
+ const outputPath = path.join(this.outputDir, `${agentName}.md`)
60
+ await fs.writeFile(outputPath, content, 'utf-8')
69
61
  log.debug(`${agentName} agent created`)
70
62
 
71
63
  return { name: agentName }
@@ -135,10 +127,10 @@ ${config.contextFilter || 'Only relevant files'}
135
127
 
136
128
  try {
137
129
  const files = await fs.readdir(this.outputDir)
138
- const agentFiles = files.filter((f) => f.endsWith('.json') && !f.startsWith('.'))
130
+ const agentFiles = files.filter((f) => f.endsWith('.md') && !f.startsWith('.'))
139
131
 
140
132
  for (const file of agentFiles) {
141
- const type = file.replace('.json', '')
133
+ const type = file.replace('.md', '')
142
134
 
143
135
  if (!requiredAgents.includes(type)) {
144
136
  const filePath = path.join(this.outputDir, file)
@@ -160,7 +152,7 @@ ${config.contextFilter || 'Only relevant files'}
160
152
  async listAgents(): Promise<string[]> {
161
153
  try {
162
154
  const files = await fs.readdir(this.outputDir)
163
- return files.filter((f) => f.endsWith('.json') && !f.startsWith('.')).map((f) => f.replace('.json', ''))
155
+ return files.filter((f) => f.endsWith('.md') && !f.startsWith('.')).map((f) => f.replace('.md', ''))
164
156
  } catch {
165
157
  return []
166
158
  }
package/core/errors.ts ADDED
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Custom Error Hierarchy for prjct-cli
3
+ *
4
+ * Base error class with specific subclasses for different error domains.
5
+ * Enables typed error handling and better error messages.
6
+ *
7
+ * @module core/errors
8
+ * @version 1.0.0
9
+ */
10
+
11
+ /**
12
+ * Base error class for all prjct errors
13
+ */
14
+ export class PrjctError extends Error {
15
+ readonly code: string
16
+ readonly isOperational: boolean
17
+
18
+ constructor(message: string, code = 'PRJCT_ERROR') {
19
+ super(message)
20
+ this.name = 'PrjctError'
21
+ this.code = code
22
+ this.isOperational = true // Distinguishes from programming errors
23
+ Error.captureStackTrace?.(this, this.constructor)
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Configuration-related errors
29
+ */
30
+ export class ConfigError extends PrjctError {
31
+ constructor(message: string, code = 'CONFIG_ERROR') {
32
+ super(message, code)
33
+ this.name = 'ConfigError'
34
+ }
35
+
36
+ static notFound(path: string): ConfigError {
37
+ return new ConfigError(`Configuration not found: ${path}`, 'CONFIG_NOT_FOUND')
38
+ }
39
+
40
+ static invalid(reason: string): ConfigError {
41
+ return new ConfigError(`Invalid configuration: ${reason}`, 'CONFIG_INVALID')
42
+ }
43
+
44
+ static parseError(path: string): ConfigError {
45
+ return new ConfigError(`Failed to parse configuration: ${path}`, 'CONFIG_PARSE_ERROR')
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Storage and file system errors
51
+ */
52
+ export class StorageError extends PrjctError {
53
+ constructor(message: string, code = 'STORAGE_ERROR') {
54
+ super(message, code)
55
+ this.name = 'StorageError'
56
+ }
57
+
58
+ static readFailed(path: string): StorageError {
59
+ return new StorageError(`Failed to read: ${path}`, 'STORAGE_READ_FAILED')
60
+ }
61
+
62
+ static writeFailed(path: string): StorageError {
63
+ return new StorageError(`Failed to write: ${path}`, 'STORAGE_WRITE_FAILED')
64
+ }
65
+
66
+ static notFound(path: string): StorageError {
67
+ return new StorageError(`File not found: ${path}`, 'STORAGE_NOT_FOUND')
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Project-related errors
73
+ */
74
+ export class ProjectError extends PrjctError {
75
+ constructor(message: string, code = 'PROJECT_ERROR') {
76
+ super(message, code)
77
+ this.name = 'ProjectError'
78
+ }
79
+
80
+ static notInitialized(): ProjectError {
81
+ return new ProjectError('Project not initialized. Run /p:init first.', 'PROJECT_NOT_INIT')
82
+ }
83
+
84
+ static notFound(projectId: string): ProjectError {
85
+ return new ProjectError(`Project not found: ${projectId}`, 'PROJECT_NOT_FOUND')
86
+ }
87
+
88
+ static invalidId(projectId: string): ProjectError {
89
+ return new ProjectError(`Invalid project ID: ${projectId}`, 'PROJECT_INVALID_ID')
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Command execution errors
95
+ */
96
+ export class CommandError extends PrjctError {
97
+ constructor(message: string, code = 'COMMAND_ERROR') {
98
+ super(message, code)
99
+ this.name = 'CommandError'
100
+ }
101
+
102
+ static notFound(commandName: string): CommandError {
103
+ return new CommandError(`Command not found: ${commandName}`, 'COMMAND_NOT_FOUND')
104
+ }
105
+
106
+ static invalidParams(reason: string): CommandError {
107
+ return new CommandError(`Invalid parameters: ${reason}`, 'COMMAND_INVALID_PARAMS')
108
+ }
109
+
110
+ static executionFailed(commandName: string, reason: string): CommandError {
111
+ return new CommandError(`Command '${commandName}' failed: ${reason}`, 'COMMAND_EXEC_FAILED')
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Template-related errors
117
+ */
118
+ export class TemplateError extends PrjctError {
119
+ constructor(message: string, code = 'TEMPLATE_ERROR') {
120
+ super(message, code)
121
+ this.name = 'TemplateError'
122
+ }
123
+
124
+ static notFound(templateName: string): TemplateError {
125
+ return new TemplateError(`Template not found: ${templateName}.md`, 'TEMPLATE_NOT_FOUND')
126
+ }
127
+
128
+ static parseFailed(templateName: string): TemplateError {
129
+ return new TemplateError(`Failed to parse template: ${templateName}`, 'TEMPLATE_PARSE_ERROR')
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Agent-related errors
135
+ */
136
+ export class AgentError extends PrjctError {
137
+ constructor(message: string, code = 'AGENT_ERROR') {
138
+ super(message, code)
139
+ this.name = 'AgentError'
140
+ }
141
+
142
+ static notSupported(agentType: string): AgentError {
143
+ return new AgentError(`Unsupported agent type: ${agentType}`, 'AGENT_NOT_SUPPORTED')
144
+ }
145
+
146
+ static initFailed(reason: string): AgentError {
147
+ return new AgentError(`Agent initialization failed: ${reason}`, 'AGENT_INIT_FAILED')
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Type guard to check if error is a PrjctError
153
+ */
154
+ export function isPrjctError(error: unknown): error is PrjctError {
155
+ return error instanceof PrjctError
156
+ }
157
+
158
+ /**
159
+ * Type guard for specific error types
160
+ */
161
+ export function isConfigError(error: unknown): error is ConfigError {
162
+ return error instanceof ConfigError
163
+ }
164
+
165
+ export function isStorageError(error: unknown): error is StorageError {
166
+ return error instanceof StorageError
167
+ }
168
+
169
+ export function isProjectError(error: unknown): error is ProjectError {
170
+ return error instanceof ProjectError
171
+ }
172
+
173
+ export function isCommandError(error: unknown): error is CommandError {
174
+ return error instanceof CommandError
175
+ }
176
+
177
+ export function isTemplateError(error: unknown): error is TemplateError {
178
+ return error instanceof TemplateError
179
+ }
180
+
181
+ export function isAgentError(error: unknown): error is AgentError {
182
+ return error instanceof AgentError
183
+ }
184
+
185
+ /**
186
+ * Extract error message safely from unknown error
187
+ */
188
+ export function getErrorMessage(error: unknown): string {
189
+ if (isPrjctError(error)) {
190
+ return error.message
191
+ }
192
+ if (error instanceof Error) {
193
+ return error.message
194
+ }
195
+ if (typeof error === 'string') {
196
+ return error
197
+ }
198
+ return 'Unknown error'
199
+ }
200
+
201
+ /**
202
+ * Extract error code safely from unknown error
203
+ */
204
+ export function getErrorCode(error: unknown): string {
205
+ if (isPrjctError(error)) {
206
+ return error.code
207
+ }
208
+ return 'UNKNOWN_ERROR'
209
+ }
@@ -13,7 +13,9 @@
13
13
  import fs from 'fs/promises'
14
14
  import path from 'path'
15
15
  import pathManager from './path-manager'
16
+ import authorDetector from './author-detector'
16
17
  import { VERSION } from '../utils/version'
18
+ import { ConfigError, getErrorMessage } from '../errors'
17
19
 
18
20
  interface Author {
19
21
  name: string
@@ -50,7 +52,13 @@ class ConfigManager {
50
52
  const configPath = pathManager.getLocalConfigPath(projectPath)
51
53
  const content = await fs.readFile(configPath, 'utf-8')
52
54
  return JSON.parse(content)
53
- } catch {
55
+ } catch (error) {
56
+ // File not found is expected - return null
57
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
58
+ return null
59
+ }
60
+ // JSON parse errors or other issues - log and return null
61
+ console.warn(`Warning: Could not read config at ${projectPath}: ${getErrorMessage(error)}`)
54
62
  return null
55
63
  }
56
64
  }
@@ -77,7 +85,13 @@ class ConfigManager {
77
85
  const configPath = pathManager.getGlobalProjectConfigPath(projectId)
78
86
  const content = await fs.readFile(configPath, 'utf-8')
79
87
  return JSON.parse(content)
80
- } catch {
88
+ } catch (error) {
89
+ // File not found is expected for new projects
90
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
91
+ return null
92
+ }
93
+ // Log other errors for debugging
94
+ console.warn(`Warning: Could not read global config for ${projectId}: ${getErrorMessage(error)}`)
81
95
  return null
82
96
  }
83
97
  }
@@ -202,7 +216,12 @@ class ConfigManager {
202
216
  try {
203
217
  const coreFiles = await fs.readdir(path.join(globalPath, 'core'))
204
218
  return coreFiles.length === 0
205
- } catch {
219
+ } catch (error) {
220
+ // Directory not found means migration needed
221
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
222
+ return true
223
+ }
224
+ // Permission errors or other issues - assume migration needed
206
225
  return true
207
226
  }
208
227
  }
@@ -275,8 +294,6 @@ class ConfigManager {
275
294
  * Get current author for session (detect or get from global config)
276
295
  */
277
296
  async getCurrentAuthor(projectPath: string): Promise<string> {
278
- // Dynamic import to avoid circular dependency
279
- const authorDetector = (await import('./author-detector')).default
280
297
  const author = await authorDetector.detect()
281
298
 
282
299
  const projectId = await this.getProjectId(projectPath)