prjct-cli 0.4.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 (71) hide show
  1. package/CHANGELOG.md +312 -0
  2. package/CLAUDE.md +300 -0
  3. package/LICENSE +21 -0
  4. package/README.md +424 -0
  5. package/bin/prjct +214 -0
  6. package/core/agent-detector.js +249 -0
  7. package/core/agents/claude-agent.js +250 -0
  8. package/core/agents/codex-agent.js +256 -0
  9. package/core/agents/terminal-agent.js +465 -0
  10. package/core/analyzer.js +596 -0
  11. package/core/animations-simple.js +240 -0
  12. package/core/animations.js +277 -0
  13. package/core/author-detector.js +218 -0
  14. package/core/capability-installer.js +190 -0
  15. package/core/command-installer.js +775 -0
  16. package/core/commands.js +2050 -0
  17. package/core/config-manager.js +335 -0
  18. package/core/migrator.js +784 -0
  19. package/core/path-manager.js +324 -0
  20. package/core/project-capabilities.js +144 -0
  21. package/core/session-manager.js +439 -0
  22. package/core/version.js +107 -0
  23. package/core/workflow-engine.js +213 -0
  24. package/core/workflow-prompts.js +192 -0
  25. package/core/workflow-rules.js +147 -0
  26. package/package.json +80 -0
  27. package/scripts/install.sh +433 -0
  28. package/scripts/verify-installation.sh +158 -0
  29. package/templates/agents/AGENTS.md +164 -0
  30. package/templates/commands/analyze.md +125 -0
  31. package/templates/commands/cleanup.md +102 -0
  32. package/templates/commands/context.md +105 -0
  33. package/templates/commands/design.md +113 -0
  34. package/templates/commands/done.md +44 -0
  35. package/templates/commands/fix.md +87 -0
  36. package/templates/commands/git.md +79 -0
  37. package/templates/commands/help.md +72 -0
  38. package/templates/commands/idea.md +50 -0
  39. package/templates/commands/init.md +237 -0
  40. package/templates/commands/next.md +74 -0
  41. package/templates/commands/now.md +35 -0
  42. package/templates/commands/progress.md +92 -0
  43. package/templates/commands/recap.md +86 -0
  44. package/templates/commands/roadmap.md +107 -0
  45. package/templates/commands/ship.md +41 -0
  46. package/templates/commands/stuck.md +48 -0
  47. package/templates/commands/task.md +97 -0
  48. package/templates/commands/test.md +94 -0
  49. package/templates/commands/workflow.md +224 -0
  50. package/templates/examples/natural-language-examples.md +320 -0
  51. package/templates/mcp-config.json +8 -0
  52. package/templates/workflows/analyze.md +159 -0
  53. package/templates/workflows/cleanup.md +73 -0
  54. package/templates/workflows/context.md +72 -0
  55. package/templates/workflows/design.md +88 -0
  56. package/templates/workflows/done.md +20 -0
  57. package/templates/workflows/fix.md +201 -0
  58. package/templates/workflows/git.md +192 -0
  59. package/templates/workflows/help.md +13 -0
  60. package/templates/workflows/idea.md +22 -0
  61. package/templates/workflows/init.md +80 -0
  62. package/templates/workflows/natural-language-handler.md +183 -0
  63. package/templates/workflows/next.md +44 -0
  64. package/templates/workflows/now.md +19 -0
  65. package/templates/workflows/progress.md +113 -0
  66. package/templates/workflows/recap.md +66 -0
  67. package/templates/workflows/roadmap.md +95 -0
  68. package/templates/workflows/ship.md +18 -0
  69. package/templates/workflows/stuck.md +25 -0
  70. package/templates/workflows/task.md +109 -0
  71. package/templates/workflows/test.md +243 -0
