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 CHANGED
@@ -1,5 +1,44 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.45.0] - 2026-01-29
4
+
5
+ ### Feature: Smart Context Filtering Tools (PRJ-127)
6
+
7
+ Terminal tools for AI agents to explore codebases efficiently **without consuming tokens for filtering**.
8
+
9
+ **New Command: `prjct context <tool>`**
10
+
11
+ | Tool | Purpose | Compression |
12
+ |------|---------|-------------|
13
+ | `files <task>` | Find relevant files for a task | 93% fewer files |
14
+ | `signatures <path>` | Extract code structure only | ~92% token reduction |
15
+ | `imports <file>` | Analyze dependency graphs | - |
16
+ | `recent [commits]` | Find hot files from git | - |
17
+ | `summary <path>` | Intelligent file summary | ~96% token reduction |
18
+
19
+ **Real Cost Savings (multi-model support)**
20
+
21
+ ```json
22
+ "cost": {
23
+ "saved": 2.32,
24
+ "byModel": [
25
+ { "model": "claude-sonnet-4.5", "inputSaved": 0.93, "outputPotential": 1.39 },
26
+ { "model": "gpt-4o", "inputSaved": 0.77, "outputPotential": 0.93 }
27
+ ]
28
+ }
29
+ ```
30
+
31
+ **Example: Full codebase signatures**
32
+ ```bash
33
+ prjct context signatures core/ --recursive
34
+ # 336K → 26K tokens (92% compression)
35
+ # Saves $2.32/call (Sonnet) or $3.87/call (Opus)
36
+ ```
37
+
38
+ **New Files:**
39
+ - `core/context-tools/` - Complete module (7 files)
40
+ - `core/schemas/metrics.ts` - Updated pricing (Jan 2026)
41
+
3
42
  ## [0.44.1] - 2026-01-29
4
43
 
5
44
  ### Fixed: Workflow Templates with Mandatory Steps (PRJ-143)
package/bin/prjct.ts CHANGED
@@ -85,6 +85,20 @@ if (args[0] === 'start' || args[0] === 'setup') {
85
85
  console.error('Server error:', (error as Error).message)
86
86
  process.exitCode = 1
87
87
  }
