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.
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Context Tools Types
3
+ *
4
+ * Shared interfaces for all context filtering tools.
5
+ * These tools are designed for AI agents to efficiently explore
6
+ * codebases WITHOUT consuming tokens for filtering.
7
+ *
8
+ * @module context-tools/types
9
+ * @version 1.0.0
10
+ */
11
+
12
+ // =============================================================================
13
+ // Token Measurement Types
14
+ // =============================================================================
15
+
16
+ /**
17
+ * Cost savings breakdown by model
18
+ */
19
+ export interface CostBreakdown {
20
+ model: string
21
+ inputSaved: number // $ saved on input tokens
22
+ outputPotential: number // $ potential savings on output (estimated)
23
+ total: number // Combined savings
24
+ }
25
+
26
+ /**
27
+ * Token measurement result
28
+ */
29
+ export interface TokenMetrics {
30
+ tokens: {
31
+ original: number
32
+ filtered: number
33
+ saved: number
34
+ }
35
+ compression: number // 0-1 (e.g., 0.90 = 90% reduction)
36
+ cost: {
37
+ saved: number // $ saved (using default model)
38
+ formatted: string // Human-readable (e.g., "$0.02")
39
+ byModel: CostBreakdown[] // Breakdown by popular models
40
+ }
41
+ }
42
+
43
+ // =============================================================================
44
+ // Files Tool Types
45
+ // =============================================================================
46
+
47
+ /**
48
+ * Relevance score reasons
49
+ */
50
+ export type ScoreReason =
51
+ | `keyword:${string}` // Matched keyword in path
52
+ | `domain:${string}` // Matched domain pattern
53
+ | `recent:${string}` // Recently modified (e.g., "3d" = 3 days)
54
+ | `import:${number}` // Import distance from entry point
55
+ | `extension:${string}` // File extension match
56
+
57
+ /**
58
+ * File with relevance score
59
+ */
60
+ export interface ScoredFile {
61
+ path: string
62
+ score: number // 0-1
63
+ reasons: ScoreReason[]
64
+ }
65
+
66
+ /**
67
+ * Files tool output
68
+ */
69
+ export interface FilesToolOutput {
70
+ files: ScoredFile[]
71
+ metrics: {
72
+ filesScanned: number
73
+ filesReturned: number
74
+ scanDuration: number // ms
75
+ }
76
+ }
77
+
78
+ // =============================================================================
79
+ // Signatures Tool Types
80
+ // =============================================================================
81
+
82
+ /**
83
+ * Code signature types
84
+ */
85
+ export type SignatureType =
86
+ | 'function'
87
+ | 'method'
88
+ | 'class'
89
+ | 'interface'
90
+ | 'type'
91
+ | 'enum'
92
+ | 'const'
93
+ | 'variable'
94
+ | 'export'
95
+ | 'import'
96
+
97
+ /**
98
+ * Extracted code signature
99
+ */
100
+ export interface CodeSignature {
101
+ type: SignatureType
102
+ name: string
103
+ signature: string // Full signature string (e.g., "(token: string) => Promise<User>")
104
+ exported: boolean
105
+ line: number
106
+ docstring?: string
107
+ }
108
+
109
+ /**
110
+ * Signatures tool output
111
+ */
112
+ export interface SignaturesToolOutput {
113
+ file: string
114
+ language: string
115
+ signatures: CodeSignature[]
116
+ fallback: boolean // True if full file was returned (no grammar)
117
+ fallbackReason?: string
118
+ metrics: TokenMetrics
119
+ }
120
+
121
+ // =============================================================================
122
+ // Imports Tool Types
123
+ // =============================================================================
124
+
125
+ /**
126
+ * Import relationship
127
+ */
128
+ export interface ImportRelation {
129
+ source: string // Import path (e.g., "./types", "lodash")
130
+ resolved: string | null // Resolved file path (null for external)
131
+ isExternal: boolean
132
+ importedNames?: string[] // Named imports
133
+ isDefault?: boolean
134
+ isNamespace?: boolean // import * as X
135
+ }
136
+
137
+ /**
138
+ * File that imports the target
139
+ */
140
+ export interface ImportedBy {
141
+ file: string
142
+ importedNames?: string[]
143
+ }
144
+
145
+ /**
146
+ * Dependency tree node
147
+ */
148
+ export interface DependencyNode {
149
+ file: string
150
+ imports: DependencyNode[]
151
+ depth: number
152
+ }
153
+
154
+ /**
155
+ * Imports tool output
156
+ */
157
+ export interface ImportsToolOutput {
158
+ file: string
159
+ imports: ImportRelation[]
160
+ importedBy: ImportedBy[]
161
+ dependencyTree?: DependencyNode
162
+ metrics: {
163
+ totalImports: number
164
+ externalImports: number
165
+ internalImports: number
166
+ importedByCount: number
167
+ }
168
+ }
169
+
170
+ // =============================================================================
171
+ // Recent Tool Types
172
+ // =============================================================================
173
+
174
+ /**
175
+ * Hot file from git analysis
176
+ */
177
+ export interface HotFile {
178
+ path: string
179
+ changes: number // Number of commits touching this file
180
+ heatScore: number // 0-1 normalized score
181
+ lastChanged: string // Human-readable (e.g., "2h ago", "3d ago")
182
+ lastChangedAt: string // ISO timestamp
183
+ }
184
+
185
+ /**
186
+ * Recent tool output
187
+ */
188
+ export interface RecentToolOutput {
189
+ hotFiles: HotFile[]
190
+ branchOnlyFiles: string[] // Files only changed in current branch
191
+ metrics: {
192
+ commitsAnalyzed: number
193
+ totalFilesChanged: number
194
+ filesReturned: number
195
+ analysisWindow: string // e.g., "30 commits", "main..HEAD"
196
+ }
197
+ }
198
+
199
+ // =============================================================================
200
+ // Summary Tool Types
201
+ // =============================================================================
202
+
203
+ /**
204
+ * Public API entry
205
+ */
206
+ export interface PublicAPIEntry {
207
+ name: string
208
+ type: SignatureType
209
+ signature: string
210
+ description?: string // From JSDoc/docstring
211
+ }
212
+
213
+ /**
214
+ * Summary tool output
215
+ */
216
+ export interface SummaryToolOutput {
217
+ file: string
218
+ purpose: string // Short description of file purpose
219
+ publicAPI: PublicAPIEntry[]
220
+ dependencies: string[] // Key dependencies
221
+ metrics: TokenMetrics
222
+ }
223
+
224
+ // =============================================================================
225
+ // Context Tool Usage Tracking
226
+ // =============================================================================
227
+
228
+ /**
229
+ * Tool usage record for metrics
230
+ */
231
+ export interface ContextToolUsage {
232
+ tool: 'files' | 'signatures' | 'imports' | 'recent' | 'summary'
233
+ timestamp: string
234
+ inputArgs: string
235
+ tokensSaved: number
236
+ compressionRate: number
237
+ duration: number // ms
238
+ }
239
+
240
+ // =============================================================================
241
+ // Main Tool Result Type
242
+ // =============================================================================
243
+
244
+ /**
245
+ * Union type for all tool outputs
246
+ */
247
+ export type ContextToolOutput =
248
+ | { tool: 'files'; result: FilesToolOutput }
249
+ | { tool: 'signatures'; result: SignaturesToolOutput }
250
+ | { tool: 'imports'; result: ImportsToolOutput }
251
+ | { tool: 'recent'; result: RecentToolOutput }
252
+ | { tool: 'summary'; result: SummaryToolOutput }
253
+ | { tool: 'error'; result: { error: string; code: string } }
package/core/index.ts CHANGED
@@ -111,6 +111,10 @@ async function main(): Promise<void> {
111
111
  ship: (p) => commands.ship(p),
112
112
  // Analytics
113
113
  dash: (p) => commands.dash(p || 'default'),
114
+ stats: () => commands.stats(process.cwd(), {
115
+ json: options.json === true,
116
+ export: options.export === true,
117
+ }),
114
118
  help: (p) => commands.help(p || ''),
115
119
  // Maintenance
116
120
  recover: () => commands.recover(),
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Metrics Schema
3
+ *
4
+ * Defines the structure for metrics.json - value dashboard metrics.
5
+ * Tracks token savings, sync performance, and usage trends.
6
+ *
7
+ * Uses Zod for runtime validation and TypeScript type inference.
8
+ * @version 1.0.0
9
+ */
10
+
11
+ import { z } from 'zod'
12
+
13
+ // =============================================================================
14
+ // Zod Schemas - Source of Truth
15
+ // =============================================================================
16
+
17
+ /**
18
+ * Daily stats for trend analysis
19
+ */
20
+ export const DailyStatsSchema = z.object({
21
+ date: z.string(), // YYYY-MM-DD
22
+ tokensSaved: z.number(), // Tokens saved that day
23
+ syncs: z.number(), // Number of syncs
24
+ avgCompressionRate: z.number(), // Average compression rate (0-1)
25
+ totalDuration: z.number(), // Total sync time in ms
26
+ })
27
+
28
+ /**
29
+ * Agent usage tracking
30
+ */
31
+ export const AgentUsageSchema = z.object({
32
+ agentName: z.string(), // e.g., "backend", "frontend"
33
+ usageCount: z.number(), // Times invoked
34
+ tokensSaved: z.number(), // Tokens saved by this agent
35
+ })
36
+
37
+ /**
38
+ * Main metrics JSON structure
39
+ */
40
+ export const MetricsJsonSchema = z.object({
41
+ // Token metrics
42
+ totalTokensSaved: z.number(),
43
+ avgCompressionRate: z.number(), // 0-1 (e.g., 0.63 = 63% reduction)
44
+
45
+ // Sync metrics
46
+ syncCount: z.number(),
47
+ watchTriggers: z.number(), // Auto-syncs from watch mode
48
+ avgSyncDuration: z.number(), // Average in ms
49
+ totalSyncDuration: z.number(), // Total in ms
50
+
51
+ // Agent usage
52
+ agentUsage: z.array(AgentUsageSchema),
53
+
54
+ // Time series for trends
55
+ dailyStats: z.array(DailyStatsSchema),
56
+
57
+ // Metadata
58
+ firstSync: z.string(), // ISO8601 - when tracking started
59
+ lastUpdated: z.string(), // ISO8601
60
+ })
61
+
62
+ // =============================================================================
63
+ // Inferred Types
64
+ // =============================================================================
65
+
66
+ export type DailyStats = z.infer<typeof DailyStatsSchema>
67
+ export type AgentUsage = z.infer<typeof AgentUsageSchema>
68
+ export type MetricsJson = z.infer<typeof MetricsJsonSchema>
69
+
70
+ // =============================================================================
71
+ // Validation Helpers
72
+ // =============================================================================
73
+
74
+ /** Parse and validate metrics.json content */
75
+ export const parseMetrics = (data: unknown): MetricsJson => MetricsJsonSchema.parse(data)
76
+
77
+ /** Safe parse with error result */
78
+ export const safeParseMetrics = (data: unknown) => MetricsJsonSchema.safeParse(data)
79
+
80
+ // =============================================================================
81
+ // Defaults
82
+ // =============================================================================
83
+
84
+ export const DEFAULT_METRICS: MetricsJson = {
85
+ totalTokensSaved: 0,
86
+ avgCompressionRate: 0,
87
+ syncCount: 0,
88
+ watchTriggers: 0,
89
+ avgSyncDuration: 0,
90
+ totalSyncDuration: 0,
91
+ agentUsage: [],
92
+ dailyStats: [],
93
+ firstSync: '',
94
+ lastUpdated: '',
95
+ }
96
+
97
+ // =============================================================================
98
+ // Cost Calculation Constants (January 2026 Pricing)
99
+ // =============================================================================
100
+
101
+ /**
102
+ * Token costs per 1000 tokens (INPUT pricing)
103
+ * Source: https://docs.anthropic.com/en/docs/about-claude/models
104
+ *
105
+ * Used for estimating cost savings from context compression
106
+ */
107
+ export const TOKEN_COSTS = {
108
+ // Current models (2026)
109
+ 'claude-opus-4.5': 0.005, // $5/M input - flagship
110
+ 'claude-sonnet-4.5': 0.003, // $3/M input - balanced
111
+ 'claude-haiku-4.5': 0.001, // $1/M input - fastest
112
+ // Legacy models
113
+ 'claude-opus-4': 0.015, // $15/M input
114
+ 'claude-sonnet-4': 0.003, // $3/M input
115
+ 'claude-3-opus': 0.015, // $15/M input (deprecated)
116
+ 'claude-3-sonnet': 0.003, // $3/M input (deprecated)
117
+ // Other providers
118
+ 'gpt-4o': 0.0025, // $2.50/M input
119
+ 'gpt-4': 0.01, // $10/M input (legacy)
120
+ 'gemini-pro': 0.00125, // $1.25/M input
121
+ // Default: Claude Sonnet (most common for Claude Code)
122
+ default: 0.003, // $3/M input
123
+ } as const
124
+
125
+ export type ModelName = keyof typeof TOKEN_COSTS
126
+
127
+ /**
128
+ * Calculate estimated cost saved based on tokens
129
+ */
130
+ export function estimateCostSaved(tokens: number, model: ModelName = 'default'): number {
131
+ const costPer1k = TOKEN_COSTS[model] || TOKEN_COSTS.default
132
+ return (tokens / 1000) * costPer1k
133
+ }
134
+
135
+ /**
136
+ * Format cost as currency string
137
+ */
138
+ export function formatCost(cost: number): string {
139
+ if (cost < 0.01) {
140
+ return `$${(cost * 100).toFixed(2)}¢`
141
+ }
142
+ return `$${cost.toFixed(2)}`
143
+ }
@@ -22,6 +22,7 @@ import { promisify } from 'util'
22
22
  import pathManager from '../infrastructure/path-manager'
23
23
  import configManager from '../infrastructure/config-manager'
24
24
  import dateHelper from '../utils/date-helper'
25
+ import { metricsStorage } from '../storage/metrics-storage'
25
26
  import {
26
27
  generateAIToolContexts,
27
28
  DEFAULT_AI_TOOLS,
@@ -91,6 +92,13 @@ interface AIToolResult {
91
92
  success: boolean
92
93
  }
93
94
 
95
+ interface SyncMetrics {
96
+ duration: number // Sync duration in ms
97
+ originalSize: number // Estimated tokens before compression
98
+ filteredSize: number // Actual tokens in context files
99
+ compressionRate: number // Percentage saved
100
+ }
101
+
94
102
  interface SyncResult {
95
103
  success: boolean
96
104
  projectId: string
@@ -103,6 +111,7 @@ interface SyncResult {
103
111
  skills: { agent: string; skill: string }[]
104
112
  contextFiles: string[]
105
113
  aiTools: AIToolResult[]
114
+ syncMetrics?: SyncMetrics
106
115
  error?: string
107
116
  }
108
117
 
@@ -129,6 +138,7 @@ class SyncService {
129
138
  */
130
139
  async sync(projectPath: string = process.cwd(), options: SyncOptions = {}): Promise<SyncResult> {
131
140
  this.projectPath = projectPath
141
+ const startTime = Date.now()
132
142
 
133
143
  // Resolve AI tools: supports 'auto', 'all', or specific list
134
144
  let aiToolIds: string[]
@@ -217,6 +227,15 @@ class SyncService {
217
227
  // 8. Log to memory
218
228
  await this.logToMemory(git, stats)
219
229
 
230
+ // 9. Record metrics for value dashboard
231
+ const duration = Date.now() - startTime
232
+ const syncMetrics = await this.recordSyncMetrics(
233
+ stats,
234
+ contextFiles,
235
+ agents,
236
+ duration
237
+ )
238
+
220
239
  return {
221
240
  success: true,
222
241
  projectId: this.projectId,
@@ -233,6 +252,7 @@ class SyncService {
233
252
  outputFile: r.outputFile,
234
253
  success: r.success,
235
254
  })),
255
+ syncMetrics,
236
256
  }
237
257
  } catch (error) {
238
258
  return {
@@ -1067,6 +1087,85 @@ ${
1067
1087
  await fs.appendFile(memoryPath, JSON.stringify(event) + '\n', 'utf-8')
1068
1088
  }
1069
1089
 
1090
+ // ==========================================================================
1091
+ // METRICS RECORDING
1092
+ // ==========================================================================
1093
+
1094
+ /**
1095
+ * Record sync metrics for the value dashboard
1096
+ *
1097
+ * Calculates token savings by comparing:
1098
+ * - Original: Estimated tokens if we sent all source files
1099
+ * - Filtered: Actual tokens in generated context files
1100
+ *
1101
+ * Token estimation: ~4 chars per token (industry standard)
1102
+ */
1103
+ private async recordSyncMetrics(
1104
+ stats: ProjectStats,
1105
+ contextFiles: string[],
1106
+ agents: AgentInfo[],
1107
+ duration: number
1108
+ ): Promise<SyncMetrics> {
1109
+ const CHARS_PER_TOKEN = 4
1110
+
1111
+ // Calculate filtered size (actual context files generated)
1112
+ let filteredChars = 0
1113
+ for (const file of contextFiles) {
1114
+ try {
1115
+ const filePath = path.join(this.globalPath, file)
1116
+ const content = await fs.readFile(filePath, 'utf-8')
1117
+ filteredChars += content.length
1118
+ } catch {
1119
+ // File might not exist, skip
1120
+ }
1121
+ }
1122
+
1123
+ // Also count agent files
1124
+ for (const agent of agents) {
1125
+ try {
1126
+ const agentPath = path.join(this.globalPath, 'agents', `${agent.name}.md`)
1127
+ const content = await fs.readFile(agentPath, 'utf-8')
1128
+ filteredChars += content.length
1129
+ } catch {
1130
+ // Skip if not found
1131
+ }
1132
+ }
1133
+
1134
+ const filteredSize = Math.floor(filteredChars / CHARS_PER_TOKEN)
1135
+
1136
+ // Estimate original size (what it would take without prjct)
1137
+ // Conservative estimate: avg 500 tokens per source file
1138
+ // Plus overhead for manually creating context
1139
+ const avgTokensPerFile = 500
1140
+ const originalSize = stats.fileCount * avgTokensPerFile
1141
+
1142
+ // Calculate compression rate
1143
+ const compressionRate = originalSize > 0
1144
+ ? Math.max(0, (originalSize - filteredSize) / originalSize)
1145
+ : 0
1146
+
1147
+ // Record to storage
1148
+ try {
1149
+ await metricsStorage.recordSync(this.projectId!, {
1150
+ originalSize,
1151
+ filteredSize,
1152
+ duration,
1153
+ isWatch: false,
1154
+ agents: agents.filter(a => a.type === 'domain').map(a => a.name),
1155
+ })
1156
+ } catch (error) {
1157
+ // Non-blocking - metrics are nice to have
1158
+ console.error('Warning: Failed to record metrics:', (error as Error).message)
1159
+ }
1160
+
1161
+ return {
1162
+ duration,
1163
+ originalSize,
1164
+ filteredSize,
1165
+ compressionRate,
1166
+ }
1167
+ }
1168
+
1070
1169
  // ==========================================================================
1071
1170
  // HELPERS
1072
1171
  // ==========================================================================
@@ -40,6 +40,7 @@ export { stateStorage } from './state-storage'
40
40
  export { queueStorage } from './queue-storage'
41
41
  export { ideasStorage } from './ideas-storage'
42
42
  export { shippedStorage } from './shipped-storage'
43
+ export { metricsStorage } from './metrics-storage'
43
44
 
44
45
  // ========== GRANULAR STORAGE (Legacy) ==========
45
46
  export { getStorage, default } from './storage'
@@ -53,4 +54,7 @@ export type {
53
54
  IdeaPriority,
54
55
  ShippedFeature,
55
56
  ShippedJson,
57
+ DailyStats,
58
+ AgentUsage,
59
+ MetricsJson,
56
60
  } from '../types'