prjct-cli 0.11.3 → 0.11.5
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 +14 -0
- package/bin/prjct +4 -0
- package/bin/serve.js +22 -6
- package/core/__tests__/utils/date-helper.test.js +416 -0
- package/core/agentic/agent-router.js +30 -18
- package/core/agentic/command-executor.js +20 -24
- package/core/agentic/context-builder.js +7 -8
- package/core/agentic/memory-system.js +14 -19
- package/core/agentic/prompt-builder.js +41 -27
- package/core/agentic/template-loader.js +8 -2
- package/core/infrastructure/agent-detector.js +7 -4
- package/core/infrastructure/migrator.js +10 -13
- package/core/infrastructure/session-manager.js +10 -10
- package/package.json +1 -1
- package/packages/web/app/project/[id]/stats/page.tsx +102 -343
- package/packages/web/components/stats/ActivityTimeline.tsx +201 -0
- package/packages/web/components/stats/AgentsCard.tsx +56 -0
- package/packages/web/components/stats/BentoCard.tsx +88 -0
- package/packages/web/components/stats/BentoGrid.tsx +22 -0
- package/packages/web/components/stats/EmptyState.tsx +67 -0
- package/packages/web/components/stats/HeroSection.tsx +172 -0
- package/packages/web/components/stats/IdeasCard.tsx +59 -0
- package/packages/web/components/stats/NowCard.tsx +71 -0
- package/packages/web/components/stats/ProgressRing.tsx +74 -0
- package/packages/web/components/stats/QueueCard.tsx +58 -0
- package/packages/web/components/stats/RoadmapCard.tsx +97 -0
- package/packages/web/components/stats/ShipsCard.tsx +70 -0
- package/packages/web/components/stats/SparklineChart.tsx +44 -0
- package/packages/web/components/stats/StreakCard.tsx +59 -0
- package/packages/web/components/stats/VelocityCard.tsx +60 -0
- package/packages/web/components/stats/index.ts +17 -0
- package/packages/web/components/ui/tooltip.tsx +2 -2
- package/packages/web/next-env.d.ts +1 -1
- package/packages/web/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.11.5] - 2025-12-09
|
|
4
|
+
|
|
5
|
+
### Fixed - Production Server Mode
|
|
6
|
+
|
|
7
|
+
Critical bug fix: `prjct serve` now runs in production mode instead of development mode.
|
|
8
|
+
|
|
9
|
+
- **Server Mode** - Changed from dev to production
|
|
10
|
+
- Default port: 3000 → 9472 (avoids conflicts)
|
|
11
|
+
- Mode: `npm run dev` → `npm run start:prod` with `NODE_ENV=production`
|
|
12
|
+
- Auto-build on first run if `.next/` doesn't exist
|
|
13
|
+
- All features now work correctly (were broken in dev mode)
|
|
14
|
+
|
|
15
|
+
- **packages/web/package.json** - Added `start:prod` script
|
|
16
|
+
|
|
3
17
|
## [0.11.0] - 2025-12-08
|
|
4
18
|
|
|
5
19
|
### Added - Web Application & Server Components
|
package/bin/prjct
CHANGED
|
@@ -15,6 +15,10 @@ if (args[0] === 'dev') {
|
|
|
15
15
|
// Launch prjct dev environment
|
|
16
16
|
require('./dev.js')
|
|
17
17
|
process.exitCode = 0
|
|
18
|
+
} else if (args[0] === 'web' || args[0] === 'serve') {
|
|
19
|
+
// Launch prjct web server
|
|
20
|
+
require('./serve.js')
|
|
21
|
+
process.exitCode = 0
|
|
18
22
|
} else {
|
|
19
23
|
|
|
20
24
|
// Ensure setup has run for this version
|
package/bin/serve.js
CHANGED
|
@@ -20,7 +20,7 @@ const webDir = path.join(packagesDir, 'web')
|
|
|
20
20
|
// Parse arguments
|
|
21
21
|
const args = process.argv.slice(2)
|
|
22
22
|
const portArg = args.find((a) => a.startsWith('--port='))
|
|
23
|
-
const port = portArg ? portArg.split('=')[1] : '
|
|
23
|
+
const port = portArg ? portArg.split('=')[1] : '9472'
|
|
24
24
|
|
|
25
25
|
// Check if web package exists
|
|
26
26
|
if (!fs.existsSync(webDir)) {
|
|
@@ -144,21 +144,37 @@ console.log(`
|
|
|
144
144
|
║ ║
|
|
145
145
|
║ ⚡ prjct - Developer Momentum ║
|
|
146
146
|
║ ║
|
|
147
|
-
║
|
|
147
|
+
║ Production server ready ║
|
|
148
148
|
║ ║
|
|
149
|
-
║ Web: http://localhost:${port}
|
|
149
|
+
║ Web: http://localhost:${port} ║
|
|
150
150
|
║ ║
|
|
151
151
|
║ Using your Claude subscription - $0 API costs ║
|
|
152
152
|
║ ║
|
|
153
153
|
╚═══════════════════════════════════════════════════════════╝
|
|
154
154
|
`)
|
|
155
155
|
|
|
156
|
-
//
|
|
157
|
-
const
|
|
156
|
+
// Build for production if needed (first run)
|
|
157
|
+
const nextDir = path.join(webDir, '.next')
|
|
158
|
+
if (!fs.existsSync(nextDir)) {
|
|
159
|
+
console.log('🔨 Building for production (first run)...\n')
|
|
160
|
+
const buildResult = spawnSync('npm', ['run', 'build'], {
|
|
161
|
+
cwd: webDir,
|
|
162
|
+
stdio: 'inherit',
|
|
163
|
+
shell: true,
|
|
164
|
+
})
|
|
165
|
+
if (buildResult.status !== 0) {
|
|
166
|
+
console.error('❌ Build failed')
|
|
167
|
+
process.exit(1)
|
|
168
|
+
}
|
|
169
|
+
console.log('✅ Build complete!\n')
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Start web server in production mode
|
|
173
|
+
const web = spawn('npm', ['run', 'start:prod'], {
|
|
158
174
|
cwd: webDir,
|
|
159
175
|
stdio: 'inherit',
|
|
160
176
|
shell: true,
|
|
161
|
-
env: { ...process.env, PORT: port },
|
|
177
|
+
env: { ...process.env, PORT: port, NODE_ENV: 'production' },
|
|
162
178
|
})
|
|
163
179
|
|
|
164
180
|
// Open browser after a short delay
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Date Helper Tests
|
|
3
|
+
* Tests for centralized date operations and formatting
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
formatDate,
|
|
8
|
+
formatMonth,
|
|
9
|
+
getTodayKey,
|
|
10
|
+
getDateKey,
|
|
11
|
+
getYearMonthDay,
|
|
12
|
+
parseDate,
|
|
13
|
+
getTimestamp,
|
|
14
|
+
getDaysAgo,
|
|
15
|
+
getDaysFromNow,
|
|
16
|
+
getDateRange,
|
|
17
|
+
isToday,
|
|
18
|
+
isWithinLastDays,
|
|
19
|
+
formatDuration,
|
|
20
|
+
calculateDuration,
|
|
21
|
+
getStartOfDay,
|
|
22
|
+
getEndOfDay,
|
|
23
|
+
} = require('../../utils/date-helper')
|
|
24
|
+
|
|
25
|
+
describe('DateHelper', () => {
|
|
26
|
+
describe('formatDate', () => {
|
|
27
|
+
it('should format date to YYYY-MM-DD', () => {
|
|
28
|
+
const date = new Date(2025, 9, 4) // Oct 4, 2025
|
|
29
|
+
expect(formatDate(date)).toBe('2025-10-04')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('should pad single digit months', () => {
|
|
33
|
+
const date = new Date(2025, 0, 15) // Jan 15, 2025
|
|
34
|
+
expect(formatDate(date)).toBe('2025-01-15')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('should pad single digit days', () => {
|
|
38
|
+
const date = new Date(2025, 11, 5) // Dec 5, 2025
|
|
39
|
+
expect(formatDate(date)).toBe('2025-12-05')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('should handle year boundaries', () => {
|
|
43
|
+
const date = new Date(2024, 11, 31) // Dec 31, 2024
|
|
44
|
+
expect(formatDate(date)).toBe('2024-12-31')
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('formatMonth', () => {
|
|
49
|
+
it('should format date to YYYY-MM', () => {
|
|
50
|
+
const date = new Date(2025, 9, 15) // Oct 15, 2025
|
|
51
|
+
expect(formatMonth(date)).toBe('2025-10')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('should pad single digit months', () => {
|
|
55
|
+
const date = new Date(2025, 2, 1) // Mar 1, 2025
|
|
56
|
+
expect(formatMonth(date)).toBe('2025-03')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('should handle December', () => {
|
|
60
|
+
const date = new Date(2025, 11, 25) // Dec 25, 2025
|
|
61
|
+
expect(formatMonth(date)).toBe('2025-12')
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('getTodayKey', () => {
|
|
66
|
+
it('should return today in YYYY-MM-DD format', () => {
|
|
67
|
+
vi.useFakeTimers()
|
|
68
|
+
vi.setSystemTime(new Date(2025, 5, 15)) // June 15, 2025
|
|
69
|
+
|
|
70
|
+
expect(getTodayKey()).toBe('2025-06-15')
|
|
71
|
+
|
|
72
|
+
vi.useRealTimers()
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
describe('getDateKey', () => {
|
|
77
|
+
it('should return date in YYYY-MM-DD format (alias for formatDate)', () => {
|
|
78
|
+
const date = new Date(2025, 7, 20) // Aug 20, 2025
|
|
79
|
+
expect(getDateKey(date)).toBe('2025-08-20')
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
describe('getYearMonthDay', () => {
|
|
84
|
+
it('should return separate year, month, day strings', () => {
|
|
85
|
+
const date = new Date(2025, 9, 4) // Oct 4, 2025
|
|
86
|
+
const result = getYearMonthDay(date)
|
|
87
|
+
|
|
88
|
+
expect(result.year).toBe('2025')
|
|
89
|
+
expect(result.month).toBe('10')
|
|
90
|
+
expect(result.day).toBe('04')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('should pad month values', () => {
|
|
94
|
+
const date = new Date(2025, 0, 15) // Jan 15, 2025
|
|
95
|
+
const result = getYearMonthDay(date)
|
|
96
|
+
|
|
97
|
+
expect(result.month).toBe('01')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('should pad day values', () => {
|
|
101
|
+
const date = new Date(2025, 5, 7) // June 7, 2025
|
|
102
|
+
const result = getYearMonthDay(date)
|
|
103
|
+
|
|
104
|
+
expect(result.day).toBe('07')
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
describe('parseDate', () => {
|
|
109
|
+
it('should parse YYYY-MM-DD format', () => {
|
|
110
|
+
const result = parseDate('2025-10-04')
|
|
111
|
+
expect(result.getFullYear()).toBe(2025)
|
|
112
|
+
expect(result.getMonth()).toBe(9) // 0-indexed
|
|
113
|
+
expect(result.getDate()).toBe(4)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('should parse ISO strings', () => {
|
|
117
|
+
const result = parseDate('2025-10-04T14:30:00.000Z')
|
|
118
|
+
expect(result.getFullYear()).toBe(2025)
|
|
119
|
+
expect(result.getMonth()).toBe(9)
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
describe('getTimestamp', () => {
|
|
124
|
+
it('should return ISO timestamp', () => {
|
|
125
|
+
vi.useFakeTimers()
|
|
126
|
+
vi.setSystemTime(new Date('2025-10-04T14:30:00.000Z'))
|
|
127
|
+
|
|
128
|
+
expect(getTimestamp()).toBe('2025-10-04T14:30:00.000Z')
|
|
129
|
+
|
|
130
|
+
vi.useRealTimers()
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('should include milliseconds', () => {
|
|
134
|
+
const timestamp = getTimestamp()
|
|
135
|
+
expect(timestamp).toMatch(/\.\d{3}Z$/)
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
describe('getDaysAgo', () => {
|
|
140
|
+
beforeEach(() => {
|
|
141
|
+
vi.useFakeTimers()
|
|
142
|
+
vi.setSystemTime(new Date(2025, 9, 15)) // Oct 15, 2025
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
afterEach(() => {
|
|
146
|
+
vi.useRealTimers()
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('should calculate past dates correctly', () => {
|
|
150
|
+
const result = getDaysAgo(5)
|
|
151
|
+
expect(formatDate(result)).toBe('2025-10-10')
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('should handle month boundaries', () => {
|
|
155
|
+
const result = getDaysAgo(20)
|
|
156
|
+
expect(formatDate(result)).toBe('2025-09-25')
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('should return today for 0 days ago', () => {
|
|
160
|
+
const result = getDaysAgo(0)
|
|
161
|
+
expect(formatDate(result)).toBe('2025-10-15')
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
describe('getDaysFromNow', () => {
|
|
166
|
+
beforeEach(() => {
|
|
167
|
+
vi.useFakeTimers()
|
|
168
|
+
vi.setSystemTime(new Date(2025, 9, 15)) // Oct 15, 2025
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
afterEach(() => {
|
|
172
|
+
vi.useRealTimers()
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('should calculate future dates correctly', () => {
|
|
176
|
+
const result = getDaysFromNow(5)
|
|
177
|
+
expect(formatDate(result)).toBe('2025-10-20')
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('should handle month boundaries', () => {
|
|
181
|
+
const result = getDaysFromNow(20)
|
|
182
|
+
expect(formatDate(result)).toBe('2025-11-04')
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('should return today for 0 days from now', () => {
|
|
186
|
+
const result = getDaysFromNow(0)
|
|
187
|
+
expect(formatDate(result)).toBe('2025-10-15')
|
|
188
|
+
})
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
describe('getDateRange', () => {
|
|
192
|
+
it('should return array of dates in range', () => {
|
|
193
|
+
const from = new Date(2025, 9, 1) // Oct 1
|
|
194
|
+
const to = new Date(2025, 9, 5) // Oct 5
|
|
195
|
+
|
|
196
|
+
const result = getDateRange(from, to)
|
|
197
|
+
|
|
198
|
+
expect(result.length).toBe(5)
|
|
199
|
+
expect(formatDate(result[0])).toBe('2025-10-01')
|
|
200
|
+
expect(formatDate(result[4])).toBe('2025-10-05')
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('should include start and end dates', () => {
|
|
204
|
+
const from = new Date(2025, 9, 10)
|
|
205
|
+
const to = new Date(2025, 9, 12)
|
|
206
|
+
|
|
207
|
+
const result = getDateRange(from, to)
|
|
208
|
+
|
|
209
|
+
expect(formatDate(result[0])).toBe('2025-10-10')
|
|
210
|
+
expect(formatDate(result[result.length - 1])).toBe('2025-10-12')
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('should return single date if from equals to', () => {
|
|
214
|
+
const date = new Date(2025, 9, 15)
|
|
215
|
+
const result = getDateRange(date, date)
|
|
216
|
+
|
|
217
|
+
expect(result.length).toBe(1)
|
|
218
|
+
expect(formatDate(result[0])).toBe('2025-10-15')
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('should handle month boundaries', () => {
|
|
222
|
+
const from = new Date(2025, 9, 30) // Oct 30
|
|
223
|
+
const to = new Date(2025, 10, 2) // Nov 2
|
|
224
|
+
|
|
225
|
+
const result = getDateRange(from, to)
|
|
226
|
+
|
|
227
|
+
expect(result.length).toBe(4)
|
|
228
|
+
expect(formatDate(result[0])).toBe('2025-10-30')
|
|
229
|
+
expect(formatDate(result[3])).toBe('2025-11-02')
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('should return empty array if from is after to', () => {
|
|
233
|
+
const from = new Date(2025, 9, 15)
|
|
234
|
+
const to = new Date(2025, 9, 10)
|
|
235
|
+
|
|
236
|
+
const result = getDateRange(from, to)
|
|
237
|
+
|
|
238
|
+
expect(result.length).toBe(0)
|
|
239
|
+
})
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
describe('isToday', () => {
|
|
243
|
+
beforeEach(() => {
|
|
244
|
+
vi.useFakeTimers()
|
|
245
|
+
vi.setSystemTime(new Date(2025, 9, 15)) // Oct 15, 2025
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
afterEach(() => {
|
|
249
|
+
vi.useRealTimers()
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it('should return true for today', () => {
|
|
253
|
+
const today = new Date(2025, 9, 15)
|
|
254
|
+
expect(isToday(today)).toBe(true)
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('should return false for yesterday', () => {
|
|
258
|
+
const yesterday = new Date(2025, 9, 14)
|
|
259
|
+
expect(isToday(yesterday)).toBe(false)
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
it('should return false for tomorrow', () => {
|
|
263
|
+
const tomorrow = new Date(2025, 9, 16)
|
|
264
|
+
expect(isToday(tomorrow)).toBe(false)
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it('should ignore time component', () => {
|
|
268
|
+
const todayLate = new Date(2025, 9, 15, 23, 59, 59)
|
|
269
|
+
expect(isToday(todayLate)).toBe(true)
|
|
270
|
+
})
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
describe('isWithinLastDays', () => {
|
|
274
|
+
beforeEach(() => {
|
|
275
|
+
vi.useFakeTimers()
|
|
276
|
+
vi.setSystemTime(new Date(2025, 9, 15, 12, 0, 0)) // Oct 15, 2025 at noon
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
afterEach(() => {
|
|
280
|
+
vi.useRealTimers()
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
it('should return true for dates within range', () => {
|
|
284
|
+
const recent = new Date(2025, 9, 12) // 3 days ago
|
|
285
|
+
expect(isWithinLastDays(recent, 7)).toBe(true)
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it('should return false for dates outside range', () => {
|
|
289
|
+
const old = new Date(2025, 9, 1) // 14 days ago
|
|
290
|
+
expect(isWithinLastDays(old, 7)).toBe(false)
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
it('should include today', () => {
|
|
294
|
+
const today = new Date(2025, 9, 15)
|
|
295
|
+
expect(isWithinLastDays(today, 7)).toBe(true)
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
it('should include boundary date', () => {
|
|
299
|
+
const boundary = new Date(2025, 9, 8) // exactly 7 days ago
|
|
300
|
+
expect(isWithinLastDays(boundary, 7)).toBe(true)
|
|
301
|
+
})
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
describe('formatDuration', () => {
|
|
305
|
+
it('should format seconds', () => {
|
|
306
|
+
expect(formatDuration(5000)).toBe('5s')
|
|
307
|
+
expect(formatDuration(45000)).toBe('45s')
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
it('should format minutes', () => {
|
|
311
|
+
expect(formatDuration(60000)).toBe('1m')
|
|
312
|
+
expect(formatDuration(120000)).toBe('2m')
|
|
313
|
+
expect(formatDuration(90000)).toBe('1m') // 1.5 min rounds down
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
it('should format hours and minutes', () => {
|
|
317
|
+
expect(formatDuration(3600000)).toBe('1h 0m')
|
|
318
|
+
expect(formatDuration(5400000)).toBe('1h 30m')
|
|
319
|
+
expect(formatDuration(7200000)).toBe('2h 0m')
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
it('should format days and hours', () => {
|
|
323
|
+
expect(formatDuration(86400000)).toBe('1d 0h')
|
|
324
|
+
expect(formatDuration(90000000)).toBe('1d 1h')
|
|
325
|
+
expect(formatDuration(172800000)).toBe('2d 0h')
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
it('should handle zero', () => {
|
|
329
|
+
expect(formatDuration(0)).toBe('0s')
|
|
330
|
+
})
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
describe('calculateDuration', () => {
|
|
334
|
+
it('should calculate duration between two dates', () => {
|
|
335
|
+
const start = new Date('2025-10-15T10:00:00.000Z')
|
|
336
|
+
const end = new Date('2025-10-15T12:30:00.000Z')
|
|
337
|
+
|
|
338
|
+
expect(calculateDuration(start, end)).toBe('2h 30m')
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
it('should default to now if no end date', () => {
|
|
342
|
+
vi.useFakeTimers()
|
|
343
|
+
vi.setSystemTime(new Date('2025-10-15T12:00:00.000Z'))
|
|
344
|
+
|
|
345
|
+
const start = new Date('2025-10-15T10:00:00.000Z')
|
|
346
|
+
expect(calculateDuration(start)).toBe('2h 0m')
|
|
347
|
+
|
|
348
|
+
vi.useRealTimers()
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
it('should handle short durations', () => {
|
|
352
|
+
const start = new Date('2025-10-15T10:00:00.000Z')
|
|
353
|
+
const end = new Date('2025-10-15T10:00:30.000Z')
|
|
354
|
+
|
|
355
|
+
expect(calculateDuration(start, end)).toBe('30s')
|
|
356
|
+
})
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
describe('getStartOfDay', () => {
|
|
360
|
+
it('should set time to 00:00:00.000', () => {
|
|
361
|
+
const date = new Date(2025, 9, 15, 14, 30, 45, 500)
|
|
362
|
+
const result = getStartOfDay(date)
|
|
363
|
+
|
|
364
|
+
expect(result.getHours()).toBe(0)
|
|
365
|
+
expect(result.getMinutes()).toBe(0)
|
|
366
|
+
expect(result.getSeconds()).toBe(0)
|
|
367
|
+
expect(result.getMilliseconds()).toBe(0)
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
it('should preserve the date', () => {
|
|
371
|
+
const date = new Date(2025, 9, 15, 23, 59, 59)
|
|
372
|
+
const result = getStartOfDay(date)
|
|
373
|
+
|
|
374
|
+
expect(result.getFullYear()).toBe(2025)
|
|
375
|
+
expect(result.getMonth()).toBe(9)
|
|
376
|
+
expect(result.getDate()).toBe(15)
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
it('should not mutate original date', () => {
|
|
380
|
+
const original = new Date(2025, 9, 15, 14, 30)
|
|
381
|
+
getStartOfDay(original)
|
|
382
|
+
|
|
383
|
+
expect(original.getHours()).toBe(14)
|
|
384
|
+
expect(original.getMinutes()).toBe(30)
|
|
385
|
+
})
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
describe('getEndOfDay', () => {
|
|
389
|
+
it('should set time to 23:59:59.999', () => {
|
|
390
|
+
const date = new Date(2025, 9, 15, 10, 0, 0, 0)
|
|
391
|
+
const result = getEndOfDay(date)
|
|
392
|
+
|
|
393
|
+
expect(result.getHours()).toBe(23)
|
|
394
|
+
expect(result.getMinutes()).toBe(59)
|
|
395
|
+
expect(result.getSeconds()).toBe(59)
|
|
396
|
+
expect(result.getMilliseconds()).toBe(999)
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
it('should preserve the date', () => {
|
|
400
|
+
const date = new Date(2025, 9, 15, 0, 0, 0)
|
|
401
|
+
const result = getEndOfDay(date)
|
|
402
|
+
|
|
403
|
+
expect(result.getFullYear()).toBe(2025)
|
|
404
|
+
expect(result.getMonth()).toBe(9)
|
|
405
|
+
expect(result.getDate()).toBe(15)
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
it('should not mutate original date', () => {
|
|
409
|
+
const original = new Date(2025, 9, 15, 10, 30)
|
|
410
|
+
getEndOfDay(original)
|
|
411
|
+
|
|
412
|
+
expect(original.getHours()).toBe(10)
|
|
413
|
+
expect(original.getMinutes()).toBe(30)
|
|
414
|
+
})
|
|
415
|
+
})
|
|
416
|
+
})
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Agent Router
|
|
3
|
-
*
|
|
4
|
-
* AGENTIC: All decisions made by Claude via templates/agent-assignment.md
|
|
5
|
-
* JS only orchestrates: load agents, build context, delegate to Claude
|
|
6
|
-
*
|
|
7
|
-
* NO scoring logic, NO matching algorithms, NO hardcoded mappings
|
|
2
|
+
* Agent Router
|
|
3
|
+
* Orchestrates agent loading and context building for Claude delegation.
|
|
8
4
|
*
|
|
5
|
+
* @module agentic/agent-router
|
|
9
6
|
* @version 2.0.0
|
|
10
7
|
*/
|
|
11
8
|
|
|
@@ -14,15 +11,22 @@ const path = require('path')
|
|
|
14
11
|
const configManager = require('../infrastructure/config-manager')
|
|
15
12
|
const pathManager = require('../infrastructure/path-manager')
|
|
16
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Routes tasks to specialized agents based on Claude's decisions.
|
|
16
|
+
* Handles agent loading, context building, and usage logging.
|
|
17
|
+
*/
|
|
17
18
|
class AgentRouter {
|
|
18
19
|
constructor() {
|
|
20
|
+
/** @type {string|null} */
|
|
19
21
|
this.projectId = null
|
|
22
|
+
/** @type {string|null} */
|
|
20
23
|
this.agentsPath = null
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
/**
|
|
24
|
-
* Initialize with project context
|
|
25
|
-
*
|
|
27
|
+
* Initialize router with project context
|
|
28
|
+
*
|
|
29
|
+
* @param {string} projectPath - Path to the project
|
|
26
30
|
*/
|
|
27
31
|
async initialize(projectPath) {
|
|
28
32
|
this.projectId = await configManager.getProjectId(projectPath)
|
|
@@ -31,7 +35,8 @@ class AgentRouter {
|
|
|
31
35
|
|
|
32
36
|
/**
|
|
33
37
|
* Load all available agents from project
|
|
34
|
-
*
|
|
38
|
+
*
|
|
39
|
+
* @returns {Promise<Array<{name: string, content: string}>>} Available agents
|
|
35
40
|
*/
|
|
36
41
|
async loadAvailableAgents() {
|
|
37
42
|
try {
|
|
@@ -56,8 +61,9 @@ class AgentRouter {
|
|
|
56
61
|
}
|
|
57
62
|
|
|
58
63
|
/**
|
|
59
|
-
* Get agent names
|
|
60
|
-
*
|
|
64
|
+
* Get list of available agent names
|
|
65
|
+
*
|
|
66
|
+
* @returns {Promise<string[]>} Agent names
|
|
61
67
|
*/
|
|
62
68
|
async getAgentNames() {
|
|
63
69
|
const agents = await this.loadAvailableAgents()
|
|
@@ -65,8 +71,10 @@ class AgentRouter {
|
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
/**
|
|
68
|
-
* Load specific agent by name
|
|
69
|
-
*
|
|
74
|
+
* Load a specific agent by name
|
|
75
|
+
*
|
|
76
|
+
* @param {string} name - Agent name (without .md extension)
|
|
77
|
+
* @returns {Promise<{name: string, content: string}|null>} Agent or null
|
|
70
78
|
*/
|
|
71
79
|
async loadAgent(name) {
|
|
72
80
|
try {
|
|
@@ -79,10 +87,11 @@ class AgentRouter {
|
|
|
79
87
|
}
|
|
80
88
|
|
|
81
89
|
/**
|
|
82
|
-
* Build context for agent assignment
|
|
83
|
-
* ORCHESTRATION: Data gathering only
|
|
90
|
+
* Build context for Claude to decide agent assignment
|
|
84
91
|
*
|
|
85
|
-
*
|
|
92
|
+
* @param {string|Object} task - Task description or object
|
|
93
|
+
* @param {string} projectPath - Project path
|
|
94
|
+
* @returns {Promise<Object>} Assignment context for Claude
|
|
86
95
|
*/
|
|
87
96
|
async buildAssignmentContext(task, projectPath) {
|
|
88
97
|
const agents = await this.getAgentNames()
|
|
@@ -98,8 +107,11 @@ class AgentRouter {
|
|
|
98
107
|
}
|
|
99
108
|
|
|
100
109
|
/**
|
|
101
|
-
* Log agent usage
|
|
102
|
-
*
|
|
110
|
+
* Log agent usage to JSONL file
|
|
111
|
+
*
|
|
112
|
+
* @param {string|Object} task - Task description
|
|
113
|
+
* @param {string|Object} agent - Agent used
|
|
114
|
+
* @param {string} projectPath - Project path (unused, kept for API compat)
|
|
103
115
|
*/
|
|
104
116
|
async logUsage(task, agent, projectPath) {
|
|
105
117
|
try {
|
|
@@ -1,21 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Command Executor
|
|
3
|
-
*
|
|
3
|
+
* Orchestrates command execution with agentic delegation.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* JS only:
|
|
9
|
-
* - Loads templates
|
|
10
|
-
* - Builds context
|
|
11
|
-
* - Returns prompt for Claude
|
|
12
|
-
*
|
|
13
|
-
* Claude:
|
|
14
|
-
* - Reads agent-routing.md
|
|
15
|
-
* - Decides best agent for task
|
|
16
|
-
* - Delegates via Task(subagent_type='general-purpose', prompt='Read: path/to/agent.md...')
|
|
17
|
-
*
|
|
18
|
-
* Source: Claude Code, Devin, Augment Code patterns
|
|
5
|
+
* @module agentic/command-executor
|
|
6
|
+
* @version 3.4
|
|
19
7
|
*/
|
|
20
8
|
|
|
21
9
|
const fs = require('fs')
|
|
@@ -45,14 +33,17 @@ const RUNNING_FILE = path.join(os.homedir(), '.prjct-cli', '.running')
|
|
|
45
33
|
// - code-intelligence → Claude Code has native LSP integration
|
|
46
34
|
// - browser-preview → Claude Code can use Bash directly
|
|
47
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Orchestrates prjct command execution.
|
|
38
|
+
* Handles template loading, context building, validation, and agentic delegation.
|
|
39
|
+
*/
|
|
48
40
|
class CommandExecutor {
|
|
49
|
-
constructor() {
|
|
50
|
-
// 100% AGENTIC: No agent router here
|
|
51
|
-
// Claude decides agent assignment via templates and Task tool
|
|
52
|
-
}
|
|
41
|
+
constructor() {}
|
|
53
42
|
|
|
54
43
|
/**
|
|
55
44
|
* Signal that a command is running (for status line)
|
|
45
|
+
*
|
|
46
|
+
* @param {string} commandName - Name of the running command
|
|
56
47
|
*/
|
|
57
48
|
signalStart(commandName) {
|
|
58
49
|
try {
|
|
@@ -80,7 +71,12 @@ class CommandExecutor {
|
|
|
80
71
|
}
|
|
81
72
|
|
|
82
73
|
/**
|
|
83
|
-
* Execute command with
|
|
74
|
+
* Execute a prjct command with full agentic delegation
|
|
75
|
+
*
|
|
76
|
+
* @param {string} commandName - Command to execute (e.g., 'now', 'ship')
|
|
77
|
+
* @param {Object} params - Command parameters
|
|
78
|
+
* @param {string} projectPath - Path to the project
|
|
79
|
+
* @returns {Promise<Object>} Execution result with prompt, context, helpers
|
|
84
80
|
*/
|
|
85
81
|
async execute(commandName, params, projectPath) {
|
|
86
82
|
// Signal start for status line
|
|
@@ -378,12 +374,12 @@ class CommandExecutor {
|
|
|
378
374
|
}
|
|
379
375
|
|
|
380
376
|
/**
|
|
381
|
-
* Simple execution for direct tool access
|
|
382
|
-
*
|
|
377
|
+
* Simple execution for direct tool access (legacy migration helper)
|
|
378
|
+
*
|
|
383
379
|
* @param {string} commandName - Command name
|
|
384
|
-
* @param {Function} executionFn - Function
|
|
380
|
+
* @param {Function} executionFn - Function receiving (tools, context)
|
|
385
381
|
* @param {string} projectPath - Project path
|
|
386
|
-
* @returns {Promise<Object>}
|
|
382
|
+
* @returns {Promise<Object>} Result with success flag
|
|
387
383
|
*/
|
|
388
384
|
async executeSimple(commandName, executionFn, projectPath) {
|
|
389
385
|
try {
|