88
+ } else if (args[0] === 'context') {
89
+ // Context tools - smart context filtering for AI agents
90
+ const projectPath = process.cwd()
91
+ const projectId = await configManager.getProjectId(projectPath)
92
+
93
+ if (!projectId) {
94
+ console.error('No prjct project found. Run "prjct init" first.')
95
+ process.exitCode = 1
96
+ } else {
97
+ const { runContextTool } = await import('../core/context-tools')
98
+ const result = await runContextTool(args.slice(1), projectId, projectPath)
99
+ console.log(JSON.stringify(result, null, 2))
100
+ process.exitCode = result.tool === 'error' ? 1 : 0
101
+ }
88
102
  } else if (args[0] === 'linear') {
89
103
  // Linear CLI subcommand - direct access to Linear SDK
90
104
  const { spawn } = await import('child_process')
@@ -18,6 +18,8 @@ import { generateContext } from '../context/generator'
18
18
  import commandInstaller from '../infrastructure/command-installer'
19
19
  import { syncService } from '../services'
20
20
  import { showNextSteps } from '../utils/next-steps'
21
+ import { metricsStorage } from '../storage/metrics-storage'
22
+ import { formatCost } from '../schemas/metrics'
21
23
 
22
24
  export class AnalysisCommands extends PrjctCommandsBase {
23
25
  /**
@@ -311,4 +313,241 @@ export class AnalysisCommands extends PrjctCommandsBase {
311
313
  }
312
314
  }
313
315
 
316
+ /**
317
+ * /p:stats - Value dashboard showing accumulated savings and impact
318
+ *
319
+ * Displays:
320
+ * - Token savings (total, compression rate, estimated cost)
321
+ * - Performance metrics (sync count, avg duration)
322
+ * - Agent usage breakdown
323
+ * - 30-day trend visualization
324
+ */
325
+ async stats(
326
+ projectPath: string = process.cwd(),
327
+ options: { json?: boolean; export?: boolean } = {}
328
+ ): Promise<CommandResult> {
329
+ try {
330
+ const initResult = await this.ensureProjectInit(projectPath)
331
+ if (!initResult.success) return initResult
332
+
333
+ const projectId = await configManager.getProjectId(projectPath)
334
+ if (!projectId) {
335
+ return { success: false, error: 'No project ID found' }
336
+ }
337
+
338
+ // Get metrics summary
339
+ const summary = await metricsStorage.getSummary(projectId)
340
+ const dailyStats = await metricsStorage.getDailyStats(projectId, 30)
341
+
342
+ // JSON output mode
343
+ if (options.json) {
344
+ const jsonOutput = {
345
+ totalTokensSaved: summary.totalTokensSaved,
346
+ estimatedCostSaved: summary.estimatedCostSaved,
347
+ compressionRate: summary.compressionRate,
348
+ syncCount: summary.syncCount,
349
+ avgSyncDuration: summary.avgSyncDuration,
350
+ topAgents: summary.topAgents,
351
+ last30DaysTokens: summary.last30DaysTokens,
352
+ trend: summary.trend,
353
+ dailyStats,
354
+ }
355
+ console.log(JSON.stringify(jsonOutput, null, 2))
356
+ return { success: true, data: jsonOutput }
357
+ }
358
+
359
+ // Get project info for header
360
+ const globalPath = pathManager.getGlobalProjectPath(projectId)
361
+ let projectName = 'Unknown'
362
+ try {
363
+ const fs = require('fs/promises')
364
+ const path = require('path')
365
+ const projectJson = JSON.parse(
366
+ await fs.readFile(path.join(globalPath, 'project.json'), 'utf-8')
367
+ )
368
+ projectName = projectJson.name || 'Unknown'
369
+ } catch {
370
+ // Use fallback
371
+ }
372
+
373
+ // Determine first sync date
374
+ const metricsData = await metricsStorage.read(projectId)
375
+ const firstSyncDate = metricsData.firstSync
376
+ ? new Date(metricsData.firstSync).toLocaleDateString('en-US', {
377
+ month: 'short',
378
+ day: 'numeric',
379
+ year: 'numeric',
380
+ })
381
+ : 'N/A'
382
+
383
+ // ASCII Dashboard
384
+ console.log('')
385
+ console.log('╭─────────────────────────────────────────────────╮')
386
+ console.log('│ 📊 prjct-cli Value Dashboard │')
387
+ console.log(`│ Project: ${projectName.padEnd(20).slice(0, 20)} | Since: ${firstSyncDate.padEnd(12).slice(0, 12)} │`)
388
+ console.log('╰─────────────────────────────────────────────────╯')
389
+ console.log('')
390
+
391
+ // Token Savings Section
392
+ console.log('💰 TOKEN SAVINGS')
393
+ console.log(` Total saved: ${this._formatTokens(summary.totalTokensSaved)} tokens`)
394
+ console.log(` Compression: ${(summary.compressionRate * 100).toFixed(0)}% average reduction`)
395
+ console.log(` Estimated cost: ${formatCost(summary.estimatedCostSaved)} saved`)
396
+ console.log('')
397
+
398
+ // Performance Section
399
+ console.log('⚡ PERFORMANCE')
400
+ console.log(` Syncs completed: ${summary.syncCount.toLocaleString()}`)
401
+ console.log(` Avg sync time: ${this._formatDuration(summary.avgSyncDuration)}`)
402
+ console.log('')
403
+
404
+ // Agent Usage Section
405
+ if (summary.topAgents.length > 0) {
406
+ console.log('🤖 AGENT USAGE')
407
+ const totalUsage = summary.topAgents.reduce((sum, a) => sum + a.usageCount, 0)
408
+ for (const agent of summary.topAgents) {
409
+ const pct = totalUsage > 0 ? ((agent.usageCount / totalUsage) * 100).toFixed(0) : 0
410
+ console.log(` ${agent.agentName.padEnd(12)}: ${pct}% (${agent.usageCount} uses)`)
411
+ }
412
+ console.log('')
413
+ }
414
+
415
+ // 30-Day Trend Section
416
+ if (dailyStats.length > 0) {
417
+ console.log('📈 TREND (last 30 days)')
418
+ const sparkline = this._generateSparkline(dailyStats)
419
+ console.log(` ${sparkline} ${this._formatTokens(summary.last30DaysTokens)} tokens saved`)
420
+
421
+ if (summary.trend !== 0) {
422
+ const trendIcon = summary.trend > 0 ? '↑' : '↓'
423
+ const trendSign = summary.trend > 0 ? '+' : ''
424
+ console.log(` ${trendIcon} ${trendSign}${summary.trend.toFixed(0)}% vs previous 30 days`)
425
+ }
426
+ console.log('')
427
+ }
428
+
429
+ // Footer
430
+ console.log('───────────────────────────────────────────────────')
431
+ console.log(`Export: prjct stats --export > stats.md`)
432
+ console.log('')
433
+
434
+ // Export mode - return markdown
435
+ if (options.export) {
436
+ const markdown = this._generateStatsMarkdown(summary, dailyStats, projectName, firstSyncDate)
437
+ console.log(markdown)
438
+ return { success: true, data: { markdown } }
439
+ }
440
+
441
+ return {
442
+ success: true,
443
+ data: summary,
444
+ }
445
+ } catch (error) {
446
+ console.error('❌ Error:', (error as Error).message)
447
+ return { success: false, error: (error as Error).message }
448
+ }
449
+ }
450
+
451
+ // =========== Stats Helper Methods ===========
452
+
453
+ private _formatTokens(tokens: number): string {
454
+ if (tokens >= 1_000_000) {
455
+ return `${(tokens / 1_000_000).toFixed(1)}M`
456
+ }
457
+ if (tokens >= 1_000) {
458
+ return `${(tokens / 1_000).toFixed(1)}K`
459
+ }
460
+ return tokens.toLocaleString()
461
+ }
462
+
463
+ private _formatDuration(ms: number): string {
464
+ if (ms < 1000) {
465
+ return `${Math.round(ms)}ms`
466
+ }
467
+ return `${(ms / 1000).toFixed(1)}s`
468
+ }
469
+
470
+ private _generateSparkline(dailyStats: { tokensSaved: number }[]): string {
471
+ if (dailyStats.length === 0) return ''
472
+
473
+ const chars = '▁▂▃▄▅▆▇█'
474
+ const values = dailyStats.map((d) => d.tokensSaved)
475
+ const max = Math.max(...values, 1)
476
+
477
+ return values
478
+ .map((v) => {
479
+ const idx = Math.min(Math.floor((v / max) * (chars.length - 1)), chars.length - 1)
480
+ return chars[idx]
481
+ })
482
+ .join('')
483
+ }
484
+
485
+ private _generateStatsMarkdown(
486
+ summary: {
487
+ totalTokensSaved: number
488
+ estimatedCostSaved: number
489
+ compressionRate: number
490
+ syncCount: number
491
+ avgSyncDuration: number
492
+ topAgents: { agentName: string; usageCount: number }[]
493
+ last30DaysTokens: number
494
+ trend: number
495
+ },
496
+ dailyStats: { date: string; tokensSaved: number; syncs: number }[],
497
+ projectName: string,
498
+ firstSyncDate: string
499
+ ): string {
500
+ const lines: string[] = []
501
+
502
+ lines.push(`# ${projectName} - Value Dashboard`)
503
+ lines.push('')
504
+ lines.push(`_Generated: ${new Date().toLocaleString()} | Tracking since: ${firstSyncDate}_`)
505
+ lines.push('')
506
+
507
+ lines.push('## 💰 Token Savings')
508
+ lines.push('')
509
+ lines.push(`| Metric | Value |`)
510
+ lines.push(`|--------|-------|`)
511
+ lines.push(`| Total saved | ${this._formatTokens(summary.totalTokensSaved)} tokens |`)
512
+ lines.push(`| Compression | ${(summary.compressionRate * 100).toFixed(0)}% |`)
513
+ lines.push(`| Cost saved | ${formatCost(summary.estimatedCostSaved)} |`)
514
+ lines.push('')
515
+
516
+ lines.push('## ⚡ Performance')
517
+ lines.push('')
518
+ lines.push(`| Metric | Value |`)
519
+ lines.push(`|--------|-------|`)
520
+ lines.push(`| Syncs | ${summary.syncCount} |`)
521
+ lines.push(`| Avg time | ${this._formatDuration(summary.avgSyncDuration)} |`)
522
+ lines.push('')
523
+
524
+ if (summary.topAgents.length > 0) {
525
+ lines.push('## 🤖 Agent Usage')
526
+ lines.push('')
527
+ lines.push(`| Agent | Usage |`)
528
+ lines.push(`|-------|-------|`)
529
+ const totalUsage = summary.topAgents.reduce((sum, a) => sum + a.usageCount, 0)
530
+ for (const agent of summary.topAgents) {
531
+ const pct = totalUsage > 0 ? ((agent.usageCount / totalUsage) * 100).toFixed(0) : 0
532
+ lines.push(`| ${agent.agentName} | ${pct}% (${agent.usageCount}) |`)
533
+ }
534
+ lines.push('')
535
+ }
536
+
537
+ lines.push('## 📈 30-Day Trend')
538
+ lines.push('')
539
+ lines.push(`- Tokens saved: ${this._formatTokens(summary.last30DaysTokens)}`)
540
+ if (summary.trend !== 0) {
541
+ const trendSign = summary.trend > 0 ? '+' : ''
542
+ lines.push(`- Trend: ${trendSign}${summary.trend.toFixed(0)}% vs previous period`)
543
+ }
544
+ lines.push('')
545
+
546
+ lines.push('---')
547
+ lines.push('')
548
+ lines.push('_Generated with [prjct-cli](https://prjct.app)_')
549
+
550
+ return lines.join('\n')
551
+ }
552
+
314
553
  }
@@ -142,6 +142,17 @@ export const COMMANDS: CommandMeta[] = [
142
142
  hasTemplate: true,
143
143
  requiresProject: true,
144
144
  },
145
+ {
146
+ name: 'stats',
147
+ group: 'core',
148
+ description: 'Value dashboard - token savings, performance, and impact',
149
+ usage: { claude: '/p:stats', terminal: 'prjct stats' },
150
+ params: '[--json] [--export]',
151
+ implemented: true,
152
+ hasTemplate: true,
153
+ requiresProject: true,
154
+ features: ['Token savings tracking', 'Compression metrics', 'Cost estimates', '30-day trends'],
155
+ },
145
156
  {
146
157
  name: 'sync',
147
158
  group: 'core',
@@ -308,13 +319,19 @@ export const COMMANDS: CommandMeta[] = [
308
319
  {
309
320
  name: 'context',
310
321
  group: 'setup',
311
- description: 'Get project context as JSON for Claude templates',
312
- usage: { claude: null, terminal: 'prjct context <command> [args]' },
313
- params: '<command> [args]',
322
+ description: 'Smart context filtering tools for AI agents',
323
+ usage: { claude: null, terminal: 'prjct context <tool> [args]' },
324
+ params: '<tool> [args]',
314
325
  implemented: true,
315
326
  hasTemplate: false,
316
327
  requiresProject: true,
317
- features: ['Returns JSON with project context', 'Runs orchestrator', 'Loads agents and skills'],
328
+ features: [
329
+ 'files - Find relevant files for a task',
330
+ 'signatures - Extract code structure (~90% compression)',
331
+ 'imports - Analyze dependency graphs',
332
+ 'recent - Find hot files from git history',
333
+ 'summary - Intelligent file summarization',
334
+ ],
318
335
  },
319
336
  ]
320
337
 
@@ -158,6 +158,10 @@ class PrjctCommands {
158
158
  return this.analysis.sync(projectPath, options)
159
159
  }
160
160
 
161
+ async stats(projectPath: string = process.cwd(), options: { json?: boolean; export?: boolean } = {}): Promise<CommandResult> {
162
+ return this.analysis.stats(projectPath, options)
163
+ }
164
+
161
165
  // ========== Context Commands ==========
162
166
 
163
167
  async context(input: string | null = null, projectPath: string = process.cwd()): Promise<CommandResult> {
@@ -81,6 +81,7 @@ export function registerAllCommands(): void {
81
81
  // Analysis commands
82
82
  commandRegistry.registerMethod('analyze', analysis, 'analyze', getMeta('analyze'))
83
83
  commandRegistry.registerMethod('sync', analysis, 'sync', getMeta('sync'))
84
+ commandRegistry.registerMethod('stats', analysis, 'stats', getMeta('stats'))
84
85
 
85
86
  // Setup commands
86
87
  commandRegistry.registerMethod('start', setup, 'start', getMeta('start'))