prjct-cli 0.44.1 → 0.45.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +114 -0
- package/bin/prjct.ts +131 -10
- package/core/__tests__/agentic/memory-system.test.ts +39 -26
- package/core/__tests__/agentic/plan-mode.test.ts +64 -46
- package/core/__tests__/agentic/prompt-builder.test.ts +14 -14
- package/core/__tests__/services/project-index.test.ts +353 -0
- package/core/__tests__/types/fs.test.ts +3 -3
- package/core/__tests__/utils/date-helper.test.ts +10 -10
- package/core/__tests__/utils/output.test.ts +9 -6
- package/core/__tests__/utils/project-commands.test.ts +5 -6
- package/core/agentic/agent-router.ts +9 -10
- package/core/agentic/chain-of-thought.ts +16 -4
- package/core/agentic/command-executor.ts +66 -40
- package/core/agentic/context-builder.ts +8 -5
- package/core/agentic/ground-truth.ts +15 -9
- package/core/agentic/index.ts +145 -152
- package/core/agentic/loop-detector.ts +40 -11
- package/core/agentic/memory-system.ts +98 -35
- package/core/agentic/orchestrator-executor.ts +135 -71
- package/core/agentic/plan-mode.ts +46 -16
- package/core/agentic/prompt-builder.ts +108 -42
- package/core/agentic/services.ts +10 -9
- package/core/agentic/skill-loader.ts +9 -15
- package/core/agentic/smart-context.ts +129 -79
- package/core/agentic/template-executor.ts +13 -12
- package/core/agentic/template-loader.ts +7 -4
- package/core/agentic/tool-registry.ts +16 -13
- package/core/agents/index.ts +1 -1
- package/core/agents/performance.ts +10 -27
- package/core/ai-tools/formatters.ts +8 -6
- package/core/ai-tools/generator.ts +4 -4
- package/core/ai-tools/index.ts +1 -1
- package/core/ai-tools/registry.ts +21 -11
- package/core/bus/bus.ts +23 -16
- package/core/bus/index.ts +2 -2
- package/core/cli/linear.ts +3 -5
- package/core/cli/start.ts +28 -25
- package/core/commands/analysis.ts +287 -29
- package/core/commands/analytics.ts +52 -44
- package/core/commands/base.ts +15 -13
- package/core/commands/cleanup.ts +6 -13
- package/core/commands/command-data.ts +49 -8
- package/core/commands/commands.ts +60 -23
- package/core/commands/context.ts +4 -4
- package/core/commands/design.ts +3 -10
- package/core/commands/index.ts +5 -8
- package/core/commands/maintenance.ts +7 -4
- package/core/commands/planning.ts +179 -56
- package/core/commands/register.ts +14 -9
- package/core/commands/registry.ts +15 -14
- package/core/commands/setup.ts +26 -14
- package/core/commands/shipping.ts +11 -16
- package/core/commands/snapshots.ts +16 -32
- package/core/commands/uninstall.ts +541 -0
- package/core/commands/workflow.ts +24 -28
- package/core/constants/index.ts +10 -22
- package/core/context/generator.ts +82 -33
- package/core/context-tools/files-tool.ts +583 -0
- package/core/context-tools/imports-tool.ts +403 -0
- package/core/context-tools/index.ts +433 -0
- package/core/context-tools/recent-tool.ts +307 -0
- package/core/context-tools/signatures-tool.ts +501 -0
- package/core/context-tools/summary-tool.ts +307 -0
- package/core/context-tools/token-counter.ts +284 -0
- package/core/context-tools/types.ts +253 -0
- package/core/domain/agent-generator.ts +7 -5
- package/core/domain/agent-loader.ts +2 -2
- package/core/domain/analyzer.ts +19 -16
- package/core/domain/architecture-generator.ts +6 -3
- package/core/domain/context-estimator.ts +3 -4
- package/core/domain/snapshot-manager.ts +25 -22
- package/core/domain/task-stack.ts +24 -14
- package/core/errors.ts +1 -1
- package/core/events/events.ts +2 -4
- package/core/events/index.ts +1 -2
- package/core/index.ts +28 -12
- package/core/infrastructure/agent-detector.ts +3 -3
- package/core/infrastructure/ai-provider.ts +23 -20
- package/core/infrastructure/author-detector.ts +16 -10
- package/core/infrastructure/capability-installer.ts +2 -2
- package/core/infrastructure/claude-agent.ts +6 -6
- package/core/infrastructure/command-installer.ts +22 -17
- package/core/infrastructure/config-manager.ts +18 -14
- package/core/infrastructure/editors-config.ts +8 -4
- package/core/infrastructure/path-manager.ts +8 -6
- package/core/infrastructure/permission-manager.ts +20 -17
- package/core/infrastructure/setup.ts +42 -38
- package/core/infrastructure/update-checker.ts +5 -5
- package/core/integrations/issue-tracker/enricher.ts +8 -19
- package/core/integrations/issue-tracker/index.ts +2 -2
- package/core/integrations/issue-tracker/manager.ts +15 -15
- package/core/integrations/issue-tracker/types.ts +5 -22
- package/core/integrations/jira/client.ts +67 -59
- package/core/integrations/jira/index.ts +11 -14
- package/core/integrations/jira/mcp-adapter.ts +5 -10
- package/core/integrations/jira/service.ts +10 -10
- package/core/integrations/linear/client.ts +27 -18
- package/core/integrations/linear/index.ts +9 -12
- package/core/integrations/linear/service.ts +11 -11
- package/core/integrations/linear/sync.ts +8 -8
- package/core/outcomes/analyzer.ts +5 -18
- package/core/outcomes/index.ts +2 -2
- package/core/outcomes/recorder.ts +3 -3
- package/core/plugin/builtin/webhook.ts +19 -15
- package/core/plugin/hooks.ts +29 -21
- package/core/plugin/index.ts +7 -7
- package/core/plugin/loader.ts +19 -19
- package/core/plugin/registry.ts +12 -23
- package/core/schemas/agents.ts +1 -1
- package/core/schemas/analysis.ts +1 -1
- package/core/schemas/enriched-task.ts +62 -49
- package/core/schemas/ideas.ts +13 -13
- package/core/schemas/index.ts +17 -27
- package/core/schemas/issues.ts +40 -25
- package/core/schemas/metrics.ts +143 -0
- package/core/schemas/outcomes.ts +70 -62
- package/core/schemas/permissions.ts +15 -12
- package/core/schemas/prd.ts +27 -14
- package/core/schemas/project.ts +3 -3
- package/core/schemas/roadmap.ts +47 -34
- package/core/schemas/schemas.ts +3 -4
- package/core/schemas/shipped.ts +3 -3
- package/core/schemas/state.ts +43 -29
- package/core/server/index.ts +5 -6
- package/core/server/routes-extended.ts +68 -72
- package/core/server/routes.ts +3 -3
- package/core/server/server.ts +31 -26
- package/core/services/agent-generator.ts +237 -0
- package/core/services/agent-service.ts +2 -2
- package/core/services/breakdown-service.ts +2 -4
- package/core/services/context-generator.ts +299 -0
- package/core/services/context-selector.ts +420 -0
- package/core/services/doctor-service.ts +426 -0
- package/core/services/file-categorizer.ts +448 -0
- package/core/services/file-scorer.ts +270 -0
- package/core/services/git-analyzer.ts +267 -0
- package/core/services/index.ts +27 -10
- package/core/services/memory-service.ts +3 -4
- package/core/services/project-index.ts +911 -0
- package/core/services/project-service.ts +4 -4
- package/core/services/skill-installer.ts +14 -17
- package/core/services/skill-lock.ts +3 -3
- package/core/services/skill-service.ts +12 -6
- package/core/services/stack-detector.ts +245 -0
- package/core/services/sync-service.ts +170 -329
- package/core/services/watch-service.ts +294 -0
- package/core/session/compaction.ts +23 -31
- package/core/session/index.ts +11 -5
- package/core/session/log-migration.ts +3 -3
- package/core/session/metrics.ts +19 -14
- package/core/session/session-log-manager.ts +12 -17
- package/core/session/task-session-manager.ts +25 -25
- package/core/session/utils.ts +1 -1
- package/core/storage/ideas-storage.ts +41 -57
- package/core/storage/index-storage.ts +514 -0
- package/core/storage/index.ts +41 -13
- package/core/storage/metrics-storage.ts +320 -0
- package/core/storage/queue-storage.ts +35 -45
- package/core/storage/shipped-storage.ts +17 -20
- package/core/storage/state-storage.ts +50 -30
- package/core/storage/storage-manager.ts +6 -6
- package/core/storage/storage.ts +18 -15
- package/core/sync/auth-config.ts +3 -3
- package/core/sync/index.ts +13 -19
- package/core/sync/oauth-handler.ts +3 -3
- package/core/sync/sync-client.ts +4 -9
- package/core/sync/sync-manager.ts +12 -14
- package/core/types/commands.ts +42 -7
- package/core/types/index.ts +284 -302
- package/core/types/integrations.ts +3 -3
- package/core/types/storage.ts +49 -0
- package/core/types/utils.ts +3 -3
- package/core/utils/agent-stream.ts +3 -1
- package/core/utils/animations.ts +14 -11
- package/core/utils/branding.ts +7 -7
- package/core/utils/cache.ts +1 -3
- package/core/utils/collection-filters.ts +3 -15
- package/core/utils/date-helper.ts +2 -7
- package/core/utils/file-helper.ts +13 -8
- package/core/utils/jsonl-helper.ts +13 -10
- package/core/utils/keychain.ts +4 -8
- package/core/utils/logger.ts +1 -1
- package/core/utils/next-steps.ts +3 -3
- package/core/utils/output.ts +58 -11
- package/core/utils/project-commands.ts +6 -6
- package/core/utils/project-credentials.ts +5 -12
- package/core/utils/runtime.ts +2 -2
- package/core/utils/session-helper.ts +3 -4
- package/core/utils/version.ts +3 -3
- package/core/wizard/index.ts +13 -0
- package/core/wizard/onboarding.ts +633 -0
- package/core/workflow/state-machine.ts +7 -7
- package/dist/bin/prjct.mjs +18907 -13189
- package/dist/core/infrastructure/command-installer.js +96 -111
- package/dist/core/infrastructure/editors-config.js +6 -6
- package/dist/core/infrastructure/setup.js +256 -257
- package/dist/core/utils/version.js +9 -9
- package/package.json +11 -12
- package/scripts/build.js +3 -3
- package/scripts/postinstall.js +2 -2
- package/templates/mcp-config.json +6 -1
- package/templates/permissions/permissive.jsonc +1 -1
- package/templates/permissions/strict.jsonc +5 -9
- package/templates/global/docs/agents.md +0 -88
- package/templates/global/docs/architecture.md +0 -103
- package/templates/global/docs/commands.md +0 -96
- package/templates/global/docs/validation.md +0 -95
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metrics Storage
|
|
3
|
+
*
|
|
4
|
+
* Manages value dashboard metrics via storage/metrics.json
|
|
5
|
+
* Generates context/metrics.md for Claude
|
|
6
|
+
*
|
|
7
|
+
* Tracks:
|
|
8
|
+
* - Token savings (compression)
|
|
9
|
+
* - Sync performance
|
|
10
|
+
* - Agent usage
|
|
11
|
+
* - Daily trends for visualization
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
type AgentUsage,
|
|
16
|
+
type DailyStats,
|
|
17
|
+
DEFAULT_METRICS,
|
|
18
|
+
estimateCostSaved,
|
|
19
|
+
formatCost,
|
|
20
|
+
type MetricsJson,
|
|
21
|
+
} from '../schemas/metrics'
|
|
22
|
+
import { getTimestamp } from '../utils/date-helper'
|
|
23
|
+
import { StorageManager } from './storage-manager'
|
|
24
|
+
|
|
25
|
+
class MetricsStorage extends StorageManager<MetricsJson> {
|
|
26
|
+
constructor() {
|
|
27
|
+
super('metrics.json')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
protected getDefault(): MetricsJson {
|
|
31
|
+
return { ...DEFAULT_METRICS }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
protected getMdFilename(): string {
|
|
35
|
+
return 'metrics.md'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
protected getLayer(): string {
|
|
39
|
+
return 'context'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
protected getEventType(action: 'update' | 'create' | 'delete'): string {
|
|
43
|
+
return `metrics.${action}d`
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
protected toMarkdown(data: MetricsJson): string {
|
|
47
|
+
const lines = ['# Value Dashboard 📊', '']
|
|
48
|
+
|
|
49
|
+
if (data.syncCount === 0) {
|
|
50
|
+
lines.push('_No metrics yet. Run `prjct sync` to start tracking._')
|
|
51
|
+
lines.push('')
|
|
52
|
+
return lines.join('\n')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Token Savings
|
|
56
|
+
lines.push('## 💰 Token Savings')
|
|
57
|
+
lines.push('')
|
|
58
|
+
lines.push(`- **Total saved**: ${this.formatTokens(data.totalTokensSaved)} tokens`)
|
|
59
|
+
lines.push(
|
|
60
|
+
`- **Compression**: ${(data.avgCompressionRate * 100).toFixed(0)}% average reduction`
|
|
61
|
+
)
|
|
62
|
+
lines.push(
|
|
63
|
+
`- **Estimated cost saved**: ${formatCost(estimateCostSaved(data.totalTokensSaved))}`
|
|
64
|
+
)
|
|
65
|
+
lines.push('')
|
|
66
|
+
|
|
67
|
+
// Performance
|
|
68
|
+
lines.push('## ⚡ Performance')
|
|
69
|
+
lines.push('')
|
|
70
|
+
lines.push(`- **Syncs completed**: ${data.syncCount.toLocaleString()}`)
|
|
71
|
+
lines.push(`- **Avg sync time**: ${this.formatDuration(data.avgSyncDuration)}`)
|
|
72
|
+
if (data.watchTriggers > 0) {
|
|
73
|
+
lines.push(`- **Watch triggers**: ${data.watchTriggers.toLocaleString()} auto-syncs`)
|
|
74
|
+
}
|
|
75
|
+
lines.push('')
|
|
76
|
+
|
|
77
|
+
// Agent Usage
|
|
78
|
+
if (data.agentUsage.length > 0) {
|
|
79
|
+
lines.push('## 🤖 Agent Usage')
|
|
80
|
+
lines.push('')
|
|
81
|
+
const sortedAgents = [...data.agentUsage].sort((a, b) => b.usageCount - a.usageCount)
|
|
82
|
+
const totalUsage = sortedAgents.reduce((sum, a) => sum + a.usageCount, 0)
|
|
83
|
+
|
|
84
|
+
sortedAgents.slice(0, 5).forEach((agent) => {
|
|
85
|
+
const pct = totalUsage > 0 ? ((agent.usageCount / totalUsage) * 100).toFixed(0) : 0
|
|
86
|
+
lines.push(`- **${agent.agentName}**: ${pct}% (${agent.usageCount} uses)`)
|
|
87
|
+
})
|
|
88
|
+
lines.push('')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Trend (last 30 days)
|
|
92
|
+
if (data.dailyStats.length > 0) {
|
|
93
|
+
lines.push('## 📈 30-Day Trend')
|
|
94
|
+
lines.push('')
|
|
95
|
+
const last30 = this.getLast30Days(data.dailyStats)
|
|
96
|
+
const totalLast30 = last30.reduce((sum, d) => sum + d.tokensSaved, 0)
|
|
97
|
+
lines.push(`- **Tokens saved**: ${this.formatTokens(totalLast30)}`)
|
|
98
|
+
lines.push(`- **Syncs**: ${last30.reduce((sum, d) => sum + d.syncs, 0)}`)
|
|
99
|
+
lines.push('')
|
|
100
|
+
lines.push('```')
|
|
101
|
+
lines.push(this.generateSparkline(last30))
|
|
102
|
+
lines.push('```')
|
|
103
|
+
lines.push('')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Footer
|
|
107
|
+
lines.push('---')
|
|
108
|
+
lines.push('')
|
|
109
|
+
if (data.firstSync) {
|
|
110
|
+
lines.push(`_Tracking since ${new Date(data.firstSync).toLocaleDateString()}_`)
|
|
111
|
+
}
|
|
112
|
+
lines.push('')
|
|
113
|
+
|
|
114
|
+
return lines.join('\n')
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// =========== Domain Methods ===========
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Record a sync event with metrics
|
|
121
|
+
*/
|
|
122
|
+
async recordSync(
|
|
123
|
+
projectId: string,
|
|
124
|
+
metrics: {
|
|
125
|
+
originalSize: number // Tokens before compression
|
|
126
|
+
filteredSize: number // Tokens after compression
|
|
127
|
+
duration: number // Sync duration in ms
|
|
128
|
+
isWatch?: boolean // From watch mode?
|
|
129
|
+
agents?: string[] // Agents used
|
|
130
|
+
}
|
|
131
|
+
): Promise<void> {
|
|
132
|
+
const tokensSaved = Math.max(0, metrics.originalSize - metrics.filteredSize)
|
|
133
|
+
const compressionRate = metrics.originalSize > 0 ? tokensSaved / metrics.originalSize : 0
|
|
134
|
+
|
|
135
|
+
const today = new Date().toISOString().split('T')[0]
|
|
136
|
+
|
|
137
|
+
await this.update(projectId, (data) => {
|
|
138
|
+
// Update totals
|
|
139
|
+
const newSyncCount = data.syncCount + 1
|
|
140
|
+
const newTotalTokensSaved = data.totalTokensSaved + tokensSaved
|
|
141
|
+
const newTotalDuration = data.totalSyncDuration + metrics.duration
|
|
142
|
+
|
|
143
|
+
// Running average for compression rate
|
|
144
|
+
const newAvgCompression =
|
|
145
|
+
data.syncCount === 0
|
|
146
|
+
? compressionRate
|
|
147
|
+
: (data.avgCompressionRate * data.syncCount + compressionRate) / newSyncCount
|
|
148
|
+
|
|
149
|
+
// Update daily stats
|
|
150
|
+
const dailyStats = [...data.dailyStats]
|
|
151
|
+
const todayIndex = dailyStats.findIndex((d) => d.date === today)
|
|
152
|
+
|
|
153
|
+
if (todayIndex >= 0) {
|
|
154
|
+
const existing = dailyStats[todayIndex]
|
|
155
|
+
dailyStats[todayIndex] = {
|
|
156
|
+
...existing,
|
|
157
|
+
tokensSaved: existing.tokensSaved + tokensSaved,
|
|
158
|
+
syncs: existing.syncs + 1,
|
|
159
|
+
avgCompressionRate:
|
|
160
|
+
(existing.avgCompressionRate * existing.syncs + compressionRate) / (existing.syncs + 1),
|
|
161
|
+
totalDuration: existing.totalDuration + metrics.duration,
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
dailyStats.push({
|
|
165
|
+
date: today,
|
|
166
|
+
tokensSaved,
|
|
167
|
+
syncs: 1,
|
|
168
|
+
avgCompressionRate: compressionRate,
|
|
169
|
+
totalDuration: metrics.duration,
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Keep only last 90 days
|
|
174
|
+
const cutoff = new Date()
|
|
175
|
+
cutoff.setDate(cutoff.getDate() - 90)
|
|
176
|
+
const cutoffStr = cutoff.toISOString().split('T')[0]
|
|
177
|
+
const trimmedStats = dailyStats.filter((d) => d.date >= cutoffStr)
|
|
178
|
+
|
|
179
|
+
// Update agent usage
|
|
180
|
+
const agentUsage = [...data.agentUsage]
|
|
181
|
+
if (metrics.agents) {
|
|
182
|
+
for (const agentName of metrics.agents) {
|
|
183
|
+
const idx = agentUsage.findIndex((a) => a.agentName === agentName)
|
|
184
|
+
if (idx >= 0) {
|
|
185
|
+
agentUsage[idx] = {
|
|
186
|
+
...agentUsage[idx],
|
|
187
|
+
usageCount: agentUsage[idx].usageCount + 1,
|
|
188
|
+
tokensSaved:
|
|
189
|
+
agentUsage[idx].tokensSaved + Math.floor(tokensSaved / metrics.agents.length),
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
agentUsage.push({
|
|
193
|
+
agentName,
|
|
194
|
+
usageCount: 1,
|
|
195
|
+
tokensSaved: Math.floor(tokensSaved / metrics.agents.length),
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
totalTokensSaved: newTotalTokensSaved,
|
|
203
|
+
avgCompressionRate: newAvgCompression,
|
|
204
|
+
syncCount: newSyncCount,
|
|
205
|
+
watchTriggers: data.watchTriggers + (metrics.isWatch ? 1 : 0),
|
|
206
|
+
avgSyncDuration: newTotalDuration / newSyncCount,
|
|
207
|
+
totalSyncDuration: newTotalDuration,
|
|
208
|
+
agentUsage,
|
|
209
|
+
dailyStats: trimmedStats,
|
|
210
|
+
firstSync: data.firstSync || getTimestamp(),
|
|
211
|
+
lastUpdated: getTimestamp(),
|
|
212
|
+
}
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Get metrics summary for dashboard
|
|
218
|
+
*/
|
|
219
|
+
async getSummary(projectId: string): Promise<{
|
|
220
|
+
totalTokensSaved: number
|
|
221
|
+
estimatedCostSaved: number
|
|
222
|
+
compressionRate: number
|
|
223
|
+
syncCount: number
|
|
224
|
+
avgSyncDuration: number
|
|
225
|
+
topAgents: AgentUsage[]
|
|
226
|
+
last30DaysTokens: number
|
|
227
|
+
trend: number // Percentage change vs previous 30 days
|
|
228
|
+
}> {
|
|
229
|
+
const data = await this.read(projectId)
|
|
230
|
+
|
|
231
|
+
const last30 = this.getLast30Days(data.dailyStats)
|
|
232
|
+
const prev30 = this.getPrev30Days(data.dailyStats)
|
|
233
|
+
|
|
234
|
+
const last30Tokens = last30.reduce((sum, d) => sum + d.tokensSaved, 0)
|
|
235
|
+
const prev30Tokens = prev30.reduce((sum, d) => sum + d.tokensSaved, 0)
|
|
236
|
+
|
|
237
|
+
const trend = prev30Tokens > 0 ? ((last30Tokens - prev30Tokens) / prev30Tokens) * 100 : 0
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
totalTokensSaved: data.totalTokensSaved,
|
|
241
|
+
estimatedCostSaved: estimateCostSaved(data.totalTokensSaved),
|
|
242
|
+
compressionRate: data.avgCompressionRate,
|
|
243
|
+
syncCount: data.syncCount,
|
|
244
|
+
avgSyncDuration: data.avgSyncDuration,
|
|
245
|
+
topAgents: [...data.agentUsage].sort((a, b) => b.usageCount - a.usageCount).slice(0, 5),
|
|
246
|
+
last30DaysTokens: last30Tokens,
|
|
247
|
+
trend,
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Get daily stats for a period
|
|
253
|
+
*/
|
|
254
|
+
async getDailyStats(projectId: string, days: number = 30): Promise<DailyStats[]> {
|
|
255
|
+
const data = await this.read(projectId)
|
|
256
|
+
const cutoff = new Date()
|
|
257
|
+
cutoff.setDate(cutoff.getDate() - days)
|
|
258
|
+
const cutoffStr = cutoff.toISOString().split('T')[0]
|
|
259
|
+
|
|
260
|
+
return data.dailyStats
|
|
261
|
+
.filter((d) => d.date >= cutoffStr)
|
|
262
|
+
.sort((a, b) => a.date.localeCompare(b.date))
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// =========== Helper Methods ===========
|
|
266
|
+
|
|
267
|
+
private getLast30Days(dailyStats: DailyStats[]): DailyStats[] {
|
|
268
|
+
const cutoff = new Date()
|
|
269
|
+
cutoff.setDate(cutoff.getDate() - 30)
|
|
270
|
+
const cutoffStr = cutoff.toISOString().split('T')[0]
|
|
271
|
+
return dailyStats.filter((d) => d.date >= cutoffStr)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private getPrev30Days(dailyStats: DailyStats[]): DailyStats[] {
|
|
275
|
+
const end = new Date()
|
|
276
|
+
end.setDate(end.getDate() - 30)
|
|
277
|
+
const start = new Date()
|
|
278
|
+
start.setDate(start.getDate() - 60)
|
|
279
|
+
|
|
280
|
+
const startStr = start.toISOString().split('T')[0]
|
|
281
|
+
const endStr = end.toISOString().split('T')[0]
|
|
282
|
+
|
|
283
|
+
return dailyStats.filter((d) => d.date >= startStr && d.date < endStr)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private formatTokens(tokens: number): string {
|
|
287
|
+
if (tokens >= 1_000_000) {
|
|
288
|
+
return `${(tokens / 1_000_000).toFixed(1)}M`
|
|
289
|
+
}
|
|
290
|
+
if (tokens >= 1_000) {
|
|
291
|
+
return `${(tokens / 1_000).toFixed(1)}K`
|
|
292
|
+
}
|
|
293
|
+
return tokens.toLocaleString()
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
private formatDuration(ms: number): string {
|
|
297
|
+
if (ms < 1000) {
|
|
298
|
+
return `${Math.round(ms)}ms`
|
|
299
|
+
}
|
|
300
|
+
return `${(ms / 1000).toFixed(1)}s`
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private generateSparkline(dailyStats: DailyStats[]): string {
|
|
304
|
+
if (dailyStats.length === 0) return ''
|
|
305
|
+
|
|
306
|
+
const chars = '▁▂▃▄▅▆▇█'
|
|
307
|
+
const values = dailyStats.map((d) => d.tokensSaved)
|
|
308
|
+
const max = Math.max(...values, 1)
|
|
309
|
+
|
|
310
|
+
return values
|
|
311
|
+
.map((v) => {
|
|
312
|
+
const idx = Math.min(Math.floor((v / max) * (chars.length - 1)), chars.length - 1)
|
|
313
|
+
return chars[idx]
|
|
314
|
+
})
|
|
315
|
+
.join('')
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export const metricsStorage = new MetricsStorage()
|
|
320
|
+
export default metricsStorage
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* Generates context/next.md for Claude
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { StorageManager } from './storage-manager'
|
|
9
8
|
import { generateUUID } from '../schemas'
|
|
9
|
+
import type { Priority, QueueJson, QueueTask, TaskSection } from '../schemas/state'
|
|
10
10
|
import { getTimestamp } from '../utils/date-helper'
|
|
11
|
-
import
|
|
11
|
+
import { StorageManager } from './storage-manager'
|
|
12
12
|
|
|
13
13
|
class QueueStorage extends StorageManager<QueueJson> {
|
|
14
14
|
constructor() {
|
|
@@ -18,7 +18,7 @@ class QueueStorage extends StorageManager<QueueJson> {
|
|
|
18
18
|
protected getDefault(): QueueJson {
|
|
19
19
|
return {
|
|
20
20
|
tasks: [],
|
|
21
|
-
lastUpdated: ''
|
|
21
|
+
lastUpdated: '',
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -37,9 +37,11 @@ class QueueStorage extends StorageManager<QueueJson> {
|
|
|
37
37
|
protected toMarkdown(data: QueueJson): string {
|
|
38
38
|
const lines = ['# Priority Queue', '']
|
|
39
39
|
|
|
40
|
-
const activeTasks = data.tasks.filter(t => t.section === 'active' && !t.completed)
|
|
41
|
-
const backlogTasks = data.tasks.filter(t => t.section === 'backlog' && !t.completed)
|
|
42
|
-
const previouslyActive = data.tasks.filter(
|
|
40
|
+
const activeTasks = data.tasks.filter((t) => t.section === 'active' && !t.completed)
|
|
41
|
+
const backlogTasks = data.tasks.filter((t) => t.section === 'backlog' && !t.completed)
|
|
42
|
+
const previouslyActive = data.tasks.filter(
|
|
43
|
+
(t) => t.section === 'previously_active' && !t.completed
|
|
44
|
+
)
|
|
43
45
|
|
|
44
46
|
// Active section
|
|
45
47
|
lines.push('## Active Tasks')
|
|
@@ -60,7 +62,7 @@ class QueueStorage extends StorageManager<QueueJson> {
|
|
|
60
62
|
// Previously active section (if any)
|
|
61
63
|
if (previouslyActive.length > 0) {
|
|
62
64
|
lines.push('## Previously Active')
|
|
63
|
-
previouslyActive.forEach(task => {
|
|
65
|
+
previouslyActive.forEach((task) => {
|
|
64
66
|
lines.push(`- [ ] ${task.description}`)
|
|
65
67
|
})
|
|
66
68
|
lines.push('')
|
|
@@ -69,7 +71,7 @@ class QueueStorage extends StorageManager<QueueJson> {
|
|
|
69
71
|
// Backlog section
|
|
70
72
|
lines.push('## Backlog')
|
|
71
73
|
if (backlogTasks.length > 0) {
|
|
72
|
-
backlogTasks.forEach(task => {
|
|
74
|
+
backlogTasks.forEach((task) => {
|
|
73
75
|
const priority = task.priority !== 'medium' ? ` [${task.priority.toUpperCase()}]` : ''
|
|
74
76
|
const bug = task.type === 'bug' ? ' \u{1F41B}' : ''
|
|
75
77
|
lines.push(`- [ ]${bug}${priority} ${task.description}`)
|
|
@@ -97,7 +99,7 @@ class QueueStorage extends StorageManager<QueueJson> {
|
|
|
97
99
|
*/
|
|
98
100
|
async getActiveTasks(projectId: string): Promise<QueueTask[]> {
|
|
99
101
|
const queue = await this.read(projectId)
|
|
100
|
-
return queue.tasks.filter(t => t.section === 'active' && !t.completed)
|
|
102
|
+
return queue.tasks.filter((t) => t.section === 'active' && !t.completed)
|
|
101
103
|
}
|
|
102
104
|
|
|
103
105
|
/**
|
|
@@ -105,7 +107,7 @@ class QueueStorage extends StorageManager<QueueJson> {
|
|
|
105
107
|
*/
|
|
106
108
|
async getBacklog(projectId: string): Promise<QueueTask[]> {
|
|
107
109
|
const queue = await this.read(projectId)
|
|
108
|
-
return queue.tasks.filter(t => t.section === 'backlog' && !t.completed)
|
|
110
|
+
return queue.tasks.filter((t) => t.section === 'backlog' && !t.completed)
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
/**
|
|
@@ -127,12 +129,12 @@ class QueueStorage extends StorageManager<QueueJson> {
|
|
|
127
129
|
...task,
|
|
128
130
|
id: generateUUID(),
|
|
129
131
|
createdAt: getTimestamp(),
|
|
130
|
-
completed: false
|
|
132
|
+
completed: false,
|
|
131
133
|
}
|
|
132
134
|
|
|
133
135
|
await this.update(projectId, (queue) => ({
|
|
134
136
|
tasks: [...queue.tasks, newTask],
|
|
135
|
-
lastUpdated: getTimestamp()
|
|
137
|
+
lastUpdated: getTimestamp(),
|
|
136
138
|
}))
|
|
137
139
|
|
|
138
140
|
// Publish incremental event
|
|
@@ -140,7 +142,7 @@ class QueueStorage extends StorageManager<QueueJson> {
|
|
|
140
142
|
taskId: newTask.id,
|
|
141
143
|
description: newTask.description,
|
|
142
144
|
priority: newTask.priority,
|
|
143
|
-
section: newTask.section
|
|
145
|
+
section: newTask.section,
|
|
144
146
|
})
|
|
145
147
|
|
|
146
148
|
return newTask
|
|
@@ -154,22 +156,22 @@ class QueueStorage extends StorageManager<QueueJson> {
|
|
|
154
156
|
tasks: Omit<QueueTask, 'id' | 'createdAt' | 'completed' | 'completedAt'>[]
|
|
155
157
|
): Promise<QueueTask[]> {
|
|
156
158
|
const now = getTimestamp()
|
|
157
|
-
const newTasks: QueueTask[] = tasks.map(task => ({
|
|
159
|
+
const newTasks: QueueTask[] = tasks.map((task) => ({
|
|
158
160
|
...task,
|
|
159
161
|
id: generateUUID(),
|
|
160
162
|
createdAt: now,
|
|
161
|
-
completed: false
|
|
163
|
+
completed: false,
|
|
162
164
|
}))
|
|
163
165
|
|
|
164
166
|
await this.update(projectId, (queue) => ({
|
|
165
167
|
tasks: [...queue.tasks, ...newTasks],
|
|
166
|
-
lastUpdated: now
|
|
168
|
+
lastUpdated: now,
|
|
167
169
|
}))
|
|
168
170
|
|
|
169
171
|
// Publish event for batch add
|
|
170
172
|
await this.publishEvent(projectId, 'queue.tasks_added', {
|
|
171
173
|
count: newTasks.length,
|
|
172
|
-
tasks: newTasks.map(t => ({ id: t.id, description: t.description }))
|
|
174
|
+
tasks: newTasks.map((t) => ({ id: t.id, description: t.description })),
|
|
173
175
|
})
|
|
174
176
|
|
|
175
177
|
return newTasks
|
|
@@ -180,8 +182,8 @@ class QueueStorage extends StorageManager<QueueJson> {
|
|
|
180
182
|
*/
|
|
181
183
|
async removeTask(projectId: string, taskId: string): Promise<void> {
|
|
182
184
|
await this.update(projectId, (queue) => ({
|
|
183
|
-
tasks: queue.tasks.filter(t => t.id !== taskId),
|
|
184
|
-
lastUpdated: getTimestamp()
|
|
185
|
+
tasks: queue.tasks.filter((t) => t.id !== taskId),
|
|
186
|
+
lastUpdated: getTimestamp(),
|
|
185
187
|
}))
|
|
186
188
|
|
|
187
189
|
await this.publishEvent(projectId, 'queue.task_removed', { taskId })
|
|
@@ -194,12 +196,12 @@ class QueueStorage extends StorageManager<QueueJson> {
|
|
|
194
196
|
let completedTask: QueueTask | null = null
|
|
195
197
|
|
|
196
198
|
await this.update(projectId, (queue) => {
|
|
197
|
-
const tasks = queue.tasks.map(t => {
|
|
199
|
+
const tasks = queue.tasks.map((t) => {
|
|
198
200
|
if (t.id === taskId) {
|
|
199
201
|
completedTask = {
|
|
200
202
|
...t,
|
|
201
203
|
completed: true,
|
|
202
|
-
completedAt: getTimestamp()
|
|
204
|
+
completedAt: getTimestamp(),
|
|
203
205
|
}
|
|
204
206
|
return completedTask
|
|
205
207
|
}
|
|
@@ -213,7 +215,7 @@ class QueueStorage extends StorageManager<QueueJson> {
|
|
|
213
215
|
await this.publishEvent(projectId, 'queue.task_completed', {
|
|
214
216
|
taskId,
|
|
215
217
|
description: task.description,
|
|
216
|
-
completedAt: task.completedAt
|
|
218
|
+
completedAt: task.completedAt,
|
|
217
219
|
})
|
|
218
220
|
}
|
|
219
221
|
|
|
@@ -223,32 +225,20 @@ class QueueStorage extends StorageManager<QueueJson> {
|
|
|
223
225
|
/**
|
|
224
226
|
* Move task to different section
|
|
225
227
|
*/
|
|
226
|
-
async moveToSection(
|
|
227
|
-
projectId: string,
|
|
228
|
-
taskId: string,
|
|
229
|
-
section: TaskSection
|
|
230
|
-
): Promise<void> {
|
|
228
|
+
async moveToSection(projectId: string, taskId: string, section: TaskSection): Promise<void> {
|
|
231
229
|
await this.update(projectId, (queue) => ({
|
|
232
|
-
tasks: queue.tasks.map(t =>
|
|
233
|
-
|
|
234
|
-
),
|
|
235
|
-
lastUpdated: getTimestamp()
|
|
230
|
+
tasks: queue.tasks.map((t) => (t.id === taskId ? { ...t, section } : t)),
|
|
231
|
+
lastUpdated: getTimestamp(),
|
|
236
232
|
}))
|
|
237
233
|
}
|
|
238
234
|
|
|
239
235
|
/**
|
|
240
236
|
* Set task priority
|
|
241
237
|
*/
|
|
242
|
-
async setPriority(
|
|
243
|
-
projectId: string,
|
|
244
|
-
taskId: string,
|
|
245
|
-
priority: Priority
|
|
246
|
-
): Promise<void> {
|
|
238
|
+
async setPriority(projectId: string, taskId: string, priority: Priority): Promise<void> {
|
|
247
239
|
await this.update(projectId, (queue) => ({
|
|
248
|
-
tasks: queue.tasks.map(t =>
|
|
249
|
-
|
|
250
|
-
),
|
|
251
|
-
lastUpdated: getTimestamp()
|
|
240
|
+
tasks: queue.tasks.map((t) => (t.id === taskId ? { ...t, priority } : t)),
|
|
241
|
+
lastUpdated: getTimestamp(),
|
|
252
242
|
}))
|
|
253
243
|
}
|
|
254
244
|
|
|
@@ -257,11 +247,11 @@ class QueueStorage extends StorageManager<QueueJson> {
|
|
|
257
247
|
*/
|
|
258
248
|
async clearCompleted(projectId: string): Promise<number> {
|
|
259
249
|
const queue = await this.read(projectId)
|
|
260
|
-
const completedCount = queue.tasks.filter(t => t.completed).length
|
|
250
|
+
const completedCount = queue.tasks.filter((t) => t.completed).length
|
|
261
251
|
|
|
262
252
|
await this.update(projectId, (q) => ({
|
|
263
|
-
tasks: q.tasks.filter(t => !t.completed),
|
|
264
|
-
lastUpdated: getTimestamp()
|
|
253
|
+
tasks: q.tasks.filter((t) => !t.completed),
|
|
254
|
+
lastUpdated: getTimestamp(),
|
|
265
255
|
}))
|
|
266
256
|
|
|
267
257
|
return completedCount
|
|
@@ -275,13 +265,13 @@ class QueueStorage extends StorageManager<QueueJson> {
|
|
|
275
265
|
critical: 0,
|
|
276
266
|
high: 1,
|
|
277
267
|
medium: 2,
|
|
278
|
-
low: 3
|
|
268
|
+
low: 3,
|
|
279
269
|
}
|
|
280
270
|
|
|
281
271
|
const sectionOrder: Record<TaskSection, number> = {
|
|
282
272
|
active: 0,
|
|
283
273
|
previously_active: 1,
|
|
284
|
-
backlog: 2
|
|
274
|
+
backlog: 2,
|
|
285
275
|
}
|
|
286
276
|
|
|
287
277
|
return [...tasks].sort((a, b) => {
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* Generates context/shipped.md for Claude
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { StorageManager } from './storage-manager'
|
|
9
8
|
import { generateUUID } from '../schemas'
|
|
10
|
-
import { getTimestamp } from '../utils/date-helper'
|
|
11
9
|
import type { ShippedFeature, ShippedJson } from '../types'
|
|
10
|
+
import { getTimestamp } from '../utils/date-helper'
|
|
11
|
+
import { StorageManager } from './storage-manager'
|
|
12
12
|
|
|
13
13
|
class ShippedStorage extends StorageManager<ShippedJson> {
|
|
14
14
|
constructor() {
|
|
@@ -18,7 +18,7 @@ class ShippedStorage extends StorageManager<ShippedJson> {
|
|
|
18
18
|
protected getDefault(): ShippedJson {
|
|
19
19
|
return {
|
|
20
20
|
shipped: [],
|
|
21
|
-
lastUpdated: ''
|
|
21
|
+
lastUpdated: '',
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -46,7 +46,7 @@ class ShippedStorage extends StorageManager<ShippedJson> {
|
|
|
46
46
|
// Group by month
|
|
47
47
|
const byMonth = new Map<string, ShippedFeature[]>()
|
|
48
48
|
|
|
49
|
-
data.shipped.forEach(ship => {
|
|
49
|
+
data.shipped.forEach((ship) => {
|
|
50
50
|
const date = new Date(ship.shippedAt)
|
|
51
51
|
const month = date.toLocaleDateString('en-US', { year: 'numeric', month: 'long' })
|
|
52
52
|
|
|
@@ -63,18 +63,18 @@ class ShippedStorage extends StorageManager<ShippedJson> {
|
|
|
63
63
|
return dateB.getTime() - dateA.getTime()
|
|
64
64
|
})
|
|
65
65
|
|
|
66
|
-
sortedMonths.forEach(month => {
|
|
66
|
+
sortedMonths.forEach((month) => {
|
|
67
67
|
lines.push(`## ${month}`)
|
|
68
68
|
lines.push('')
|
|
69
69
|
|
|
70
|
-
const ships = byMonth
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
const ships = byMonth
|
|
71
|
+
.get(month)!
|
|
72
|
+
.sort((a, b) => new Date(b.shippedAt).getTime() - new Date(a.shippedAt).getTime())
|
|
73
73
|
|
|
74
|
-
ships.forEach(ship => {
|
|
74
|
+
ships.forEach((ship) => {
|
|
75
75
|
const date = new Date(ship.shippedAt).toLocaleDateString('en-US', {
|
|
76
76
|
month: 'short',
|
|
77
|
-
day: 'numeric'
|
|
77
|
+
day: 'numeric',
|
|
78
78
|
})
|
|
79
79
|
const version = ship.version ? ` v${ship.version}` : ''
|
|
80
80
|
const duration = ship.duration ? ` (${ship.duration})` : ''
|
|
@@ -126,12 +126,12 @@ class ShippedStorage extends StorageManager<ShippedJson> {
|
|
|
126
126
|
const shipped: ShippedFeature = {
|
|
127
127
|
...feature,
|
|
128
128
|
id: generateUUID(),
|
|
129
|
-
shippedAt: getTimestamp()
|
|
129
|
+
shippedAt: getTimestamp(),
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
await this.update(projectId, (data) => ({
|
|
133
133
|
shipped: [shipped, ...data.shipped], // Prepend
|
|
134
|
-
lastUpdated: getTimestamp()
|
|
134
|
+
lastUpdated: getTimestamp(),
|
|
135
135
|
}))
|
|
136
136
|
|
|
137
137
|
// Publish event
|
|
@@ -139,7 +139,7 @@ class ShippedStorage extends StorageManager<ShippedJson> {
|
|
|
139
139
|
shipId: shipped.id,
|
|
140
140
|
name: shipped.name,
|
|
141
141
|
version: shipped.version,
|
|
142
|
-
shippedAt: shipped.shippedAt
|
|
142
|
+
shippedAt: shipped.shippedAt,
|
|
143
143
|
})
|
|
144
144
|
|
|
145
145
|
return shipped
|
|
@@ -148,12 +148,9 @@ class ShippedStorage extends StorageManager<ShippedJson> {
|
|
|
148
148
|
/**
|
|
149
149
|
* Get shipped by version
|
|
150
150
|
*/
|
|
151
|
-
async getByVersion(
|
|
152
|
-
projectId: string,
|
|
153
|
-
version: string
|
|
154
|
-
): Promise<ShippedFeature | undefined> {
|
|
151
|
+
async getByVersion(projectId: string, version: string): Promise<ShippedFeature | undefined> {
|
|
155
152
|
const data = await this.read(projectId)
|
|
156
|
-
return data.shipped.find(s => s.version === version)
|
|
153
|
+
return data.shipped.find((s) => s.version === version)
|
|
157
154
|
}
|
|
158
155
|
|
|
159
156
|
/**
|
|
@@ -173,7 +170,7 @@ class ShippedStorage extends StorageManager<ShippedJson> {
|
|
|
173
170
|
endDate: Date
|
|
174
171
|
): Promise<ShippedFeature[]> {
|
|
175
172
|
const data = await this.read(projectId)
|
|
176
|
-
return data.shipped.filter(s => {
|
|
173
|
+
return data.shipped.filter((s) => {
|
|
177
174
|
const date = new Date(s.shippedAt)
|
|
178
175
|
return date >= startDate && date <= endDate
|
|
179
176
|
})
|
|
@@ -205,7 +202,7 @@ class ShippedStorage extends StorageManager<ShippedJson> {
|
|
|
205
202
|
|
|
206
203
|
return {
|
|
207
204
|
count: shipped.length,
|
|
208
|
-
period
|
|
205
|
+
period,
|
|
209
206
|
}
|
|
210
207
|
}
|
|
211
208
|
}
|