prjct-cli 0.44.1 → 0.45.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 +39 -0
- package/bin/prjct.ts +14 -0
- package/core/commands/analysis.ts +239 -0
- package/core/commands/command-data.ts +21 -4
- package/core/commands/commands.ts +4 -0
- package/core/commands/register.ts +1 -0
- package/core/context-tools/files-tool.ts +584 -0
- package/core/context-tools/imports-tool.ts +423 -0
- package/core/context-tools/index.ts +458 -0
- package/core/context-tools/recent-tool.ts +313 -0
- package/core/context-tools/signatures-tool.ts +510 -0
- package/core/context-tools/summary-tool.ts +309 -0
- package/core/context-tools/token-counter.ts +279 -0
- package/core/context-tools/types.ts +253 -0
- package/core/index.ts +4 -0
- package/core/schemas/metrics.ts +143 -0
- package/core/services/sync-service.ts +99 -0
- package/core/storage/index.ts +4 -0
- package/core/storage/metrics-storage.ts +315 -0
- package/core/types/index.ts +3 -0
- package/core/types/storage.ts +49 -0
- package/dist/bin/prjct.mjs +5362 -2825
- package/dist/core/infrastructure/command-installer.js +10 -32
- package/dist/core/infrastructure/setup.js +10 -32
- package/package.json +1 -1
|
@@ -0,0 +1,315 @@
|
|
|
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 { StorageManager } from './storage-manager'
|
|
15
|
+
import { getTimestamp } from '../utils/date-helper'
|
|
16
|
+
import {
|
|
17
|
+
type MetricsJson,
|
|
18
|
+
type DailyStats,
|
|
19
|
+
type AgentUsage,
|
|
20
|
+
DEFAULT_METRICS,
|
|
21
|
+
estimateCostSaved,
|
|
22
|
+
formatCost,
|
|
23
|
+
} from '../schemas/metrics'
|
|
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(`- **Compression**: ${(data.avgCompressionRate * 100).toFixed(0)}% average reduction`)
|
|
60
|
+
lines.push(`- **Estimated cost saved**: ${formatCost(estimateCostSaved(data.totalTokensSaved))}`)
|
|
61
|
+
lines.push('')
|
|
62
|
+
|
|
63
|
+
// Performance
|
|
64
|
+
lines.push('## ⚡ Performance')
|
|
65
|
+
lines.push('')
|
|
66
|
+
lines.push(`- **Syncs completed**: ${data.syncCount.toLocaleString()}`)
|
|
67
|
+
lines.push(`- **Avg sync time**: ${this.formatDuration(data.avgSyncDuration)}`)
|
|
68
|
+
if (data.watchTriggers > 0) {
|
|
69
|
+
lines.push(`- **Watch triggers**: ${data.watchTriggers.toLocaleString()} auto-syncs`)
|
|
70
|
+
}
|
|
71
|
+
lines.push('')
|
|
72
|
+
|
|
73
|
+
// Agent Usage
|
|
74
|
+
if (data.agentUsage.length > 0) {
|
|
75
|
+
lines.push('## 🤖 Agent Usage')
|
|
76
|
+
lines.push('')
|
|
77
|
+
const sortedAgents = [...data.agentUsage].sort((a, b) => b.usageCount - a.usageCount)
|
|
78
|
+
const totalUsage = sortedAgents.reduce((sum, a) => sum + a.usageCount, 0)
|
|
79
|
+
|
|
80
|
+
sortedAgents.slice(0, 5).forEach(agent => {
|
|
81
|
+
const pct = totalUsage > 0 ? ((agent.usageCount / totalUsage) * 100).toFixed(0) : 0
|
|
82
|
+
lines.push(`- **${agent.agentName}**: ${pct}% (${agent.usageCount} uses)`)
|
|
83
|
+
})
|
|
84
|
+
lines.push('')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Trend (last 30 days)
|
|
88
|
+
if (data.dailyStats.length > 0) {
|
|
89
|
+
lines.push('## 📈 30-Day Trend')
|
|
90
|
+
lines.push('')
|
|
91
|
+
const last30 = this.getLast30Days(data.dailyStats)
|
|
92
|
+
const totalLast30 = last30.reduce((sum, d) => sum + d.tokensSaved, 0)
|
|
93
|
+
lines.push(`- **Tokens saved**: ${this.formatTokens(totalLast30)}`)
|
|
94
|
+
lines.push(`- **Syncs**: ${last30.reduce((sum, d) => sum + d.syncs, 0)}`)
|
|
95
|
+
lines.push('')
|
|
96
|
+
lines.push('```')
|
|
97
|
+
lines.push(this.generateSparkline(last30))
|
|
98
|
+
lines.push('```')
|
|
99
|
+
lines.push('')
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Footer
|
|
103
|
+
lines.push('---')
|
|
104
|
+
lines.push('')
|
|
105
|
+
if (data.firstSync) {
|
|
106
|
+
lines.push(`_Tracking since ${new Date(data.firstSync).toLocaleDateString()}_`)
|
|
107
|
+
}
|
|
108
|
+
lines.push('')
|
|
109
|
+
|
|
110
|
+
return lines.join('\n')
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// =========== Domain Methods ===========
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Record a sync event with metrics
|
|
117
|
+
*/
|
|
118
|
+
async recordSync(
|
|
119
|
+
projectId: string,
|
|
120
|
+
metrics: {
|
|
121
|
+
originalSize: number // Tokens before compression
|
|
122
|
+
filteredSize: number // Tokens after compression
|
|
123
|
+
duration: number // Sync duration in ms
|
|
124
|
+
isWatch?: boolean // From watch mode?
|
|
125
|
+
agents?: string[] // Agents used
|
|
126
|
+
}
|
|
127
|
+
): Promise<void> {
|
|
128
|
+
const tokensSaved = Math.max(0, metrics.originalSize - metrics.filteredSize)
|
|
129
|
+
const compressionRate = metrics.originalSize > 0
|
|
130
|
+
? tokensSaved / metrics.originalSize
|
|
131
|
+
: 0
|
|
132
|
+
|
|
133
|
+
const today = new Date().toISOString().split('T')[0]
|
|
134
|
+
|
|
135
|
+
await this.update(projectId, (data) => {
|
|
136
|
+
// Update totals
|
|
137
|
+
const newSyncCount = data.syncCount + 1
|
|
138
|
+
const newTotalTokensSaved = data.totalTokensSaved + tokensSaved
|
|
139
|
+
const newTotalDuration = data.totalSyncDuration + metrics.duration
|
|
140
|
+
|
|
141
|
+
// Running average for compression rate
|
|
142
|
+
const newAvgCompression = data.syncCount === 0
|
|
143
|
+
? compressionRate
|
|
144
|
+
: (data.avgCompressionRate * data.syncCount + compressionRate) / newSyncCount
|
|
145
|
+
|
|
146
|
+
// Update daily stats
|
|
147
|
+
const dailyStats = [...data.dailyStats]
|
|
148
|
+
const todayIndex = dailyStats.findIndex(d => d.date === today)
|
|
149
|
+
|
|
150
|
+
if (todayIndex >= 0) {
|
|
151
|
+
const existing = dailyStats[todayIndex]
|
|
152
|
+
dailyStats[todayIndex] = {
|
|
153
|
+
...existing,
|
|
154
|
+
tokensSaved: existing.tokensSaved + tokensSaved,
|
|
155
|
+
syncs: existing.syncs + 1,
|
|
156
|
+
avgCompressionRate: (existing.avgCompressionRate * existing.syncs + compressionRate) / (existing.syncs + 1),
|
|
157
|
+
totalDuration: existing.totalDuration + metrics.duration,
|
|
158
|
+
}
|
|
159
|
+
} else {
|
|
160
|
+
dailyStats.push({
|
|
161
|
+
date: today,
|
|
162
|
+
tokensSaved,
|
|
163
|
+
syncs: 1,
|
|
164
|
+
avgCompressionRate: compressionRate,
|
|
165
|
+
totalDuration: metrics.duration,
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Keep only last 90 days
|
|
170
|
+
const cutoff = new Date()
|
|
171
|
+
cutoff.setDate(cutoff.getDate() - 90)
|
|
172
|
+
const cutoffStr = cutoff.toISOString().split('T')[0]
|
|
173
|
+
const trimmedStats = dailyStats.filter(d => d.date >= cutoffStr)
|
|
174
|
+
|
|
175
|
+
// Update agent usage
|
|
176
|
+
const agentUsage = [...data.agentUsage]
|
|
177
|
+
if (metrics.agents) {
|
|
178
|
+
for (const agentName of metrics.agents) {
|
|
179
|
+
const idx = agentUsage.findIndex(a => a.agentName === agentName)
|
|
180
|
+
if (idx >= 0) {
|
|
181
|
+
agentUsage[idx] = {
|
|
182
|
+
...agentUsage[idx],
|
|
183
|
+
usageCount: agentUsage[idx].usageCount + 1,
|
|
184
|
+
tokensSaved: agentUsage[idx].tokensSaved + Math.floor(tokensSaved / metrics.agents.length),
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
agentUsage.push({
|
|
188
|
+
agentName,
|
|
189
|
+
usageCount: 1,
|
|
190
|
+
tokensSaved: Math.floor(tokensSaved / metrics.agents.length),
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
totalTokensSaved: newTotalTokensSaved,
|
|
198
|
+
avgCompressionRate: newAvgCompression,
|
|
199
|
+
syncCount: newSyncCount,
|
|
200
|
+
watchTriggers: data.watchTriggers + (metrics.isWatch ? 1 : 0),
|
|
201
|
+
avgSyncDuration: newTotalDuration / newSyncCount,
|
|
202
|
+
totalSyncDuration: newTotalDuration,
|
|
203
|
+
agentUsage,
|
|
204
|
+
dailyStats: trimmedStats,
|
|
205
|
+
firstSync: data.firstSync || getTimestamp(),
|
|
206
|
+
lastUpdated: getTimestamp(),
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Get metrics summary for dashboard
|
|
213
|
+
*/
|
|
214
|
+
async getSummary(projectId: string): Promise<{
|
|
215
|
+
totalTokensSaved: number
|
|
216
|
+
estimatedCostSaved: number
|
|
217
|
+
compressionRate: number
|
|
218
|
+
syncCount: number
|
|
219
|
+
avgSyncDuration: number
|
|
220
|
+
topAgents: AgentUsage[]
|
|
221
|
+
last30DaysTokens: number
|
|
222
|
+
trend: number // Percentage change vs previous 30 days
|
|
223
|
+
}> {
|
|
224
|
+
const data = await this.read(projectId)
|
|
225
|
+
|
|
226
|
+
const last30 = this.getLast30Days(data.dailyStats)
|
|
227
|
+
const prev30 = this.getPrev30Days(data.dailyStats)
|
|
228
|
+
|
|
229
|
+
const last30Tokens = last30.reduce((sum, d) => sum + d.tokensSaved, 0)
|
|
230
|
+
const prev30Tokens = prev30.reduce((sum, d) => sum + d.tokensSaved, 0)
|
|
231
|
+
|
|
232
|
+
const trend = prev30Tokens > 0
|
|
233
|
+
? ((last30Tokens - prev30Tokens) / prev30Tokens) * 100
|
|
234
|
+
: 0
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
totalTokensSaved: data.totalTokensSaved,
|
|
238
|
+
estimatedCostSaved: estimateCostSaved(data.totalTokensSaved),
|
|
239
|
+
compressionRate: data.avgCompressionRate,
|
|
240
|
+
syncCount: data.syncCount,
|
|
241
|
+
avgSyncDuration: data.avgSyncDuration,
|
|
242
|
+
topAgents: [...data.agentUsage].sort((a, b) => b.usageCount - a.usageCount).slice(0, 5),
|
|
243
|
+
last30DaysTokens: last30Tokens,
|
|
244
|
+
trend,
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Get daily stats for a period
|
|
250
|
+
*/
|
|
251
|
+
async getDailyStats(projectId: string, days: number = 30): Promise<DailyStats[]> {
|
|
252
|
+
const data = await this.read(projectId)
|
|
253
|
+
const cutoff = new Date()
|
|
254
|
+
cutoff.setDate(cutoff.getDate() - days)
|
|
255
|
+
const cutoffStr = cutoff.toISOString().split('T')[0]
|
|
256
|
+
|
|
257
|
+
return data.dailyStats
|
|
258
|
+
.filter(d => d.date >= cutoffStr)
|
|
259
|
+
.sort((a, b) => a.date.localeCompare(b.date))
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// =========== Helper Methods ===========
|
|
263
|
+
|
|
264
|
+
private getLast30Days(dailyStats: DailyStats[]): DailyStats[] {
|
|
265
|
+
const cutoff = new Date()
|
|
266
|
+
cutoff.setDate(cutoff.getDate() - 30)
|
|
267
|
+
const cutoffStr = cutoff.toISOString().split('T')[0]
|
|
268
|
+
return dailyStats.filter(d => d.date >= cutoffStr)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private getPrev30Days(dailyStats: DailyStats[]): DailyStats[] {
|
|
272
|
+
const end = new Date()
|
|
273
|
+
end.setDate(end.getDate() - 30)
|
|
274
|
+
const start = new Date()
|
|
275
|
+
start.setDate(start.getDate() - 60)
|
|
276
|
+
|
|
277
|
+
const startStr = start.toISOString().split('T')[0]
|
|
278
|
+
const endStr = end.toISOString().split('T')[0]
|
|
279
|
+
|
|
280
|
+
return dailyStats.filter(d => d.date >= startStr && d.date < endStr)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private formatTokens(tokens: number): string {
|
|
284
|
+
if (tokens >= 1_000_000) {
|
|
285
|
+
return `${(tokens / 1_000_000).toFixed(1)}M`
|
|
286
|
+
}
|
|
287
|
+
if (tokens >= 1_000) {
|
|
288
|
+
return `${(tokens / 1_000).toFixed(1)}K`
|
|
289
|
+
}
|
|
290
|
+
return tokens.toLocaleString()
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private formatDuration(ms: number): string {
|
|
294
|
+
if (ms < 1000) {
|
|
295
|
+
return `${Math.round(ms)}ms`
|
|
296
|
+
}
|
|
297
|
+
return `${(ms / 1000).toFixed(1)}s`
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private generateSparkline(dailyStats: DailyStats[]): string {
|
|
301
|
+
if (dailyStats.length === 0) return ''
|
|
302
|
+
|
|
303
|
+
const chars = '▁▂▃▄▅▆▇█'
|
|
304
|
+
const values = dailyStats.map(d => d.tokensSaved)
|
|
305
|
+
const max = Math.max(...values, 1)
|
|
306
|
+
|
|
307
|
+
return values.map(v => {
|
|
308
|
+
const idx = Math.min(Math.floor((v / max) * (chars.length - 1)), chars.length - 1)
|
|
309
|
+
return chars[idx]
|
|
310
|
+
}).join('')
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export const metricsStorage = new MetricsStorage()
|
|
315
|
+
export default metricsStorage
|
package/core/types/index.ts
CHANGED
package/core/types/storage.ts
CHANGED
|
@@ -146,3 +146,52 @@ export interface IdeasJson {
|
|
|
146
146
|
ideas: Idea[]
|
|
147
147
|
lastUpdated: string
|
|
148
148
|
}
|
|
149
|
+
|
|
150
|
+
// =============================================================================
|
|
151
|
+
// Metrics Storage Types
|
|
152
|
+
// =============================================================================
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Daily stats for trend analysis
|
|
156
|
+
*/
|
|
157
|
+
export interface DailyStats {
|
|
158
|
+
date: string // YYYY-MM-DD
|
|
159
|
+
tokensSaved: number // Tokens saved that day
|
|
160
|
+
syncs: number // Number of syncs
|
|
161
|
+
avgCompressionRate: number // Average compression rate (0-1)
|
|
162
|
+
totalDuration: number // Total sync time in ms
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Agent usage tracking
|
|
167
|
+
*/
|
|
168
|
+
export interface AgentUsage {
|
|
169
|
+
agentName: string // e.g., "backend", "frontend"
|
|
170
|
+
usageCount: number // Times invoked
|
|
171
|
+
tokensSaved: number // Tokens saved by this agent
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Metrics collection for value dashboard
|
|
176
|
+
*/
|
|
177
|
+
export interface MetricsJson {
|
|
178
|
+
// Token metrics
|
|
179
|
+
totalTokensSaved: number
|
|
180
|
+
avgCompressionRate: number // 0-1 (e.g., 0.63 = 63% reduction)
|
|
181
|
+
|
|
182
|
+
// Sync metrics
|
|
183
|
+
syncCount: number
|
|
184
|
+
watchTriggers: number // Auto-syncs from watch mode
|
|
185
|
+
avgSyncDuration: number // Average in ms
|
|
186
|
+
totalSyncDuration: number // Total in ms
|
|
187
|
+
|
|
188
|
+
// Agent usage
|
|
189
|
+
agentUsage: AgentUsage[]
|
|
190
|
+
|
|
191
|
+
// Time series for trends
|
|
192
|
+
dailyStats: DailyStats[]
|
|
193
|
+
|
|
194
|
+
// Metadata
|
|
195
|
+
firstSync: string // ISO8601 - when tracking started
|
|
196
|
+
lastUpdated: string // ISO8601
|
|
197
|
+
}
|