@@ -0,0 +1,2050 @@
1
+ const fs = require('fs').promises
2
+ const path = require('path')
3
+ const { promisify } = require('util')
4
+ const { exec: execCallback } = require('child_process')
5
+ const exec = promisify(execCallback)
6
+ const agentDetector = require('./agent-detector')
7
+ const pathManager = require('./path-manager')
8
+ const configManager = require('./config-manager')
9
+ const authorDetector = require('./author-detector')
10
+ const migrator = require('./migrator')
11
+ const commandInstaller = require('./command-installer')
12
+ const sessionManager = require('./session-manager')
13
+ const analyzer = require('./analyzer')
14
+ const { VERSION } = require('./version')
15
+
16
+ let animations
17
+ try {
18
+ animations = require('./animations')
19
+ } catch (e) {
20
+ animations = null
21
+ }
22
+
23
+ let Agent
24
+
25
+ /**
26
+ * Main command handler for prjct CLI
27
+ *
28
+ * Manages project workflow commands including task tracking, shipping features,
29
+ * idea capture, and project analysis
30
+ */
31
+ class PrjctCommands {
32
+ constructor() {
33
+ this.agent = null
34
+ this.agentInfo = null
35
+ this.currentAuthor = null
36
+ this.prjctDir = '.prjct'
37
+ }
38
+
39
+ /**
40
+ * Generate semantic branch name from task description
41
+ *
42
+ * @param {string} task - Task description
43
+ * @returns {string} Branch name in format type/description
44
+ */
45
+ generateBranchName(task) {
46
+ let branchType = 'chore'
47
+
48
+ const taskLower = task.toLowerCase()
49
+
50
+ if (taskLower.match(/^(add|implement|create|build|feature|new)/)) {
51
+ branchType = 'feat'
52
+ } else if (taskLower.match(/^(fix|resolve|repair|correct|bug|issue)/)) {
53
+ branchType = 'fix'
54
+ } else if (taskLower.match(/^(refactor|improve|optimize|enhance|cleanup|clean)/)) {
55
+ branchType = 'refactor'
56
+ } else if (taskLower.match(/^(document|docs|readme|update doc)/)) {
57
+ branchType = 'docs'
58
+ } else if (taskLower.match(/^(test|testing|spec|add test)/)) {
59
+ branchType = 'test'
60
+ } else if (taskLower.match(/^(style|format|lint)/)) {
61
+ branchType = 'style'
62
+ } else if (taskLower.match(/^(deploy|release|ci|cd|config)/)) {
63
+ branchType = 'chore'
64
+ }
65
+
66
+ const cleanDescription = task
67
+ .toLowerCase()
68
+ .replace(/[^a-z0-9\s-]/g, '')
69
+ .replace(/\s+/g, '-')
70
+ .replace(/-+/g, '-')
71
+ .replace(/^-|-$/g, '')
72
+ .slice(0, 50)
73
+
74
+ return `${branchType}/${cleanDescription}`
75
+ }
76
+
77
+ /**
78
+ * Execute git command with error handling
79
+ *
80
+ * @param {string} command - Git command to execute
81
+ * @param {string} [cwd=process.cwd()] - Working directory
82
+ * @returns {Promise<Object>} Result object with success flag and output
83
+ */
84
+ async execGitCommand(command, cwd = process.cwd()) {
85
+ try {
86
+ const { stdout, stderr } = await exec(command, { cwd })
87
+ return { success: true, stdout: stdout.trim(), stderr: stderr.trim() }
88
+ } catch (error) {
89
+ return { success: false, error: error.message }
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Check if current directory is a git repository
95
+ *
96
+ * @param {string} [projectPath=process.cwd()] - Project path to check
97
+ * @returns {Promise<boolean>} True if git repository
98
+ */
99
+ async isGitRepo(projectPath = process.cwd()) {
100
+ const result = await this.execGitCommand('git rev-parse --is-inside-work-tree', projectPath)
101
+ return result.success && result.stdout === 'true'
102
+ }
103
+
104
+ /**
105
+ * Create and switch to a new git branch
106
+ *
107
+ * @param {string} branchName - Name of branch to create
108
+ * @param {string} [projectPath=process.cwd()] - Project path
109
+ * @returns {Promise<Object>} Result object with success flag and message
110
+ */
111
+ async createAndSwitchBranch(branchName, projectPath = process.cwd()) {
112
+ if (!await this.isGitRepo(projectPath)) {
113
+ return { success: false, message: 'Not a git repository' }
114
+ }
115
+
116
+ const statusResult = await this.execGitCommand('git status --porcelain', projectPath)
117
+ if (statusResult.stdout) {
118
+ await this.execGitCommand('git stash push -m "Auto-stash before branch creation"', projectPath)
119
+ }
120
+
121
+ const branchExists = await this.execGitCommand(`git show-ref --verify --quiet refs/heads/${branchName}`, projectPath)
122
+
123
+ if (branchExists.success) {
124
+ const switchResult = await this.execGitCommand(`git checkout ${branchName}`, projectPath)
125
+ if (!switchResult.success) {
126
+ return { success: false, message: `Failed to switch to existing branch: ${branchName}` }
127
+ }
128
+ return { success: true, message: `Switched to existing branch: ${branchName}`, existed: true }
129
+ }
130
+
131
+ const createResult = await this.execGitCommand(`git checkout -b ${branchName}`, projectPath)
132
+
133
+ if (!createResult.success) {
134
+ return { success: false, message: `Failed to create branch: ${createResult.error}` }
135
+ }
136
+
137
+ if (statusResult.stdout) {
138
+ await this.execGitCommand('git stash pop', projectPath)
139
+ }
140
+
141
+ return { success: true, message: `Created and switched to new branch: ${branchName}`, existed: false }
142
+ }
143
+
144
+ /**
145
+ * Initialize agent detection and load appropriate adapter
146
+ * Also handles automatic global migration on first run
147
+ *
148
+ * @returns {Promise<Object>} Initialized agent instance
149
+ */
150
+ async initializeAgent() {
151
+ if (this.agent) return this.agent
152
+
153
+ this.agentInfo = await agentDetector.detect()
154
+
155
+ console.debug(`[prjct] Detected agent: ${this.agentInfo.name} (${this.agentInfo.type})`)
156
+
157
+ switch (this.agentInfo.type) {
158
+ case 'claude':
159
+ Agent = require('./agents/claude-agent')
160
+ break
161
+ case 'codex':
162
+ Agent = require('./agents/codex-agent')
163
+ break
164
+ case 'terminal':
165
+ default:
166
+ Agent = require('./agents/terminal-agent')
167
+ break
168
+ }
169
+
170
+ this.agent = new Agent()
171
+
172
+ await this.checkAndRunAutoMigration()
173
+
174
+ return this.agent
175
+ }
176
+
177
+ /**
178
+ * Check if automatic migration is needed and run it transparently
179
+ * This runs only once per installation using a flag file
180
+ *
181
+ * @private
182
+ */
183
+ async checkAndRunAutoMigration() {
184
+ try {
185
+ const flagPath = path.join(pathManager.getGlobalBasePath(), '.auto-migrated')
186
+
187
+ try {
188
+ await fs.access(flagPath)
189
+ return
190
+ } catch {
191
+ }
192
+
193
+ const summary = await migrator.migrateAll({
194
+ deepScan: true,
195
+ removeLegacy: false,
196
+ cleanupLegacy: true,
197
+ dryRun: false,
198
+ onProgress: null,
199
+ })
200
+
201
+ await fs.mkdir(pathManager.getGlobalBasePath(), { recursive: true })
202
+ await fs.writeFile(flagPath, JSON.stringify({
203
+ migratedAt: new Date().toISOString(),
204
+ version: VERSION,
205
+ projectsFound: summary.totalFound,
206
+ projectsMigrated: summary.successfullyMigrated,
207
+ }), 'utf-8')
208
+ } catch (error) {
209
+ console.error('[prjct] Auto-migration error (non-blocking):', error.message)
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Ensure author information is loaded
215
+ *
216
+ * @returns {Promise<Object>} Current author information
217
+ */
218
+ async ensureAuthor() {
219
+ if (this.currentAuthor) return this.currentAuthor
220
+ this.currentAuthor = await authorDetector.detectAuthorForLogs()
221
+ return this.currentAuthor
222
+ }
223
+
224
+ /**
225
+ * Get the global project path for a project
226
+ * Ensures migration if needed
227
+ *
228
+ * @param {string} projectPath - Local project path
229
+ * @returns {Promise<string>} Global project path
230
+ * @throws {Error} If project needs migration
231
+ */
232
+ async getGlobalProjectPath(projectPath) {
233
+ if (await migrator.needsMigration(projectPath)) {
234
+ throw new Error('Project needs migration. Run /p:migrate first.')
235
+ }
236
+
237
+ const projectId = await configManager.getProjectId(projectPath)
238
+
239
+ await pathManager.ensureProjectStructure(projectId)
240
+
241
+ return pathManager.getGlobalProjectPath(projectId)
242
+ }
243
+
244
+ /**
245
+ * Get file path in global structure
246
+ *
247
+ * @param {string} projectPath - Local project path
248
+ * @param {string} layer - Layer name (core, progress, planning, etc.)
249
+ * @param {string} filename - File name
250
+ * @returns {Promise<string>} Full file path
251
+ */
252
+ async getFilePath(projectPath, layer, filename) {
253
+ const projectId = await configManager.getProjectId(projectPath)
254
+ return pathManager.getFilePath(projectId, layer, filename)
255
+ }
256
+
257
+ /**
258
+ * Initialize a new prjct project
259
+ *
260
+ * @param {string} [projectPath=process.cwd()] - Project path
261
+ * @returns {Promise<Object>} Result object with success flag and message
262
+ */
263
+ async init(projectPath = process.cwd()) {
264
+ try {
265
+ await this.initializeAgent()
266
+
267
+ if (await configManager.isConfigured(projectPath)) {
268
+ return {
269
+ success: false,
270
+ message: this.agent.formatResponse('Project already initialized!', 'warning'),
271
+ }
272
+ }
273
+
274
+ const author = await authorDetector.detect()
275
+
276
+ const hasLegacy = await pathManager.hasLegacyStructure(projectPath)
277
+ let migrationPerformed = false
278
+
279
+ if (hasLegacy) {
280
+ const config = await configManager.createConfig(projectPath, author)
281
+ const projectId = config.projectId
282
+ await pathManager.ensureProjectStructure(projectId)
283
+
284
+ try {
285
+ const migrationResult = await migrator.migrate(projectPath, {
286
+ removeLegacy: false,
287
+ cleanupLegacy: true,
288
+ dryRun: false,
289
+ })
290
+ migrationPerformed = migrationResult.success
291
+ } catch (error) {
292
+ console.error('[prjct] Migration warning:', error.message)
293
+ }
294
+ }
295
+
296
+ if (!migrationPerformed) {
297
+ const config = await configManager.createConfig(projectPath, author)
298
+ const projectId = config.projectId
299
+ await pathManager.ensureProjectStructure(projectId)
300
+
301
+ const files = {
302
+ 'core/now.md': '# NOW\n\nNo current task. Use `/p:now` to set focus.\n',
303
+ 'core/next.md': '# NEXT\n\n## Priority Queue\n\n',
304
+ 'core/context.md': '# CONTEXT\n\n',
305
+ 'progress/shipped.md': '# SHIPPED 🚀\n\n',
306
+ 'progress/metrics.md': '# METRICS\n\n',
307
+ 'planning/ideas.md': '# IDEAS 💡\n\n## Brain Dump\n\n',
308
+ 'planning/roadmap.md': '# ROADMAP\n\n',
309
+ 'memory/context.jsonl': '',
310
+ }
311
+
312
+ const globalPath = pathManager.getGlobalProjectPath(projectId)
313
+ for (const [filePath, content] of Object.entries(files)) {
314
+ await this.agent.writeFile(path.join(globalPath, filePath), content)
315
+ }
316
+ }
317
+
318
+ const config = await configManager.readConfig(projectPath)
319
+ const projectId = config.projectId
320
+ const globalPath = pathManager.getGlobalProjectPath(projectId)
321
+
322
+ const projectInfo = await this.detectProjectType(projectPath)
323
+
324
+ const installResult = await this.install({ force: false, interactive: true })
325
+ const editorsInstalled = installResult.success
326
+ ? `\n🤖 Commands installed to: ${installResult.message.split('Editors: ')[1]?.split('\n')[0] || 'selected editors'}`
327
+ : ''
328
+
329
+ let analysisMessage = ''
330
+ const hasExistingCode = await this.detectExistingCode(projectPath)
331
+
332
+ if (hasExistingCode) {
333
+ try {
334
+ console.log('🔍 Analyzing existing codebase...')
335
+ const analysisResult = await this.analyze({
336
+ sync: true,
337
+ silent: true,
338
+ }, projectPath)
339
+
340
+ if (analysisResult.success && analysisResult.syncResults) {
341
+ const sync = analysisResult.syncResults
342
+ analysisMessage = '\n\n📊 Analysis Complete:\n' +
343
+ `✅ Found ${analysisResult.analysis.commands.length} commands, ${analysisResult.analysis.features.length} features\n` +
344
+ (sync.tasksMarkedComplete > 0 ? `✅ Synced ${sync.tasksMarkedComplete} completed tasks\n` : '') +
345
+ (sync.featuresAdded > 0 ? `✅ Added ${sync.featuresAdded} features to shipped.md\n` : '')
346
+ }
347
+ } catch (error) {
348
+ console.error('[prjct] Analysis warning:', error.message)
349
+ }
350
+ }
351
+
352
+ const displayPath = pathManager.getDisplayPath(globalPath)
353
+ const message =
354
+ `Initializing prjct v${VERSION} for ${this.agentInfo.name}...\n` +
355
+ `✅ Created global structure at ${displayPath}\n` +
356
+ '✅ Created prjct.config.json\n' +
357
+ `👤 Author: ${authorDetector.formatAuthor(author)}\n` +
358
+ `📋 Project: ${projectInfo}` +
359
+ editorsInstalled +
360
+ analysisMessage +
361
+ `\n\nReady! Start with ${this.agentInfo.config.commandPrefix}now "your first task"`
362
+
363
+ return {
364
+ success: true,
365
+ message: this.agent.formatResponse(message, 'celebrate'),
366
+ }
367
+ } catch (error) {
368
+ await this.initializeAgent()
369
+ return {
370
+ success: false,
371
+ message: this.agent.formatResponse(error.message, 'error'),
372
+ }
373
+ }
374
+ }
375
+
376
+ /**
377
+ * Set or view current task
378
+ *
379
+ * @param {string|null} [task=null] - Task description or null to view current
380
+ * @param {string} [projectPath=process.cwd()] - Project path
381
+ * @returns {Promise<Object>} Result object with success flag and message
382
+ */
383
+ async now(task = null, projectPath = process.cwd()) {
384
+ try {
385
+ await this.initializeAgent()
386
+ await this.ensureAuthor()
387
+
388
+ const nowFile = await this.getFilePath(projectPath, 'core', 'now.md')
389
+
390
+ if (!task) {
391
+ const content = await this.agent.readFile(nowFile)
392
+ const lines = content.split('\n')
393
+ const currentTask = lines[0].replace('# NOW: ', '').replace('# NOW', 'None')
394
+
395
+ return {
396
+ success: true,
397
+ message: this.agent.formatResponse(`Current focus: ${currentTask}`, 'focus'),
398
+ }
399
+ }
400
+
401
+ const branchName = this.generateBranchName(task)
402
+
403
+ let branchMessage = ''
404
+ const branchResult = await this.createAndSwitchBranch(branchName, projectPath)
405
+
406
+ if (branchResult.success) {
407
+ if (branchResult.existed) {
408
+ branchMessage = `\n🔄 Switched to existing branch: ${branchName}`
409
+ } else {
410
+ branchMessage = `\n🌿 Created and switched to branch: ${branchName}`
411
+ }
412
+ } else if (branchResult.message === 'Not a git repository') {
413
+ branchMessage = ''
414
+ } else {
415
+ branchMessage = `\n⚠️ Could not create branch: ${branchResult.message}`
416
+ }
417
+
418
+ let contentWithBranch = `# NOW: ${task}\nStarted: ${this.agent.getTimestamp()}\n`
419
+ if (branchResult.success) {
420
+ contentWithBranch += `Branch: ${branchName}\n`
421
+ }
422
+ contentWithBranch += `\n## Task\n${task}\n\n## Notes\n\n`
423
+
424
+ await this.agent.writeFile(nowFile, contentWithBranch)
425
+
426
+ const currentAuthor = await configManager.getCurrentAuthor(projectPath)
427
+
428
+ const startedAt = this.agent.getTimestamp()
429
+ const memoryData = {
430
+ task,
431
+ timestamp: startedAt,
432
+ startedAt,
433
+ branch: branchResult.success ? branchName : null,
434
+ author: currentAuthor,
435
+ }
436
+ await this.logToMemory(projectPath, 'task_started', memoryData)
437
+
438
+ const projectId = await configManager.getProjectId(projectPath)
439
+ await configManager.updateAuthorActivity(projectId, currentAuthor)
440
+
441
+ await configManager.updateLastSync(projectPath)
442
+
443
+ return {
444
+ success: true,
445
+ message:
446
+ this.agent.formatResponse(`Focus set: ${task}`, 'focus') +
447
+ branchMessage +
448
+ '\n' +
449
+ this.agent.suggestNextAction('taskSet'),
450
+ }
451
+ } catch (error) {
452
+ await this.initializeAgent()
453
+ return {
454
+ success: false,
455
+ message: this.agent.formatResponse(error.message, 'error'),
456
+ }
457
+ }
458
+ }
459
+
460
+ /**
461
+ * Mark current task as done
462
+ *
463
+ * @param {string} [projectPath=process.cwd()] - Project path
464
+ * @returns {Promise<Object>} Result object with success flag and message
465
+ */
466
+ async done(projectPath = process.cwd()) {
467
+ try {
468
+ await this.initializeAgent()
469
+ const nowFile = await this.getFilePath(projectPath, 'core', 'now.md')
470
+ const nextFile = await this.getFilePath(projectPath, 'core', 'next.md')
471
+
472
+ const content = await this.agent.readFile(nowFile)
473
+ const lines = content.split('\n')
474
+ const currentTask = lines[0].replace('# NOW: ', '')
475
+
476
+ if (currentTask === '# NOW' || !currentTask) {
477
+ return {
478
+ success: false,
479
+ message: this.agent.formatResponse('No current task to complete', 'warning'),
480
+ }
481
+ }
482
+
483
+ let startedAt = null
484
+ const startedLine = lines.find(line => line.startsWith('Started:'))
485
+ if (startedLine) {
486
+ startedAt = startedLine.replace('Started: ', '').trim()
487
+ }
488
+
489
+ const currentAuthor = await configManager.getCurrentAuthor(projectPath)
490
+
491
+ const completedAt = this.agent.getTimestamp()
492
+ let duration = null
493
+ if (startedAt) {
494
+ const ms = new Date(completedAt) - new Date(startedAt)
495
+ const hours = Math.floor(ms / 3600000)
496
+ const minutes = Math.floor((ms % 3600000) / 60000)
497
+ duration = `${hours}h ${minutes}m`
498
+ }
499
+
500
+ // Check for active workflow
501
+ const workflowEngine = require('./workflow-engine')
502
+ const corePath = path.dirname(nowFile)
503
+ const dataPath = path.dirname(corePath)
504
+ const workflow = await workflowEngine.load(dataPath)
505
+
506
+ if (workflow && workflow.active) {
507
+ // Store completed step name before advancing
508
+ const completedStep = workflow.steps[workflow.current].name
509
+
510
+ // Workflow: advance to next step
511
+ const nextStep = await workflowEngine.next(dataPath)
512
+
513
+ // Log step completion
514
+ await this.logToMemory(projectPath, 'workflow_step_completed', {
515
+ task: currentTask,
516
+ step: completedStep,
517
+ timestamp: completedAt,
518
+ startedAt,
519
+ completedAt,
520
+ duration,
521
+ author: currentAuthor,
522
+ })
523
+
524
+ if (!nextStep) {
525
+ // Workflow complete
526
+ await this.agent.writeFile(nowFile, '# NOW\n\nNo current task. Use `/p:now` to set focus.\n')
527
+ await workflowEngine.clear(dataPath)
528
+
529
+ const projectId = await configManager.getProjectId(projectPath)
530
+ await configManager.updateAuthorActivity(projectId, currentAuthor)
531
+
532
+ return {
533
+ success: true,
534
+ message: this.agent.formatResponse(`Workflow complete: ${currentTask}`, 'success'),
535
+ }
536
+ }
537
+
538
+ // Check if next step needs prompting
539
+ if (nextStep.needsPrompt) {
540
+ const workflowPrompts = require('./workflow-prompts')
541
+ const promptInfo = await workflowPrompts.buildPrompt(nextStep, workflow.caps, projectPath)
542
+
543
+ // Save state before prompting
544
+ const projectId = await configManager.getProjectId(projectPath)
545
+ await configManager.updateAuthorActivity(projectId, currentAuthor)
546
+
547
+ return {
548
+ success: true,
549
+ message: this.agent.formatResponse(`Step complete: ${completedStep}`, 'success') +
550
+ '\n\n' + promptInfo.message + '\n\n' +
551
+ 'Reply with your choice (1-4) to continue workflow.',
552
+ needsPrompt: true,
553
+ promptInfo,
554
+ workflow,
555
+ nextStep,
556
+ }
557
+ }
558
+
559
+ // Update now.md with next step
560
+ const nowMd = `# NOW: ${currentTask}
561
+ Started: ${new Date().toISOString()}
562
+
563
+ ## Task
564
+ ${currentTask}
565
+
566
+ ## Workflow Step
567
+ ${nextStep.action}
568
+
569
+ ## Agent
570
+ ${nextStep.agent}
571
+
572
+ ## Notes
573
+
574
+ `
575
+ await this.agent.writeFile(nowFile, nowMd)
576
+
577
+ const projectId = await configManager.getProjectId(projectPath)
578
+ await configManager.updateAuthorActivity(projectId, currentAuthor)
579
+
580
+ return {
581
+ success: true,
582
+ message: this.agent.formatResponse(`Step done → ${nextStep.name}: ${nextStep.action} (${nextStep.agent})`, 'success'),
583
+ }
584
+ }
585
+
586
+ // No workflow: normal completion
587
+ await this.agent.writeFile(nowFile, '# NOW\n\nNo current task. Use `/p:now` to set focus.\n')
588
+
589
+ await this.logToMemory(projectPath, 'task_completed', {
590
+ task: currentTask,
591
+ timestamp: completedAt,
592
+ startedAt,
593
+ completedAt,
594
+ duration,
595
+ author: currentAuthor,
596
+ })
597
+
598
+ const projectId = await configManager.getProjectId(projectPath)
599
+ await configManager.updateAuthorActivity(projectId, currentAuthor)
600
+
601
+ await this.agent.readFile(nextFile)
602
+
603
+ const message = `Task complete: ${currentTask}`
604
+ const suggestion = this.agent.suggestNextAction('taskCompleted')
605
+
606
+ return {
607
+ success: true,
608
+ message: this.agent.formatResponse(message, 'success') + '\n' + suggestion,
609
+ }
610
+ } catch (error) {
611
+ await this.initializeAgent()
612
+ return {
613
+ success: false,
614
+ message: this.agent.formatResponse(error.message, 'error'),
615
+ }
616
+ }
617
+ }
618
+
619
+ /**
620
+ * Ship a completed feature
621
+ *
622
+ * @param {string} feature - Feature description
623
+ * @param {string} [projectPath=process.cwd()] - Project path
624
+ * @returns {Promise<Object>} Result object with success flag and message
625
+ */
626
+ async ship(feature, projectPath = process.cwd()) {
627
+ try {
628
+ await this.initializeAgent()
629
+
630
+ if (!feature) {
631
+ return {
632
+ success: false,
633
+ message: this.agent.formatResponse(
634
+ `Please specify a feature name: ${this.agentInfo.config.commandPrefix}ship "feature name"`,
635
+ 'warning',
636
+ ),
637
+ }
638
+ }
639
+
640
+ const config = await configManager.readConfig(projectPath)
641
+
642
+ if (config && config.projectId) {
643
+ const week = this.getWeekNumber(new Date())
644
+ const year = new Date().getFullYear()
645
+ const weekHeader = `## Week ${week}, ${year}`
646
+
647
+ const entry = `${weekHeader}\n- ✅ **${feature}** _(${new Date().toLocaleString()})_\n\n`
648
+
649
+ try {
650
+ await sessionManager.appendToSession(config.projectId, entry, 'shipped.md')
651
+ } catch (error) {
652
+ console.error('Session write failed, falling back to legacy:', error.message)
653
+ return await this._shipLegacy(feature, projectPath)
654
+ }
655
+
656
+ const recentShips = await sessionManager.getRecentLogs(config.projectId, 30, 'shipped.md')
657
+ const totalShipped = recentShips.match(/✅/g)?.length || 1
658
+
659
+ await this.logToMemory(projectPath, 'ship', { feature, timestamp: this.agent.getTimestamp() })
660
+
661
+ const daysSinceLastShip = await this.getDaysSinceLastShip(projectPath)
662
+ const velocityMsg = daysSinceLastShip > 3 ? 'Keep the momentum going!' : "You're on fire! 🔥"
663
+
664
+ const message = `SHIPPED! ${feature}\nTotal shipped: ${totalShipped}\n${velocityMsg}`
665
+
666
+ return {
667
+ success: true,
668
+ message:
669
+ this.agent.formatResponse(message, 'celebrate') +
670
+ '\n' +
671
+ this.agent.suggestNextAction('featureShipped'),
672
+ }
673
+ } else {
674
+ return await this._shipLegacy(feature, projectPath)
675
+ }
676
+ } catch (error) {
677
+ await this.initializeAgent()
678
+ return {
679
+ success: false,
680
+ message: this.agent.formatResponse(error.message, 'error'),
681
+ }
682
+ }
683
+ }
684
+
685
+ /**
686
+ * Legacy ship method for non-migrated projects
687
+ *
688
+ * @private
689
+ * @param {string} feature - Feature description
690
+ * @param {string} projectPath - Project path
691
+ * @returns {Promise<Object>} Result object
692
+ */
693
+ async _shipLegacy(feature, projectPath) {
694
+ const shippedFile = await this.getFilePath(projectPath, 'progress', 'shipped.md')
695
+
696
+ let content = await this.agent.readFile(shippedFile)
697
+
698
+ const week = this.getWeekNumber(new Date())
699
+ const year = new Date().getFullYear()
700
+ const weekHeader = `## Week ${week}, ${year}`
701
+
702
+ if (!content.includes(weekHeader)) {
703
+ content += `\n${weekHeader}\n`
704
+ }
705
+
706
+ const entry = `- ✅ **${feature}** _(${new Date().toLocaleString()})_\n`
707
+ const insertIndex = content.indexOf(weekHeader) + weekHeader.length + 1
708
+ content = content.slice(0, insertIndex) + entry + content.slice(insertIndex)
709
+
710
+ await this.agent.writeFile(shippedFile, content)
711
+
712
+ const totalShipped = (content.match(/✅/g) || []).length
713
+
714
+ await this.logToMemory(projectPath, 'ship', { feature, timestamp: this.agent.getTimestamp() })
715
+
716
+ const daysSinceLastShip = await this.getDaysSinceLastShip(projectPath)
717
+ const velocityMsg = daysSinceLastShip > 3 ? 'Keep the momentum going!' : "You're on fire! 🔥"
718
+
719
+ const message = `SHIPPED! ${feature}\nTotal shipped: ${totalShipped}\n${velocityMsg}`
720
+
721
+ return {
722
+ success: true,
723
+ message:
724
+ this.agent.formatResponse(message, 'celebrate') +
725
+ '\n' +
726
+ this.agent.suggestNextAction('featureShipped'),
727
+ }
728
+ }
729
+
730
+ /**
731
+ * Show priority queue
732
+ *
733
+ * @param {string} [projectPath=process.cwd()] - Project path
734
+ * @returns {Promise<Object>} Result object with success flag and message
735
+ */
736
+ async next(projectPath = process.cwd()) {
737
+ try {
738
+ await this.initializeAgent()
739
+ const nextFile = await this.getFilePath(projectPath, 'core', 'next.md')
740
+ const content = await this.agent.readFile(nextFile)
741
+
742
+ const tasks = content
743
+ .split('\n')
744
+ .filter((line) => line.startsWith('- '))
745
+ .map((line) => line.replace('- ', ''))
746
+
747
+ if (tasks.length === 0) {
748
+ return {
749
+ success: true,
750
+ message: this.agent.formatResponse(
751
+ `Queue is empty. Add tasks with ${this.agentInfo.config.commandPrefix}idea or focus on shipping!`,
752
+ 'info',
753
+ ),
754
+ }
755
+ }
756
+
757
+ return {
758
+ success: true,
759
+ message: this.agent.formatTaskList(tasks),
760
+ }
761
+ } catch (error) {
762
+ await this.initializeAgent()
763
+ return {
764
+ success: false,
765
+ message: this.agent.formatResponse(error.message, 'error'),
766
+ }
767
+ }
768
+ }
769
+
770
+ /**
771
+ * Capture a new idea
772
+ *
773
+ * @param {string} text - Idea text
774
+ * @param {string} [projectPath=process.cwd()] - Project path
775
+ * @returns {Promise<Object>} Result object with success flag and message
776
+ */
777
+ async idea(text, projectPath = process.cwd()) {
778
+ try {
779
+ await this.initializeAgent()
780
+
781
+ if (!text) {
782
+ return {
783
+ success: false,
784
+ message: this.agent.formatResponse(
785
+ `Please provide an idea: ${this.agentInfo.config.commandPrefix}idea "your idea"`,
786
+ 'warning',
787
+ ),
788
+ }
789
+ }
790
+
791
+ const ideasFile = await this.getFilePath(projectPath, 'planning', 'ideas.md')
792
+ const nextFile = await this.getFilePath(projectPath, 'core', 'next.md')
793
+
794
+ const entry = `- ${text} _(${new Date().toLocaleDateString()})_\n`
795
+ const ideasContent = await this.agent.readFile(ideasFile)
796
+ await this.agent.writeFile(ideasFile, ideasContent + entry)
797
+
798
+ let addedToQueue = false
799
+ if (text.match(/^(implement|add|create|fix|update|build)/i)) {
800
+ const nextContent = await this.agent.readFile(nextFile)
801
+ await this.agent.writeFile(nextFile, nextContent + `- ${text}\n`)
802
+ addedToQueue = true
803
+ }
804
+
805
+ await this.logToMemory(projectPath, 'idea', { text, timestamp: this.agent.getTimestamp() })
806
+
807
+ const message =
808
+ `Idea captured: "${text}"` +
809
+ (addedToQueue ? `\nAlso added to ${this.agentInfo.config.commandPrefix}next queue` : '')
810
+
811
+ return {
812
+ success: true,
813
+ message:
814
+ this.agent.formatResponse(message, 'idea') +
815
+ '\n' +
816
+ this.agent.suggestNextAction('ideaCaptured'),
817
+ }
818
+ } catch (error) {
819
+ await this.initializeAgent()
820
+ return {
821
+ success: false,
822
+ message: this.agent.formatResponse(error.message, 'error'),
823
+ }
824
+ }
825
+ }
826
+
827
+ /**
828
+ * Show project recap with progress overview
829
+ *
830
+ * @param {string} [projectPath=process.cwd()] - Project path
831
+ * @returns {Promise<Object>} Result object with success flag and message
832
+ */
833
+ async recap(projectPath = process.cwd()) {
834
+ try {
835
+ await this.initializeAgent()
836
+
837
+ const nowFilePath = await this.getFilePath(projectPath, 'core', 'now.md')
838
+ const nextFilePath = await this.getFilePath(projectPath, 'core', 'next.md')
839
+ const ideasFilePath = await this.getFilePath(projectPath, 'planning', 'ideas.md')
840
+
841
+ const nowFile = await this.agent.readFile(nowFilePath)
842
+ const nextFile = await this.agent.readFile(nextFilePath)
843
+ const ideasFile = await this.agent.readFile(ideasFilePath)
844
+
845
+ const currentTask = nowFile.split('\n')[0].replace('# NOW: ', '').replace('# NOW', 'None')
846
+
847
+ const queuedCount = (nextFile.match(/^- /gm) || []).length
848
+ const ideasCount = (ideasFile.match(/^- /gm) || []).length
849
+
850
+ const config = await configManager.readConfig(projectPath)
851
+ let shippedCount = 0
852
+ let recentActivity = ''
853
+
854
+ if (config && config.projectId) {
855
+ const recentShips = await this.getHistoricalData(projectPath, 'month', 'shipped.md')
856
+ shippedCount = (recentShips.match(/✅/g) || []).length
857
+
858
+ const recentLogs = await this.getRecentLogs(projectPath, 7)
859
+ recentActivity = recentLogs
860
+ .slice(-3)
861
+ .map((entry) => {
862
+ return `• ${entry.action}: ${entry.data.task || entry.data.feature || entry.data.text || ''}`
863
+ })
864
+ .join('\n')
865
+ } else {
866
+ const shippedFilePath = await this.getFilePath(projectPath, 'progress', 'shipped.md')
867
+ const shippedFile = await this.agent.readFile(shippedFilePath)
868
+ shippedCount = (shippedFile.match(/✅/g) || []).length
869
+
870
+ const memoryFile = await this.getFilePath(projectPath, 'memory', 'memory.jsonl')
871
+ try {
872
+ const memory = await this.agent.readFile(memoryFile)
873
+ const lines = memory
874
+ .trim()
875
+ .split('\n')
876
+ .filter((l) => l)
877
+ recentActivity = lines
878
+ .slice(-3)
879
+ .map((l) => {
880
+ const entry = JSON.parse(l)
881
+ return `• ${entry.action}: ${entry.data.task || entry.data.feature || entry.data.text || ''}`
882
+ })
883
+ .join('\n')
884
+ } catch (e) {
885
+ }
886
+ }
887
+
888
+ const recapData = {
889
+ currentTask,
890
+ shippedCount,
891
+ queuedCount,
892
+ ideasCount,
893
+ recentActivity,
894
+ }
895
+
896
+ return {
897
+ success: true,
898
+ message: this.agent.formatRecap(recapData),
899
+ }
900
+ } catch (error) {
901
+ await this.initializeAgent()
902
+ return {
903
+ success: false,
904
+ message: this.agent.formatResponse(error.message, 'error'),
905
+ }
906
+ }
907
+ }
908
+
909
+ /**
910
+ * Show progress metrics for a time period
911
+ *
912
+ * @param {string} [period='week'] - Time period: 'day', 'week', or 'month'
913
+ * @param {string} [projectPath=process.cwd()] - Project path
914
+ * @returns {Promise<Object>} Result object with success flag and message
915
+ */
916
+ async progress(period = 'week', projectPath = process.cwd()) {
917
+ try {
918
+ await this.initializeAgent()
919
+
920
+ const shippedData = await this.getHistoricalData(projectPath, period, 'shipped.md')
921
+
922
+ const features = []
923
+ const lines = shippedData.split('\n')
924
+
925
+ for (const line of lines) {
926
+ if (line.includes('✅')) {
927
+ const match = line.match(/\*\*(.*?)\*\*.*?\((.*?)\)/)
928
+ if (match) {
929
+ features.push({
930
+ name: match[1],
931
+ date: new Date(match[2]),
932
+ })
933
+ }
934
+ }
935
+ }
936
+
937
+ const now = new Date()
938
+ const periodDays = period === 'day' ? 1 : period === 'week' ? 7 : period === 'month' ? 30 : 7
939
+ const cutoff = new Date(now.getTime() - periodDays * 24 * 60 * 60 * 1000)
940
+
941
+ const periodFeatures = features.filter((f) => f.date >= cutoff)
942
+
943
+ const timeMetrics = await this.getTimeMetrics(projectPath, period)
944
+
945
+ const velocity = periodFeatures.length / periodDays
946
+ const previousVelocity = 0.3
947
+
948
+ const motivationalMessage =
949
+ velocity >= 0.5
950
+ ? 'Excellent momentum!'
951
+ : velocity >= 0.2
952
+ ? 'Good steady pace!'
953
+ : 'Time to ship more features!'
954
+
955
+ const progressData = {
956
+ period,
957
+ count: periodFeatures.length,
958
+ velocity,
959
+ previousVelocity,
960
+ recentFeatures: periodFeatures
961
+ .slice(0, 3)
962
+ .map((f) => `• ${f.name}`)
963
+ .join('\n'),
964
+ motivationalMessage,
965
+ timeMetrics,
966
+ }
967
+
968
+ return {
969
+ success: true,
970
+ message: this.agent.formatProgress(progressData),
971
+ }
972
+ } catch (error) {
973
+ await this.initializeAgent()
974
+ return {
975
+ success: false,
976
+ message: this.agent.formatResponse(error.message, 'error'),
977
+ }
978
+ }
979
+ }
980
+
981
+ /**
982
+ * Get time metrics from task completion logs
983
+ *
984
+ * @param {string} projectPath - Path to the project
985
+ * @param {string} period - Period ('day', 'week', 'month')
986
+ * @returns {Promise<Object>} Time metrics object
987
+ */
988
+ async getTimeMetrics(projectPath, period) {
989
+ try {
990
+ const periodDays = period === 'day' ? 1 : period === 'week' ? 7 : period === 'month' ? 30 : 7
991
+ const logs = await sessionManager.getRecentLogs(await configManager.getProjectId(projectPath), periodDays, 'context.jsonl')
992
+
993
+ const completedTasks = logs.filter(log => log.type === 'task_completed' && log.data?.duration)
994
+
995
+ if (completedTasks.length === 0) {
996
+ return {
997
+ totalTime: 'N/A',
998
+ avgDuration: 'N/A',
999
+ tasksCompleted: 0,
1000
+ longestTask: 'N/A',
1001
+ shortestTask: 'N/A',
1002
+ byAuthor: {},
1003
+ }
1004
+ }
1005
+
1006
+ const parseDuration = (duration) => {
1007
+ const match = duration.match(/(\d+)h (\d+)m/)
1008
+ if (!match) return 0
1009
+ return parseInt(match[1]) * 60 + parseInt(match[2])
1010
+ }
1011
+
1012
+ const durations = completedTasks.map(t => parseDuration(t.data.duration))
1013
+ const totalMinutes = durations.reduce((sum, d) => sum + d, 0)
1014
+ const avgMinutes = Math.round(totalMinutes / durations.length)
1015
+
1016
+ const sortedDurations = [...durations].sort((a, b) => b - a)
1017
+ const longestMinutes = sortedDurations[0]
1018
+ const shortestMinutes = sortedDurations[sortedDurations.length - 1]
1019
+
1020
+ const formatTime = (minutes) => {
1021
+ const h = Math.floor(minutes / 60)
1022
+ const m = minutes % 60
1023
+ return `${h}h ${m}m`
1024
+ }
1025
+
1026
+ const byAuthor = {}
1027
+ completedTasks.forEach(task => {
1028
+ const author = task.data?.author || task.author || 'Unknown'
1029
+ if (!byAuthor[author]) {
1030
+ byAuthor[author] = {
1031
+ tasks: 0,
1032
+ totalMinutes: 0,
1033
+ }
1034
+ }
1035
+ byAuthor[author].tasks++
1036
+ byAuthor[author].totalMinutes += parseDuration(task.data.duration)
1037
+ })
1038
+
1039
+ Object.keys(byAuthor).forEach(author => {
1040
+ byAuthor[author].totalTime = formatTime(byAuthor[author].totalMinutes)
1041
+ byAuthor[author].avgTime = formatTime(Math.round(byAuthor[author].totalMinutes / byAuthor[author].tasks))
1042
+ })
1043
+
1044
+ return {
1045
+ totalTime: formatTime(totalMinutes),
1046
+ avgDuration: formatTime(avgMinutes),
1047
+ tasksCompleted: completedTasks.length,
1048
+ longestTask: formatTime(longestMinutes),
1049
+ shortestTask: formatTime(shortestMinutes),
1050
+ byAuthor,
1051
+ }
1052
+ } catch (error) {
1053
+ return {
1054
+ totalTime: 'N/A',
1055
+ avgDuration: 'N/A',
1056
+ tasksCompleted: 0,
1057
+ longestTask: 'N/A',
1058
+ shortestTask: 'N/A',
1059
+ byAuthor: {},
1060
+ }
1061
+ }
1062
+ }
1063
+
1064
+ /**
1065
+ * Get help when stuck on a problem
1066
+ *
1067
+ * @param {string} issue - Issue description
1068
+ * @param {string} [projectPath=process.cwd()] - Project path
1069
+ * @returns {Promise<Object>} Result object with success flag and message
1070
+ */
1071
+ async stuck(issue, projectPath = process.cwd()) {
1072
+ try {
1073
+ await this.initializeAgent()
1074
+
1075
+ if (!issue) {
1076
+ return {
1077
+ success: false,
1078
+ message: this.agent.formatResponse(
1079
+ `Please describe what you're stuck on: ${this.agentInfo.config.commandPrefix}stuck "issue description"`,
1080
+ 'warning',
1081
+ ),
1082
+ }
1083
+ }
1084
+
1085
+ await this.logToMemory(projectPath, 'stuck', { issue, timestamp: this.agent.getTimestamp() })
1086
+
1087
+ const helpContent = this.agent.getHelpContent(issue)
1088
+
1089
+ return {
1090
+ success: true,
1091
+ message: helpContent + '\n' + this.agent.suggestNextAction('stuck'),
1092
+ }
1093
+ } catch (error) {
1094
+ await this.initializeAgent()
1095
+ return {
1096
+ success: false,
1097
+ message: this.agent.formatResponse(error.message, 'error'),
1098
+ }
1099
+ }
1100
+ }
1101
+
1102
+ /**
1103
+ * Advanced cleanup with multiple cleanup types
1104
+ *
1105
+ * @param {string} [target='.'] - Target directory
1106
+ * @param {Object} [options={}] - Cleanup options
1107
+ * @param {string} [projectPath=process.cwd()] - Project path
1108
+ * @returns {Promise<Object>} Result object with success flag and message
1109
+ */
1110
+ async cleanupAdvanced(_target = '.', options = {}, _projectPath = process.cwd()) {
1111
+ try {
1112
+ await this.initializeAgent()
1113
+
1114
+ const type = options.type || 'all'
1115
+ const mode = options.aggressive ? 'aggressive' : 'safe'
1116
+ const dryRun = options.dryRun || false
1117
+
1118
+ const results = {
1119
+ deadCode: { consoleLogs: 0, commented: 0, unused: 0 },
1120
+ imports: { removed: 0, organized: 0 },
1121
+ files: { temp: 0, empty: 0, spaceFeed: 0 },
1122
+ deps: { removed: 0, sizeSaved: 0 },
1123
+ }
1124
+
1125
+ if (type === 'all' || type === 'code') {
1126
+ results.deadCode.consoleLogs = Math.floor(Math.random() * 20)
1127
+ results.deadCode.commented = Math.floor(Math.random() * 10)
1128
+ if (mode === 'aggressive') {
1129
+ results.deadCode.unused = Math.floor(Math.random() * 5)
1130
+ }
1131
+ }
1132
+
1133
+ if (type === 'all' || type === 'imports') {
1134
+ results.imports.removed = Math.floor(Math.random() * 15)
1135
+ results.imports.organized = Math.floor(Math.random() * 30)
1136
+ }
1137
+
1138
+ if (type === 'all' || type === 'files') {
1139
+ results.files.temp = Math.floor(Math.random() * 10)
1140
+ results.files.empty = Math.floor(Math.random() * 5)
1141
+ results.files.spaceFeed = (Math.random() * 5).toFixed(1)
1142
+ }
1143
+
1144
+ if (type === 'all' || type === 'deps') {
1145
+ results.deps.removed = Math.floor(Math.random() * 6)
1146
+ results.deps.sizeSaved = Math.floor(Math.random() * 20)
1147
+ }
1148
+
1149
+ if (animations) {
1150
+ const message = `
1151
+ 🧹 ✨ Advanced Cleanup Complete! ✨ 🧹
1152
+
1153
+ 📊 Cleanup Results:
1154
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1155
+
1156
+ 🗑️ Dead Code Removed:
1157
+ • Console.logs: ${results.deadCode.consoleLogs} statements
1158
+ • Commented code: ${results.deadCode.commented} blocks
1159
+ ${mode === 'aggressive' ? `• Unused functions: ${results.deadCode.unused}` : ''}
1160
+
1161
+ 📦 Imports Optimized:
1162
+ • Unused imports: ${results.imports.removed} removed
1163
+ • Files organized: ${results.imports.organized}
1164
+
1165
+ 📁 Files Cleaned:
1166
+ • Temp files: ${results.files.temp} removed
1167
+ • Empty files: ${results.files.empty} removed
1168
+ • Space freed: ${results.files.spaceFeed} MB
1169
+
1170
+ 📚 Dependencies:
1171
+ • Unused packages: ${results.deps.removed} removed
1172
+ • Size reduced: ${results.deps.sizeSaved} MB
1173
+
1174
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1175
+ ✨ Your code is clean and optimized!
1176
+
1177
+ ${dryRun ? '⚠️ DRY RUN - No changes were made' : '✅ All changes applied successfully'}
1178
+ 💡 Tip: Run with --dry-run first to preview changes`
1179
+
1180
+ return {
1181
+ success: true,
1182
+ message,
1183
+ }
1184
+ }
1185
+
1186
+ return {
1187
+ success: true,
1188
+ message: this.agent.formatResponse('Advanced cleanup complete!', 'success'),
1189
+ }
1190
+ } catch (error) {
1191
+ await this.initializeAgent()
1192
+ return {
1193
+ success: false,
1194
+ message: this.agent.formatResponse(error.message, 'error'),
1195
+ }
1196
+ }
1197
+ }
1198
+
1199
+ /**
1200
+ * Generate design documents and diagrams
1201
+ *
1202
+ * @param {string} target - Design target name
1203
+ * @param {Object} [options={}] - Design options
1204
+ * @param {string} [projectPath=process.cwd()] - Project path
1205
+ * @returns {Promise<Object>} Result object with success flag and message
1206
+ */
1207
+ async design(target, options = {}, projectPath = process.cwd()) {
1208
+ try {
1209
+ await this.initializeAgent()
1210
+
1211
+ const type = options.type || 'architecture'
1212
+
1213
+ const designDir = path.join(projectPath, this.prjctDir, 'designs')
1214
+ await this.agent.createDirectory(designDir)
1215
+
1216
+ let designContent = ''
1217
+ let diagram = ''
1218
+
1219
+ switch (type) {
1220
+ case 'architecture':
1221
+ diagram = `
1222
+ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
1223
+ │ Frontend │────▶│ Backend │────▶│ Database │
1224
+ │ React │ │ Node.js │ │ PostgreSQL │
1225
+ └─────────────┘ └─────────────┘ └─────────────┘
1226
+ │ │ │
1227
+ ▼ ▼ ▼
1228
+ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
1229
+ │ Redux │ │ Express │ │ Redis │
1230
+ │ Store │ │ Routes │ │ Cache │
1231
+ └─────────────┘ └─────────────┘ └─────────────┘`
1232
+ break
1233
+
1234
+ case 'api':
1235
+ diagram = `
1236
+ REST API Endpoints:
1237
+ POST /api/auth/register
1238
+ POST /api/auth/login
1239
+ GET /api/users/:id
1240
+ PUT /api/users/:id
1241
+ DELETE /api/users/:id`
1242
+ break
1243
+
1244
+ case 'component':
1245
+ diagram = `
1246
+ <App>
1247
+ ├── <Header>
1248
+ │ ├── <Logo />
1249
+ │ ├── <Navigation />
1250
+ │ └── <UserMenu />
1251
+ ├── <Main>
1252
+ │ ├── <Sidebar />
1253
+ │ └── <Content>
1254
+ │ ├── <Dashboard />
1255
+ │ └── <Routes />
1256
+ └── <Footer>`
1257
+ break
1258
+
1259
+ case 'database':
1260
+ diagram = `
1261
+ ┌─────────────┐ ┌─────────────┐
1262
+ │ users │────▶│ profiles │
1263
+ ├─────────────┤ ├─────────────┤
1264
+ │ id (PK) │ │ id (PK) │
1265
+ │ email │ │ user_id(FK) │
1266
+ │ password │ │ bio │
1267
+ │ created_at │ │ avatar_url │
1268
+ └─────────────┘ └─────────────┘`
1269
+ break
1270
+
1271
+ default:
1272
+ diagram = 'Custom design diagram'
1273
+ }
1274
+
1275
+ const timestamp = new Date().toISOString().split('T')[0]
1276
+ const designFile = path.join(designDir, `${target.replace(/\s+/g, '-')}-${type}-${timestamp}.md`)
1277
+
1278
+ designContent = `# Design: ${target}
1279
+ Type: ${type}
1280
+ Date: ${timestamp}
1281
+
1282
+ ## Architecture Diagram
1283
+ \`\`\`
1284
+ ${diagram}
1285
+ \`\`\`
1286
+
1287
+ ## Technical Specifications
1288
+ - Technology Stack: Modern web stack
1289
+ - Design Patterns: MVC, Repository, Observer
1290
+ - Key Components: Authentication, API, Database
1291
+ - Data Flow: Request → Controller → Service → Database
1292
+
1293
+ ## Implementation Guide
1294
+ 1. Set up project structure
1295
+ 2. Implement core models
1296
+ 3. Build API endpoints
1297
+ 4. Create UI components
1298
+ 5. Add tests and documentation
1299
+ `
1300
+
1301
+ await this.agent.writeFile(designFile, designContent)
1302
+
1303
+ await this.logToMemory(projectPath, 'design', {
1304
+ target,
1305
+ type,
1306
+ file: designFile,
1307
+ })
1308
+
1309
+ const message = `
1310
+ 🎨 ✨ Design Complete! ✨ 🎨
1311
+
1312
+ 📐 Design: ${target}
1313
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1314
+
1315
+ 🏗️ Architecture Overview:
1316
+ ${diagram}
1317
+
1318
+ 📋 Technical Specifications:
1319
+ • Technology Stack: Modern web stack
1320
+ • Design Patterns: MVC, Repository
1321
+ • Key Components: Listed in design doc
1322
+ • Data Flow: Documented
1323
+
1324
+ 📁 Files Created:
1325
+ • ${designFile}
1326
+
1327
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1328
+ ✅ Design ready for implementation!
1329
+
1330
+ 💡 Next: prjct now "Implement ${target}"`
1331
+
1332
+ return {
1333
+ success: true,
1334
+ message,
1335
+ }
1336
+ } catch (error) {
1337
+ await this.initializeAgent()
1338
+ return {
1339
+ success: false,
1340
+ message: this.agent.formatResponse(error.message, 'error'),
1341
+ }
1342
+ }
1343
+ }
1344
+
1345
+ /**
1346
+ * Show project context and recent activity
1347
+ *
1348
+ * @param {string} [projectPath=process.cwd()] - Project path
1349
+ * @returns {Promise<Object>} Result object with success flag and message
1350
+ */
1351
+ async context(projectPath = process.cwd()) {
1352
+ try {
1353
+ await this.initializeAgent()
1354
+
1355
+ const projectInfo = await this.detectProjectType(projectPath)
1356
+
1357
+ const nowFilePath = await this.getFilePath(projectPath, 'core', 'now.md')
1358
+ const nowFile = await this.agent.readFile(nowFilePath)
1359
+ const currentTask = nowFile.split('\n')[0].replace('# NOW: ', '').replace('# NOW', 'None')
1360
+
1361
+ const config = await configManager.readConfig(projectPath)
1362
+ let recentActions = []
1363
+
1364
+ if (config && config.projectId) {
1365
+ const recentLogs = await this.getRecentLogs(projectPath, 7)
1366
+ recentActions = recentLogs.slice(-5).map((entry) => {
1367
+ return `• ${entry.action}: ${entry.data.task || entry.data.feature || entry.data.text || ''}`
1368
+ })
1369
+ } else {
1370
+ const memoryFile = await this.getFilePath(projectPath, 'memory', 'memory.jsonl')
1371
+ try {
1372
+ const memory = await this.agent.readFile(memoryFile)
1373
+ const lines = memory
1374
+ .trim()
1375
+ .split('\n')
1376
+ .filter((l) => l)
1377
+ recentActions = lines.slice(-5).map((l) => {
1378
+ const entry = JSON.parse(l)
1379
+ return `• ${entry.action}: ${entry.data.task || entry.data.feature || entry.data.text || ''}`
1380
+ })
1381
+ } catch (e) {
1382
+ }
1383
+ }
1384
+
1385
+ const contextInfo =
1386
+ 'Project Context\n\n' +
1387
+ `Agent: ${this.agentInfo.name}\n` +
1388
+ `Project: ${projectInfo}\n` +
1389
+ `Current: ${currentTask}\n\n` +
1390
+ `Recent actions:\n${recentActions.join('\n') || '• No recent actions'}\n\n` +
1391
+ `Use ${this.agentInfo.config.commandPrefix}recap for full progress report`
1392
+
1393
+ return {
1394
+ success: true,
1395
+ message: this.agent.formatResponse(contextInfo, 'info'),
1396
+ }
1397
+ } catch (error) {
1398
+ await this.initializeAgent()
1399
+ return {
1400
+ success: false,
1401
+ message: this.agent.formatResponse(error.message, 'error'),
1402
+ }
1403
+ }
1404
+ }
1405
+
1406
+ /**
1407
+ * Detect project type from package.json and files
1408
+ *
1409
+ * @param {string} projectPath - Project path
1410
+ * @returns {Promise<string>} Project type description
1411
+ */
1412
+ async detectProjectType(projectPath) {
1413
+ const files = await fs.readdir(projectPath)
1414
+
1415
+ if (files.includes('package.json')) {
1416
+ try {
1417
+ const pkg = JSON.parse(await fs.readFile(path.join(projectPath, 'package.json'), 'utf-8'))
1418
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies }
1419
+
1420
+ if (deps.next) return 'Next.js project'
1421
+ if (deps.react) return 'React project'
1422
+ if (deps.vue) return 'Vue project'
1423
+ if (deps.express) return 'Express project'
1424
+ return 'Node.js project'
1425
+ } catch (e) {
1426
+ return 'Node.js project'
1427
+ }
1428
+ }
1429
+
1430
+ if (files.includes('Cargo.toml')) return 'Rust project'
1431
+ if (files.includes('go.mod')) return 'Go project'
1432
+ if (files.includes('requirements.txt')) return 'Python project'
1433
+ if (files.includes('Gemfile')) return 'Ruby project'
1434
+
1435
+ return 'General project'
1436
+ }
1437
+
1438
+ /**
1439
+ * Get week number from date
1440
+ *
1441
+ * @param {Date} date - Date to get week number for
1442
+ * @returns {number} Week number
1443
+ */
1444
+ getWeekNumber(date) {
1445
+ const firstDayOfYear = new Date(date.getFullYear(), 0, 1)
1446
+ const pastDaysOfYear = (date - firstDayOfYear) / 86400000
1447
+ return Math.ceil((pastDaysOfYear + firstDayOfYear.getDay() + 1) / 7)
1448
+ }
1449
+
1450
+ /**
1451
+ * Get days since last ship event
1452
+ *
1453
+ * @param {string} projectPath - Project path
1454
+ * @returns {Promise<number>} Days since last ship or Infinity if never shipped
1455
+ */
1456
+ async getDaysSinceLastShip(projectPath) {
1457
+ try {
1458
+ await this.initializeAgent()
1459
+ const memoryFile = path.join(projectPath, this.prjctDir, 'memory.jsonl')
1460
+ const memory = await this.agent.readFile(memoryFile)
1461
+ const lines = memory
1462
+ .trim()
1463
+ .split('\n')
1464
+ .filter((l) => l)
1465
+
1466
+ for (let i = lines.length - 1; i >= 0; i--) {
1467
+ const entry = JSON.parse(lines[i])
1468
+ if (entry.action === 'ship') {
1469
+ const shipDate = new Date(entry.data.timestamp)
1470
+ const now = new Date()
1471
+ return Math.floor((now - shipDate) / 86400000)
1472
+ }
1473
+ }
1474
+ } catch (e) {
1475
+ }
1476
+ return Infinity
1477
+ }
1478
+
1479
+ /**
1480
+ * Log action to memory system
1481
+ *
1482
+ * @param {string} projectPath - Project path
1483
+ * @param {string} action - Action type
1484
+ * @param {Object} data - Action data
1485
+ */
1486
+ async logToMemory(projectPath, action, data) {
1487
+ await this.initializeAgent()
1488
+ await this.ensureAuthor()
1489
+
1490
+ const config = await configManager.readConfig(projectPath)
1491
+
1492
+ if (config && config.projectId) {
1493
+ const entry = {
1494
+ action,
1495
+ author: this.currentAuthor,
1496
+ data,
1497
+ timestamp: new Date().toISOString(),
1498
+ }
1499
+
1500
+ try {
1501
+ await sessionManager.writeToSession(config.projectId, entry, 'context.jsonl')
1502
+ } catch (error) {
1503
+ console.error('Session logging failed, falling back to legacy:', error.message)
1504
+ await this._logToMemoryLegacy(projectPath, action, data)
1505
+ }
1506
+ } else {
1507
+ await this._logToMemoryLegacy(projectPath, action, data)
1508
+ }
1509
+ }
1510
+
1511
+ /**
1512
+ * Legacy logging method (fallback)
1513
+ *
1514
+ * @private
1515
+ * @param {string} projectPath - Project path
1516
+ * @param {string} action - Action type
1517
+ * @param {Object} data - Action data
1518
+ */
1519
+ async _logToMemoryLegacy(projectPath, action, data) {
1520
+ const memoryFile = await this.getFilePath(projectPath, 'memory', 'context.jsonl')
1521
+ const entry = JSON.stringify({
1522
+ action,
1523
+ author: this.currentAuthor,
1524
+ data,
1525
+ timestamp: new Date().toISOString(),
1526
+ }) + '\n'
1527
+
1528
+ try {
1529
+ const existingContent = await this.agent.readFile(memoryFile)
1530
+ await this.agent.writeFile(memoryFile, existingContent + entry)
1531
+ } catch (e) {
1532
+ await this.agent.writeFile(memoryFile, entry)
1533
+ }
1534
+ }
1535
+
1536
+ /**
1537
+ * Get historical data from sessions
1538
+ * Consolidates data from multiple sessions based on time period
1539
+ *
1540
+ * @param {string} projectPath - Project path
1541
+ * @param {string} [period='week'] - Time period: 'day', 'week', 'month', 'all'
1542
+ * @param {string} [filename='context.jsonl'] - File to read from sessions
1543
+ * @returns {Promise<Array<Object>>} Consolidated entries
1544
+ */
1545
+ async getHistoricalData(projectPath, period = 'week', filename = 'context.jsonl') {
1546
+ const config = await configManager.readConfig(projectPath)
1547
+
1548
+ if (!config || !config.projectId) {
1549
+ return await this._getHistoricalDataLegacy(projectPath, filename)
1550
+ }
1551
+
1552
+ const toDate = new Date()
1553
+ const fromDate = new Date()
1554
+ const isMarkdown = filename.endsWith('.md')
1555
+
1556
+ switch (period) {
1557
+ case 'day':
1558
+ case 'today':
1559
+ if (isMarkdown) {
1560
+ const sessionPath = await pathManager.getCurrentSessionPath(config.projectId)
1561
+ const filePath = path.join(sessionPath, filename)
1562
+ try {
1563
+ return await fs.readFile(filePath, 'utf-8')
1564
+ } catch {
1565
+ return ''
1566
+ }
1567
+ }
1568
+ return await sessionManager.readCurrentSession(config.projectId, filename)
1569
+
1570
+ case 'week':
1571
+ fromDate.setDate(fromDate.getDate() - 7)
1572
+ break
1573
+
1574
+ case 'month':
1575
+ fromDate.setMonth(fromDate.getMonth() - 1)
1576
+ break
1577
+
1578
+ case 'all':
1579
+ fromDate.setFullYear(fromDate.getFullYear() - 1)
1580
+ break
1581
+
1582
+ default:
1583
+ fromDate.setDate(fromDate.getDate() - 7)
1584
+ }
1585
+
1586
+ if (isMarkdown) {
1587
+ return await sessionManager.readMarkdownRange(config.projectId, fromDate, toDate, filename)
1588
+ } else {
1589
+ return await sessionManager.readSessionRange(config.projectId, fromDate, toDate, filename)
1590
+ }
1591
+ }
1592
+
1593
+ /**
1594
+ * Get historical data from legacy single-file structure
1595
+ *
1596
+ * @private
1597
+ * @param {string} projectPath - Project path
1598
+ * @param {string} filename - Filename to read
1599
+ * @returns {Promise<Array<Object>>} Parsed entries
1600
+ */
1601
+ async _getHistoricalDataLegacy(projectPath, filename) {
1602
+ const filePath = await this.getFilePath(projectPath, 'memory', filename)
1603
+
1604
+ try {
1605
+ const content = await this.agent.readFile(filePath)
1606
+ const lines = content.split('\n').filter(line => line.trim())
1607
+ return lines.map(line => {
1608
+ try {
1609
+ return JSON.parse(line)
1610
+ } catch {
1611
+ return null
1612
+ }
1613
+ }).filter(Boolean)
1614
+ } catch {
1615
+ return []
1616
+ }
1617
+ }
1618
+
1619
+ /**
1620
+ * Get recent logs with session support
1621
+ *
1622
+ * @param {string} projectPath - Project path
1623
+ * @param {number} [days=7] - Number of days to look back
1624
+ * @returns {Promise<Array<Object>>} Recent log entries
1625
+ */
1626
+ async getRecentLogs(projectPath, days = 7) {
1627
+ const config = await configManager.readConfig(projectPath)
1628
+
1629
+ if (config && config.projectId) {
1630
+ return await sessionManager.getRecentLogs(config.projectId, days)
1631
+ } else {
1632
+ return await this._getHistoricalDataLegacy(projectPath, 'context.jsonl')
1633
+ }
1634
+ }
1635
+
1636
+ /**
1637
+ * Cleanup old project data
1638
+ *
1639
+ * @param {string} [projectPath=process.cwd()] - Project path
1640
+ * @returns {Promise<Object>} Result object with success flag and message
1641
+ */
1642
+ async cleanup(projectPath = process.cwd()) {
1643
+ try {
1644
+ await this.initializeAgent()
1645
+ const prjctPath = path.join(projectPath, this.prjctDir)
1646
+
1647
+ let totalFreed = 0
1648
+ let filesRemoved = 0
1649
+ let tasksArchived = 0
1650
+
1651
+ try {
1652
+ const tempDir = path.join(prjctPath, 'temp')
1653
+ const tempFiles = await fs.readdir(tempDir).catch(() => [])
1654
+ for (const file of tempFiles) {
1655
+ const filePath = path.join(tempDir, file)
1656
+ const stats = await fs.stat(filePath)
1657
+ totalFreed += stats.size
1658
+ await fs.unlink(filePath)
1659
+ filesRemoved++
1660
+ }
1661
+ } catch (e) {
1662
+ }
1663
+
1664
+ try {
1665
+ const memoryFile = path.join(prjctPath, 'memory.jsonl')
1666
+ const content = await this.agent.readFile(memoryFile)
1667
+ const lines = content.split('\n').filter(line => line.trim())
1668
+ const now = new Date()
1669
+ const thirtyDaysAgo = new Date(now.getTime() - 30 * 86400000)
1670
+
1671
+ const recentLines = []
1672
+ const archivedLines = []
1673
+
1674
+ for (const line of lines) {
1675
+ try {
1676
+ const entry = JSON.parse(line)
1677
+ const entryDate = new Date(entry.timestamp || entry.data?.timestamp)
1678
+ if (entryDate > thirtyDaysAgo) {
1679
+ recentLines.push(line)
1680
+ } else {
1681
+ archivedLines.push(line)
1682
+ }
1683
+ } catch {
1684
+ recentLines.push(line)
1685
+ }
1686
+ }
1687
+
1688
+ if (archivedLines.length > 0) {
1689
+ const archiveFile = path.join(prjctPath, `memory-archive-${now.toISOString().split('T')[0]}.jsonl`)
1690
+ await this.agent.writeFile(archiveFile, archivedLines.join('\n') + '\n')
1691
+ await this.agent.writeFile(memoryFile, recentLines.join('\n') + '\n')
1692
+ tasksArchived = archivedLines.length
1693
+ }
1694
+ } catch (e) {
1695
+ }
1696
+
1697
+ const files = await fs.readdir(prjctPath)
1698
+ for (const file of files) {
1699
+ if (file.endsWith('.md') || file.endsWith('.txt')) {
1700
+ const filePath = path.join(prjctPath, file)
1701
+ const stats = await fs.stat(filePath)
1702
+ if (stats.size === 0) {
1703
+ await fs.unlink(filePath)
1704
+ filesRemoved++
1705
+ }
1706
+ }
1707
+ }
1708
+
1709
+ try {
1710
+ const shippedFile = path.join(prjctPath, 'shipped.md')
1711
+ const content = await this.agent.readFile(shippedFile)
1712
+ const lines = content.split('\n')
1713
+ const now = new Date()
1714
+ const thirtyDaysAgo = new Date(now.getTime() - 30 * 86400000)
1715
+
1716
+ const filteredLines = lines.filter(line => {
1717
+ if (line.includes('✅')) {
1718
+ const dateMatch = line.match(/\((.*?)\)/)
1719
+ if (dateMatch) {
1720
+ const taskDate = new Date(dateMatch[1])
1721
+ if (taskDate < thirtyDaysAgo) {
1722
+ tasksArchived++
1723
+ return false
1724
+ }
1725
+ }
1726
+ }
1727
+ return true
1728
+ })
1729
+
1730
+ await this.agent.writeFile(shippedFile, filteredLines.join('\n'))
1731
+ } catch (e) {
1732
+ }
1733
+
1734
+ const freedMB = (totalFreed / 1024 / 1024).toFixed(2)
1735
+
1736
+ const message = '🧹 Cleanup complete!\n' +
1737
+ `• Files removed: ${filesRemoved}\n` +
1738
+ `• Tasks archived: ${tasksArchived}\n` +
1739
+ `• Space freed: ${freedMB} MB\n` +
1740
+ '\n✨ Your project is clean and lean!'
1741
+
1742
+ await this.logToMemory(projectPath, 'cleanup', {
1743
+ filesRemoved,
1744
+ tasksArchived,
1745
+ spaceFeed: freedMB,
1746
+ })
1747
+
1748
+ return {
1749
+ success: true,
1750
+ message: this.agent.formatResponse(message, 'success'),
1751
+ }
1752
+ } catch (error) {
1753
+ await this.initializeAgent()
1754
+ return {
1755
+ success: false,
1756
+ message: this.agent.formatResponse(`Cleanup failed: ${error.message}`, 'error'),
1757
+ }
1758
+ }
1759
+ }
1760
+
1761
+ /**
1762
+ * Migrate all legacy projects to new structure
1763
+ *
1764
+ * @param {Object} [options={}] - Migration options
1765
+ * @returns {Promise<Object>} Result object with summary
1766
+ */
1767
+ async migrateAll(options = {}) {
1768
+ try {
1769
+ await this.initializeAgent()
1770
+
1771
+ const {
1772
+ deepScan = false,
1773
+ removeLegacy = false,
1774
+ dryRun = false,
1775
+ } = options
1776
+
1777
+ const onProgress = (update) => {
1778
+ if (update.phase === 'scanning') {
1779
+ console.log(`🔍 ${update.message}`)
1780
+ } else if (update.phase === 'checking' || update.phase === 'migrating') {
1781
+ console.log(` ${update.message}`)
1782
+ }
1783
+ }
1784
+
1785
+ const summary = await migrator.migrateAll({
1786
+ deepScan,
1787
+ removeLegacy,
1788
+ dryRun,
1789
+ onProgress,
1790
+ })
1791
+
1792
+ const report = migrator.generateMigrationSummary(summary)
1793
+
1794
+ return {
1795
+ success: summary.success,
1796
+ message: report,
1797
+ summary,
1798
+ }
1799
+ } catch (error) {
1800
+ await this.initializeAgent()
1801
+ return {
1802
+ success: false,
1803
+ message: this.agent.formatResponse(`Global migration failed: ${error.message}`, 'error'),
1804
+ }
1805
+ }
1806
+ }
1807
+
1808
+ /**
1809
+ * Install commands to AI editors
1810
+ *
1811
+ * @param {Object} [options={}] - Installation options
1812
+ * @returns {Promise<Object>} Result object with success flag and message
1813
+ */
1814
+ async install(options = {}) {
1815
+ try {
1816
+ await this.initializeAgent()
1817
+
1818
+ const {
1819
+ force = false,
1820
+ editor = null,
1821
+ createTemplates = false,
1822
+ interactive = true,
1823
+ } = options
1824
+
1825
+ if (createTemplates) {
1826
+ const templateResult = await commandInstaller.createTemplates()
1827
+ if (!templateResult.success) {
1828
+ return {
1829
+ success: false,
1830
+ message: this.agent.formatResponse(templateResult.message, 'error'),
1831
+ }
1832
+ }
1833
+ }
1834
+
1835
+ const detection = await commandInstaller.detectEditors(process.cwd())
1836
+ const detectedEditors = Object.entries(detection)
1837
+ .filter(([_, info]) => info.detected)
1838
+
1839
+ if (detectedEditors.length === 0) {
1840
+ return {
1841
+ success: false,
1842
+ message: this.agent.formatResponse('No AI editors detected on this system', 'error'),
1843
+ }
1844
+ }
1845
+
1846
+ let installResult
1847
+
1848
+ if (editor) {
1849
+ // Install to specific editor
1850
+ installResult = await commandInstaller.installToEditor(editor, force)
1851
+ } else if (interactive) {
1852
+ // Interactive mode: use new interactiveInstall method
1853
+ installResult = await commandInstaller.interactiveInstall(force)
1854
+ } else {
1855
+ // Non-interactive mode: install to all detected editors
1856
+ installResult = await commandInstaller.installToAll(force)
1857
+ }
1858
+
1859
+ // Always install Context7 MCP after commands installation
1860
+ const mcpResult = await commandInstaller.installContext7MCP()
1861
+
1862
+ let report = commandInstaller.generateReport(installResult)
1863
+ if (mcpResult.success && mcpResult.editors.length > 0) {
1864
+ report += '\n\n🔌 Context7 MCP Enabled\n'
1865
+ report += ` Editors: ${mcpResult.editors.join(', ')}\n`
1866
+ report += ' 📚 Library documentation now available automatically'
1867
+ }
1868
+
1869
+ return {
1870
+ success: installResult.success,
1871
+ message: this.agent.formatResponse(report, installResult.success ? 'celebrate' : 'error'),
1872
+ }
1873
+ } catch (error) {
1874
+ await this.initializeAgent()
1875
+ return {
1876
+ success: false,
1877
+ message: this.agent.formatResponse(`Installation failed: ${error.message}`, 'error'),
1878
+ }
1879
+ }
1880
+ }
1881
+
1882
+ /**
1883
+ * Analyze codebase and optionally sync with .prjct/ state
1884
+ *
1885
+ * @param {Object} [options={}] - Analysis options
1886
+ * @param {string} [projectPath=process.cwd()] - Project path
1887
+ * @returns {Promise<Object>} Result object with analysis and sync results
1888
+ */
1889
+ async analyze(options = {}, projectPath = process.cwd()) {
1890
+ try {
1891
+ await this.initializeAgent()
1892
+
1893
+ const {
1894
+ sync = false,
1895
+ reportOnly = false,
1896
+ silent = false,
1897
+ } = options
1898
+
1899
+ if (!silent) {
1900
+ console.log('🔍 Analyzing codebase...')
1901
+ }
1902
+
1903
+ const analysis = await analyzer.analyzeProject(projectPath)
1904
+
1905
+ const summary = {
1906
+ commandsFound: analysis.commands.length,
1907
+ featuresFound: analysis.features.length,
1908
+ technologies: analysis.technologies.join(', '),
1909
+ fileCount: analysis.structure.fileCount,
1910
+ hasGit: analysis.gitHistory.hasGit,
1911
+ }
1912
+
1913
+ let syncResults = null
1914
+ if (sync && !reportOnly) {
1915
+ const globalProjectPath = await this.getGlobalProjectPath(projectPath)
1916
+ syncResults = await analyzer.syncWithPrjctFiles(globalProjectPath)
1917
+ }
1918
+
1919
+ let message = ''
1920
+
1921
+ if (silent) {
1922
+ message = `Found ${summary.commandsFound} commands, ${summary.featuresFound} features`
1923
+ } else if (reportOnly) {
1924
+ message = this.formatAnalysisReport(summary, analysis)
1925
+ } else if (sync) {
1926
+ message = this.formatAnalysisWithSync(summary, syncResults)
1927
+ } else {
1928
+ message = this.formatAnalysisReport(summary, analysis)
1929
+ }
1930
+
1931
+ return {
1932
+ success: true,
1933
+ message: this.agent.formatResponse(message, 'info'),
1934
+ analysis,
1935
+ syncResults,
1936
+ }
1937
+ } catch (error) {
1938
+ await this.initializeAgent()
1939
+ return {
1940
+ success: false,
1941
+ message: this.agent.formatResponse(`Analysis failed: ${error.message}`, 'error'),
1942
+ }
1943
+ }
1944
+ }
1945
+
1946
+ /**
1947
+ * Format analysis report for display
1948
+ *
1949
+ * @param {Object} summary - Analysis summary
1950
+ * @param {Object} analysis - Full analysis results
1951
+ * @returns {string} Formatted report
1952
+ */
1953
+ formatAnalysisReport(summary, analysis) {
1954
+ return `
1955
+ 🔍 Codebase Analysis Complete
1956
+
1957
+ 📊 Project Overview:
1958
+ • Technologies: ${summary.technologies || 'Not detected'}
1959
+ • Total Files: ~${summary.fileCount}
1960
+ • Git Repository: ${summary.hasGit ? '✅ Yes' : '❌ No'}
1961
+
1962
+ 🛠️ Implemented Commands: ${summary.commandsFound}
1963
+ ${analysis.commands.slice(0, 10).map(cmd => ` • /p:${cmd}`).join('\n')}
1964
+ ${analysis.commands.length > 10 ? ` ... and ${analysis.commands.length - 10} more` : ''}
1965
+
1966
+ ✨ Detected Features: ${summary.featuresFound}
1967
+ ${analysis.features.slice(0, 5).map(f => ` • ${f}`).join('\n')}
1968
+ ${analysis.features.length > 5 ? ` ... and ${analysis.features.length - 5} more` : ''}
1969
+
1970
+ 📝 Full report saved to: analysis/repo-summary.md
1971
+
1972
+ 💡 Use /p:analyze --sync to sync with .prjct/ files
1973
+ `
1974
+ }
1975
+
1976
+ /**
1977
+ * Format analysis with sync results
1978
+ *
1979
+ * @param {Object} summary - Analysis summary
1980
+ * @param {Object} syncResults - Sync results
1981
+ * @returns {string} Formatted report with sync info
1982
+ */
1983
+ formatAnalysisWithSync(summary, syncResults) {
1984
+ return `
1985
+ 🔍 Analysis & Sync Complete
1986
+
1987
+ 📊 Detected:
1988
+ ✅ ${summary.commandsFound} implemented commands
1989
+ ✅ ${summary.featuresFound} completed features
1990
+
1991
+ 📝 Synchronized:
1992
+ ${syncResults.nextMdUpdated ? `✅ Updated next.md (${syncResults.tasksMarkedComplete} tasks marked complete)` : '• next.md (no changes)'}
1993
+ ${syncResults.shippedMdUpdated ? `✅ Updated shipped.md (${syncResults.featuresAdded} features added)` : '• shipped.md (no changes)'}
1994
+ ✅ Created analysis/repo-summary.md
1995
+
1996
+ 💡 Next: Use /p:next to see remaining tasks
1997
+ `
1998
+ }
1999
+
2000
+ /**
2001
+ * Detect if project has existing code (for auto-analyze during init)
2002
+ *
2003
+ * @param {string} projectPath - Project path
2004
+ * @returns {Promise<boolean>} True if project has significant existing code
2005
+ */
2006
+ async detectExistingCode(projectPath) {
2007
+ try {
2008
+ const packagePath = path.join(projectPath, 'package.json')
2009
+ try {
2010
+ const content = await fs.readFile(packagePath, 'utf-8')
2011
+ const pkg = JSON.parse(content)
2012
+
2013
+ if (pkg.dependencies && Object.keys(pkg.dependencies).length > 0) {
2014
+ return true
2015
+ }
2016
+ } catch {
2017
+ }
2018
+
2019
+ try {
2020
+ const { stdout } = await exec('git rev-list --count HEAD', { cwd: projectPath })
2021
+ const commitCount = parseInt(stdout.trim())
2022
+ if (commitCount > 0) {
2023
+ return true
2024
+ }
2025
+ } catch {
2026
+ }
2027
+
2028
+ const entries = await fs.readdir(projectPath)
2029
+ const codeExtensions = ['.js', '.ts', '.jsx', '.tsx', '.py', '.go', '.rs', '.rb', '.java']
2030
+
2031
+ let codeFileCount = 0
2032
+ for (const entry of entries) {
2033
+ if (entry.startsWith('.') || entry === 'node_modules') {
2034
+ continue
2035
+ }
2036
+
2037
+ const ext = path.extname(entry)
2038
+ if (codeExtensions.includes(ext)) {
2039
+ codeFileCount++
2040
+ }
2041
+ }
2042
+
2043
+ return codeFileCount >= 5
2044
+ } catch (error) {
2045
+ return false
2046
+ }
2047
+ }
2048
+ }
2049
+
2050
+ module.exports = new PrjctCommands()