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.
- package/CHANGELOG.md +312 -0
- package/CLAUDE.md +300 -0
- package/LICENSE +21 -0
- package/README.md +424 -0
- package/bin/prjct +214 -0
- package/core/agent-detector.js +249 -0
- package/core/agents/claude-agent.js +250 -0
- package/core/agents/codex-agent.js +256 -0
- package/core/agents/terminal-agent.js +465 -0
- package/core/analyzer.js +596 -0
- package/core/animations-simple.js +240 -0
- package/core/animations.js +277 -0
- package/core/author-detector.js +218 -0
- package/core/capability-installer.js +190 -0
- package/core/command-installer.js +775 -0
- package/core/commands.js +2050 -0
- package/core/config-manager.js +335 -0
- package/core/migrator.js +784 -0
- package/core/path-manager.js +324 -0
- package/core/project-capabilities.js +144 -0
- package/core/session-manager.js +439 -0
- package/core/version.js +107 -0
- package/core/workflow-engine.js +213 -0
- package/core/workflow-prompts.js +192 -0
- package/core/workflow-rules.js +147 -0
- package/package.json +80 -0
- package/scripts/install.sh +433 -0
- package/scripts/verify-installation.sh +158 -0
- package/templates/agents/AGENTS.md +164 -0
- package/templates/commands/analyze.md +125 -0
- package/templates/commands/cleanup.md +102 -0
- package/templates/commands/context.md +105 -0
- package/templates/commands/design.md +113 -0
- package/templates/commands/done.md +44 -0
- package/templates/commands/fix.md +87 -0
- package/templates/commands/git.md +79 -0
- package/templates/commands/help.md +72 -0
- package/templates/commands/idea.md +50 -0
- package/templates/commands/init.md +237 -0
- package/templates/commands/next.md +74 -0
- package/templates/commands/now.md +35 -0
- package/templates/commands/progress.md +92 -0
- package/templates/commands/recap.md +86 -0
- package/templates/commands/roadmap.md +107 -0
- package/templates/commands/ship.md +41 -0
- package/templates/commands/stuck.md +48 -0
- package/templates/commands/task.md +97 -0
- package/templates/commands/test.md +94 -0
- package/templates/commands/workflow.md +224 -0
- package/templates/examples/natural-language-examples.md +320 -0
- package/templates/mcp-config.json +8 -0
- package/templates/workflows/analyze.md +159 -0
- package/templates/workflows/cleanup.md +73 -0
- package/templates/workflows/context.md +72 -0
- package/templates/workflows/design.md +88 -0
- package/templates/workflows/done.md +20 -0
- package/templates/workflows/fix.md +201 -0
- package/templates/workflows/git.md +192 -0
- package/templates/workflows/help.md +13 -0
- package/templates/workflows/idea.md +22 -0
- package/templates/workflows/init.md +80 -0
- package/templates/workflows/natural-language-handler.md +183 -0
- package/templates/workflows/next.md +44 -0
- package/templates/workflows/now.md +19 -0
- package/templates/workflows/progress.md +113 -0
- package/templates/workflows/recap.md +66 -0
- package/templates/workflows/roadmap.md +95 -0
- package/templates/workflows/ship.md +18 -0
- package/templates/workflows/stuck.md +25 -0
- package/templates/workflows/task.md +109 -0
- package/templates/workflows/test.md +243 -0
package/core/commands.js
ADDED
|
@@ -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()
|