@bahulam/code 0.1.1
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/README.md +93 -0
- package/package.json +56 -0
- package/pulse/app/activity/page.tsx +190 -0
- package/pulse/app/api/activity/route.ts +138 -0
- package/pulse/app/api/benchmark/route.ts +113 -0
- package/pulse/app/api/benchmarks/route.ts +195 -0
- package/pulse/app/api/costs/route.ts +88 -0
- package/pulse/app/api/export/route.ts +77 -0
- package/pulse/app/api/history/route.ts +11 -0
- package/pulse/app/api/import/route.ts +31 -0
- package/pulse/app/api/memory/route.ts +50 -0
- package/pulse/app/api/plans/route.ts +9 -0
- package/pulse/app/api/projects/[slug]/route.ts +96 -0
- package/pulse/app/api/projects/route.ts +121 -0
- package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
- package/pulse/app/api/sessions/[id]/route.ts +31 -0
- package/pulse/app/api/sessions/route.ts +112 -0
- package/pulse/app/api/settings/route.ts +14 -0
- package/pulse/app/api/stats/route.ts +143 -0
- package/pulse/app/api/todos/route.ts +9 -0
- package/pulse/app/api/tools/route.ts +160 -0
- package/pulse/app/benchmarks/page.tsx +224 -0
- package/pulse/app/costs/page.tsx +179 -0
- package/pulse/app/export/page.tsx +465 -0
- package/pulse/app/favicon.ico +0 -0
- package/pulse/app/globals.css +263 -0
- package/pulse/app/help/page.tsx +143 -0
- package/pulse/app/history/page.tsx +157 -0
- package/pulse/app/layout.tsx +46 -0
- package/pulse/app/memory/page.tsx +365 -0
- package/pulse/app/overview-client.tsx +393 -0
- package/pulse/app/page.tsx +14 -0
- package/pulse/app/plans/page.tsx +308 -0
- package/pulse/app/projects/[slug]/page.tsx +390 -0
- package/pulse/app/projects/page.tsx +110 -0
- package/pulse/app/sessions/[id]/page.tsx +243 -0
- package/pulse/app/sessions/page.tsx +39 -0
- package/pulse/app/settings/page.tsx +188 -0
- package/pulse/app/todos/page.tsx +211 -0
- package/pulse/app/tools/page.tsx +249 -0
- package/pulse/cli.js +164 -0
- package/pulse/components/activity/day-of-week-chart.tsx +35 -0
- package/pulse/components/activity/streak-card.tsx +36 -0
- package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
- package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
- package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
- package/pulse/components/costs/model-token-table.tsx +60 -0
- package/pulse/components/global-search.tsx +193 -0
- package/pulse/components/keyboard-nav-provider.tsx +23 -0
- package/pulse/components/layout/bottom-nav.tsx +53 -0
- package/pulse/components/layout/client-layout.tsx +31 -0
- package/pulse/components/layout/sidebar-context.tsx +50 -0
- package/pulse/components/layout/sidebar.tsx +183 -0
- package/pulse/components/layout/top-bar.tsx +121 -0
- package/pulse/components/overview/activity-heatmap.tsx +107 -0
- package/pulse/components/overview/conversation-table.tsx +148 -0
- package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
- package/pulse/components/overview/peak-hours-chart.tsx +87 -0
- package/pulse/components/overview/project-activity-donut.tsx +96 -0
- package/pulse/components/overview/stat-card.tsx +102 -0
- package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
- package/pulse/components/projects/project-card.tsx +175 -0
- package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
- package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
- package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
- package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
- package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
- package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
- package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
- package/pulse/components/sessions/session-badges.tsx +49 -0
- package/pulse/components/sessions/session-table.tsx +299 -0
- package/pulse/components/theme-provider.tsx +44 -0
- package/pulse/components/tools/feature-adoption-table.tsx +58 -0
- package/pulse/components/tools/mcp-server-panel.tsx +45 -0
- package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
- package/pulse/components/tools/version-history-table.tsx +32 -0
- package/pulse/components/ui/alert.tsx +66 -0
- package/pulse/components/ui/badge.tsx +48 -0
- package/pulse/components/ui/breadcrumb.tsx +109 -0
- package/pulse/components/ui/button.tsx +64 -0
- package/pulse/components/ui/calendar.tsx +220 -0
- package/pulse/components/ui/card.tsx +92 -0
- package/pulse/components/ui/command.tsx +158 -0
- package/pulse/components/ui/dialog.tsx +158 -0
- package/pulse/components/ui/input.tsx +21 -0
- package/pulse/components/ui/popover.tsx +89 -0
- package/pulse/components/ui/progress.tsx +31 -0
- package/pulse/components/ui/select.tsx +190 -0
- package/pulse/components/ui/separator.tsx +28 -0
- package/pulse/components/ui/sheet.tsx +143 -0
- package/pulse/components/ui/skeleton.tsx +13 -0
- package/pulse/components/ui/table.tsx +116 -0
- package/pulse/components/ui/tabs.tsx +91 -0
- package/pulse/components/ui/tooltip.tsx +57 -0
- package/pulse/components/use-global-keyboard-nav.ts +79 -0
- package/pulse/components.json +23 -0
- package/pulse/eslint.config.mjs +18 -0
- package/pulse/lib/bahulam-paths.ts +23 -0
- package/pulse/lib/claude-reader.ts +592 -0
- package/pulse/lib/decode.ts +129 -0
- package/pulse/lib/pricing.ts +102 -0
- package/pulse/lib/replay-parser.ts +165 -0
- package/pulse/lib/tool-categories.ts +127 -0
- package/pulse/lib/utils.ts +6 -0
- package/pulse/next-env.d.ts +6 -0
- package/pulse/next.config.ts +16 -0
- package/pulse/package.json +45 -0
- package/pulse/postcss.config.mjs +7 -0
- package/pulse/public/activity.png +0 -0
- package/pulse/public/cc-lens.png +0 -0
- package/pulse/public/command-k.png +0 -0
- package/pulse/public/costs.png +0 -0
- package/pulse/public/dashboard-dark.png +0 -0
- package/pulse/public/dashboard-white.png +0 -0
- package/pulse/public/export.png +0 -0
- package/pulse/public/file.svg +1 -0
- package/pulse/public/globe.svg +1 -0
- package/pulse/public/next.svg +1 -0
- package/pulse/public/projects.png +0 -0
- package/pulse/public/session-chat.png +0 -0
- package/pulse/public/todos.png +0 -0
- package/pulse/public/tools.png +0 -0
- package/pulse/public/vercel.svg +1 -0
- package/pulse/public/window.svg +1 -0
- package/pulse/tsconfig.json +34 -0
- package/pulse/types/claude.ts +294 -0
- package/src/agents/loader.mjs +94 -0
- package/src/agents/multi_workflow_loader.mjs +330 -0
- package/src/agents/parser.mjs +205 -0
- package/src/agents/scaffold.mjs +223 -0
- package/src/agents/teams.mjs +123 -0
- package/src/agents/workflow_loader.mjs +122 -0
- package/src/agents/workflow_scaffold.mjs +249 -0
- package/src/auth/oauth.mjs +220 -0
- package/src/auth/tarang-auth.mjs +312 -0
- package/src/commands/agent.mjs +221 -0
- package/src/commands/workflow.mjs +581 -0
- package/src/config/cli-args.mjs +202 -0
- package/src/config/env.mjs +263 -0
- package/src/config/hook-runner.mjs +100 -0
- package/src/config/memory-loader.mjs +32 -0
- package/src/config/model-catalog.mjs +57 -0
- package/src/config/settings-loader.mjs +45 -0
- package/src/config/settings.mjs +132 -0
- package/src/context/ast-parser.mjs +298 -0
- package/src/context/bm25.mjs +85 -0
- package/src/context/prose-chunker.mjs +255 -0
- package/src/context/retriever.mjs +425 -0
- package/src/context/skeleton.mjs +134 -0
- package/src/context/symbol-indexer.mjs +375 -0
- package/src/core/agent-history.mjs +111 -0
- package/src/core/agent-loop.mjs +486 -0
- package/src/core/approval-log.mjs +145 -0
- package/src/core/approval.mjs +700 -0
- package/src/core/attachments.mjs +666 -0
- package/src/core/backend-url.mjs +68 -0
- package/src/core/bundled-runtime.mjs +418 -0
- package/src/core/cache-control.mjs +92 -0
- package/src/core/cache.mjs +105 -0
- package/src/core/callback-client.mjs +180 -0
- package/src/core/checkpoints.mjs +142 -0
- package/src/core/compact-history.mjs +127 -0
- package/src/core/context-envelope.mjs +54 -0
- package/src/core/context-manager.mjs +198 -0
- package/src/core/error-guidance.mjs +331 -0
- package/src/core/file-diff.mjs +217 -0
- package/src/core/headless.mjs +460 -0
- package/src/core/hooks-manager.mjs +87 -0
- package/src/core/jsonl-writer.mjs +449 -0
- package/src/core/local-agent.mjs +538 -0
- package/src/core/local-store.mjs +836 -0
- package/src/core/mode-selector.mjs +51 -0
- package/src/core/output-filter.mjs +177 -0
- package/src/core/paths.mjs +190 -0
- package/src/core/policy-resolver.mjs +156 -0
- package/src/core/pricing.mjs +336 -0
- package/src/core/project-artifacts.mjs +39 -0
- package/src/core/project-context-loader.mjs +139 -0
- package/src/core/providers.mjs +219 -0
- package/src/core/rate-limit-display.mjs +121 -0
- package/src/core/rate-limiter.mjs +119 -0
- package/src/core/resume-mode.mjs +192 -0
- package/src/core/risk-tier.mjs +388 -0
- package/src/core/safety.mjs +260 -0
- package/src/core/scheduler.mjs +173 -0
- package/src/core/session-manager.mjs +360 -0
- package/src/core/session.mjs +143 -0
- package/src/core/settings-sync.mjs +85 -0
- package/src/core/stagnation.mjs +57 -0
- package/src/core/stream-client.mjs +957 -0
- package/src/core/streaming.mjs +182 -0
- package/src/core/system-prompt.mjs +140 -0
- package/src/core/tasks.mjs +196 -0
- package/src/core/tool-executor.mjs +2231 -0
- package/src/core/trust.mjs +160 -0
- package/src/core/work-scope.mjs +248 -0
- package/src/hooks/engine.mjs +162 -0
- package/src/mcp/client.mjs +253 -0
- package/src/mcp/transport-shttp.mjs +130 -0
- package/src/mcp/transport-sse.mjs +131 -0
- package/src/mcp/transport-ws.mjs +134 -0
- package/src/onboarding/preflight.mjs +374 -0
- package/src/permissions/checker.mjs +57 -0
- package/src/permissions/command-classifier.mjs +700 -0
- package/src/permissions/injection-check.mjs +60 -0
- package/src/permissions/path-check.mjs +102 -0
- package/src/permissions/prompt.mjs +73 -0
- package/src/permissions/sandbox.mjs +112 -0
- package/src/plugins/loader.mjs +138 -0
- package/src/skills/installer.mjs +188 -0
- package/src/skills/loader.mjs +252 -0
- package/src/skills/runner.mjs +55 -0
- package/src/state/orbit.mjs +263 -0
- package/src/state/verbosity.mjs +99 -0
- package/src/telemetry/index.mjs +122 -0
- package/src/terminal/agents.mjs +353 -0
- package/src/terminal/analytics.mjs +292 -0
- package/src/terminal/ansi.mjs +695 -0
- package/src/terminal/init.mjs +145 -0
- package/src/terminal/main.mjs +310 -0
- package/src/terminal/repl-ask-form.mjs +120 -0
- package/src/terminal/repl-explore.mjs +44 -0
- package/src/terminal/repl-format.mjs +317 -0
- package/src/terminal/repl-model-form.mjs +132 -0
- package/src/terminal/repl-render.mjs +833 -0
- package/src/terminal/repl-resume.mjs +640 -0
- package/src/terminal/repl-state.mjs +120 -0
- package/src/terminal/repl-utils.mjs +34 -0
- package/src/terminal/repl.mjs +5032 -0
- package/src/terminal/skills.mjs +54 -0
- package/src/terminal/tool-display.mjs +392 -0
- package/src/tools/agent.mjs +137 -0
- package/src/tools/ask-user.mjs +61 -0
- package/src/tools/bash.mjs +231 -0
- package/src/tools/cron-create.mjs +120 -0
- package/src/tools/cron-delete.mjs +49 -0
- package/src/tools/cron-list.mjs +37 -0
- package/src/tools/edit.mjs +82 -0
- package/src/tools/enter-worktree.mjs +69 -0
- package/src/tools/exit-worktree.mjs +57 -0
- package/src/tools/glob.mjs +117 -0
- package/src/tools/grep.mjs +129 -0
- package/src/tools/lint.mjs +71 -0
- package/src/tools/ls.mjs +58 -0
- package/src/tools/lsp.mjs +115 -0
- package/src/tools/multi-edit.mjs +94 -0
- package/src/tools/notebook-edit.mjs +96 -0
- package/src/tools/project-overview.mjs +703 -0
- package/src/tools/read-mcp-resource.mjs +57 -0
- package/src/tools/read.mjs +138 -0
- package/src/tools/registry.mjs +116 -0
- package/src/tools/remote-trigger.mjs +84 -0
- package/src/tools/send-message.mjs +64 -0
- package/src/tools/skill.mjs +52 -0
- package/src/tools/test-runner.mjs +49 -0
- package/src/tools/todo-write.mjs +68 -0
- package/src/tools/tool-search.mjs +77 -0
- package/src/tools/web-fetch.mjs +65 -0
- package/src/tools/web-search.mjs +89 -0
- package/src/tools/write.mjs +55 -0
- package/src/ui/approval.mjs +510 -0
- package/src/ui/banner.mjs +232 -0
- package/src/ui/commands.mjs +537 -0
- package/src/ui/formatter.mjs +409 -0
- package/src/ui/icons.mjs +170 -0
- package/src/ui/input-dock.mjs +772 -0
- package/src/ui/markdown.mjs +278 -0
- package/src/ui/mission-report.mjs +296 -0
- package/src/ui/palette.mjs +189 -0
- package/src/ui/render-queue.mjs +500 -0
- package/src/ui/slash-commands.mjs +257 -0
- package/src/ui/spinner.mjs +116 -0
- package/src/ui/sub-agent.mjs +167 -0
- package/src/ui/term.mjs +174 -0
- package/src/ui/text-layout.mjs +127 -0
- package/src/ui/tool-card.mjs +740 -0
- package/src/ui/tool-details.mjs +504 -0
- package/src/ui/transcript-block.mjs +20 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
|
|
5
|
+
export const dynamic = 'force-dynamic'
|
|
6
|
+
|
|
7
|
+
interface BenchmarkResult {
|
|
8
|
+
instance_id: string
|
|
9
|
+
repo: string
|
|
10
|
+
model: string
|
|
11
|
+
timestamp: string
|
|
12
|
+
kepler: {
|
|
13
|
+
status: string
|
|
14
|
+
exit_code: number
|
|
15
|
+
duration_seconds: number
|
|
16
|
+
tokens_used: number
|
|
17
|
+
cost: number
|
|
18
|
+
tool_calls: number
|
|
19
|
+
sub_agents: string[]
|
|
20
|
+
}
|
|
21
|
+
patch_lines: number
|
|
22
|
+
model_patch: string
|
|
23
|
+
status: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface BenchmarkStats {
|
|
27
|
+
total_runs: number
|
|
28
|
+
passed: number
|
|
29
|
+
failed: number
|
|
30
|
+
error: number
|
|
31
|
+
success_rate: number
|
|
32
|
+
avg_duration: number
|
|
33
|
+
total_cost: number
|
|
34
|
+
total_tokens: number
|
|
35
|
+
avg_tokens_per_run: number
|
|
36
|
+
by_status: Record<string, number>
|
|
37
|
+
by_repo: Record<string, { count: number; passed: number; success_rate: number }>
|
|
38
|
+
by_model: Record<string, { count: number; passed: number; success_rate: number }>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function loadBenchmarkResults(): Promise<BenchmarkResult[]> {
|
|
42
|
+
try {
|
|
43
|
+
const resultsPath = path.join(
|
|
44
|
+
process.cwd(),
|
|
45
|
+
'benchmark/results/runs/swebench-v4-flash-300/harness-results.json'
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
if (!fs.existsSync(resultsPath)) {
|
|
49
|
+
return []
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const data = JSON.parse(fs.readFileSync(resultsPath, 'utf-8'))
|
|
53
|
+
return data.results || []
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error('Error loading benchmark results:', error)
|
|
56
|
+
return []
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function calculateStats(results: BenchmarkResult[]): BenchmarkStats {
|
|
61
|
+
if (results.length === 0) {
|
|
62
|
+
return {
|
|
63
|
+
total_runs: 0,
|
|
64
|
+
passed: 0,
|
|
65
|
+
failed: 0,
|
|
66
|
+
error: 0,
|
|
67
|
+
success_rate: 0,
|
|
68
|
+
avg_duration: 0,
|
|
69
|
+
total_cost: 0,
|
|
70
|
+
total_tokens: 0,
|
|
71
|
+
avg_tokens_per_run: 0,
|
|
72
|
+
by_status: {},
|
|
73
|
+
by_repo: {},
|
|
74
|
+
by_model: {},
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const by_status: Record<string, number> = {}
|
|
79
|
+
const by_repo: Record<string, { count: number; passed: number }> = {}
|
|
80
|
+
const by_model: Record<string, { count: number; passed: number }> = {}
|
|
81
|
+
|
|
82
|
+
let total_cost = 0
|
|
83
|
+
let total_tokens = 0
|
|
84
|
+
let total_duration = 0
|
|
85
|
+
let passed = 0
|
|
86
|
+
|
|
87
|
+
results.forEach((result) => {
|
|
88
|
+
// Count by status
|
|
89
|
+
by_status[result.status] = (by_status[result.status] || 0) + 1
|
|
90
|
+
|
|
91
|
+
// Count by repo
|
|
92
|
+
if (!by_repo[result.repo]) {
|
|
93
|
+
by_repo[result.repo] = { count: 0, passed: 0 }
|
|
94
|
+
}
|
|
95
|
+
by_repo[result.repo].count++
|
|
96
|
+
|
|
97
|
+
// Count by model
|
|
98
|
+
if (!by_model[result.model]) {
|
|
99
|
+
by_model[result.model] = { count: 0, passed: 0 }
|
|
100
|
+
}
|
|
101
|
+
by_model[result.model].count++
|
|
102
|
+
|
|
103
|
+
// Aggregate metrics
|
|
104
|
+
if (result.kepler) {
|
|
105
|
+
total_cost += result.kepler.cost || 0
|
|
106
|
+
total_tokens += result.kepler.tokens_used || 0
|
|
107
|
+
total_duration += result.kepler.duration_seconds || 0
|
|
108
|
+
|
|
109
|
+
if (result.kepler.status === 'success') {
|
|
110
|
+
passed++
|
|
111
|
+
by_repo[result.repo].passed++
|
|
112
|
+
by_model[result.model].passed++
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
// Calculate success rates
|
|
118
|
+
const by_repo_with_rates = Object.entries(by_repo).reduce(
|
|
119
|
+
(acc, [repo, data]) => {
|
|
120
|
+
acc[repo] = {
|
|
121
|
+
...data,
|
|
122
|
+
success_rate: data.count > 0 ? (data.passed / data.count) * 100 : 0,
|
|
123
|
+
}
|
|
124
|
+
return acc
|
|
125
|
+
},
|
|
126
|
+
{} as Record<string, { count: number; passed: number; success_rate: number }>
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
const by_model_with_rates = Object.entries(by_model).reduce(
|
|
130
|
+
(acc, [model, data]) => {
|
|
131
|
+
acc[model] = {
|
|
132
|
+
...data,
|
|
133
|
+
success_rate: data.count > 0 ? (data.passed / data.count) * 100 : 0,
|
|
134
|
+
}
|
|
135
|
+
return acc
|
|
136
|
+
},
|
|
137
|
+
{} as Record<string, { count: number; passed: number; success_rate: number }>
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
total_runs: results.length,
|
|
142
|
+
passed,
|
|
143
|
+
failed: by_status['failed'] || 0,
|
|
144
|
+
error: by_status['error'] || 0,
|
|
145
|
+
success_rate: (passed / results.length) * 100,
|
|
146
|
+
avg_duration: total_duration / results.length,
|
|
147
|
+
total_cost,
|
|
148
|
+
total_tokens,
|
|
149
|
+
avg_tokens_per_run: total_tokens / results.length,
|
|
150
|
+
by_status,
|
|
151
|
+
by_repo: by_repo_with_rates,
|
|
152
|
+
by_model: by_model_with_rates,
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export async function GET(request: Request) {
|
|
157
|
+
const { searchParams } = new URL(request.url)
|
|
158
|
+
const format = searchParams.get('format') || 'summary'
|
|
159
|
+
const repo = searchParams.get('repo')
|
|
160
|
+
const model = searchParams.get('model')
|
|
161
|
+
const status = searchParams.get('status')
|
|
162
|
+
|
|
163
|
+
const results = await loadBenchmarkResults()
|
|
164
|
+
|
|
165
|
+
// Filter results
|
|
166
|
+
let filtered = results
|
|
167
|
+
if (repo) {
|
|
168
|
+
filtered = filtered.filter((r) => r.repo === repo)
|
|
169
|
+
}
|
|
170
|
+
if (model) {
|
|
171
|
+
filtered = filtered.filter((r) => r.model === model)
|
|
172
|
+
}
|
|
173
|
+
if (status) {
|
|
174
|
+
filtered = filtered.filter((r) => r.status === status)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (format === 'detailed') {
|
|
178
|
+
return NextResponse.json({
|
|
179
|
+
results: filtered,
|
|
180
|
+
count: filtered.length,
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Default: summary format
|
|
185
|
+
const stats = calculateStats(filtered)
|
|
186
|
+
|
|
187
|
+
return NextResponse.json({
|
|
188
|
+
stats,
|
|
189
|
+
filters: {
|
|
190
|
+
repo: repo || null,
|
|
191
|
+
model: model || null,
|
|
192
|
+
status: status || null,
|
|
193
|
+
},
|
|
194
|
+
})
|
|
195
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { readStatsCache, getSessions } from '@/lib/claude-reader'
|
|
3
|
+
import { estimateTotalCostFromModel, cacheEfficiency, getPricing, PRICING } from '@/lib/pricing'
|
|
4
|
+
import { projectDisplayName } from '@/lib/decode'
|
|
5
|
+
import type { CostAnalytics, ModelCostBreakdown, DailyCost, ProjectCost } from '@/types/claude'
|
|
6
|
+
|
|
7
|
+
export const dynamic = 'force-dynamic'
|
|
8
|
+
|
|
9
|
+
export async function GET() {
|
|
10
|
+
const [stats, sessions] = await Promise.all([readStatsCache(), getSessions()])
|
|
11
|
+
|
|
12
|
+
if (!stats) {
|
|
13
|
+
return NextResponse.json({ error: 'stats-cache.json not found' }, { status: 404 })
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ── Per-model breakdown ────────────────────────────────────────────────────
|
|
17
|
+
let totalCost = 0
|
|
18
|
+
let totalSavings = 0
|
|
19
|
+
const models: ModelCostBreakdown[] = Object.entries(stats.modelUsage ?? {}).map(([model, usage]) => {
|
|
20
|
+
const cost = estimateTotalCostFromModel(model, usage)
|
|
21
|
+
const eff = cacheEfficiency(model, usage)
|
|
22
|
+
totalCost += cost
|
|
23
|
+
totalSavings += eff.savedUSD
|
|
24
|
+
return {
|
|
25
|
+
model,
|
|
26
|
+
input_tokens: usage.inputTokens ?? 0,
|
|
27
|
+
output_tokens: usage.outputTokens ?? 0,
|
|
28
|
+
cache_write_tokens: usage.cacheCreationInputTokens ?? 0,
|
|
29
|
+
cache_read_tokens: usage.cacheReadInputTokens ?? 0,
|
|
30
|
+
estimated_cost: cost,
|
|
31
|
+
cache_savings: eff.savedUSD ?? 0,
|
|
32
|
+
cache_hit_rate: eff.hitRate ?? 0,
|
|
33
|
+
}
|
|
34
|
+
}).sort((a, b) => b.estimated_cost - a.estimated_cost)
|
|
35
|
+
|
|
36
|
+
// ── Daily cost by model ────────────────────────────────────────────────────
|
|
37
|
+
// stats-cache.json uses "dailyModelTokens" in newer CC versions; "tokensByDate" in older ones
|
|
38
|
+
const daily: DailyCost[] = (stats.dailyModelTokens ?? stats.tokensByDate ?? []).map(d => {
|
|
39
|
+
const costs: Record<string, number> = {}
|
|
40
|
+
let dayTotal = 0
|
|
41
|
+
for (const [model, tokens] of Object.entries(d.tokensByModel ?? {})) {
|
|
42
|
+
const p = getPricing(model)
|
|
43
|
+
// tokensByDate only has total tokens, approximate as input+output split 50/50
|
|
44
|
+
const cost = tokens * p.input * 0.5 + tokens * p.output * 0.5
|
|
45
|
+
costs[model] = cost
|
|
46
|
+
dayTotal += cost
|
|
47
|
+
}
|
|
48
|
+
return { date: d.date, costs, total: dayTotal }
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
// ── Cost by project ────────────────────────────────────────────────────────
|
|
52
|
+
const projectMap = new Map<string, { cost: number; input: number; output: number }>()
|
|
53
|
+
for (const s of sessions) {
|
|
54
|
+
const pp = s.project_path ?? ''
|
|
55
|
+
const slug = pp
|
|
56
|
+
const existing = projectMap.get(slug) ?? { cost: 0, input: 0, output: 0 }
|
|
57
|
+
const cost = estimateTotalCostFromModel('claude-opus-4-6', {
|
|
58
|
+
inputTokens: s.input_tokens ?? 0,
|
|
59
|
+
outputTokens: s.output_tokens ?? 0,
|
|
60
|
+
cacheCreationInputTokens: s.cache_creation_input_tokens ?? 0,
|
|
61
|
+
cacheReadInputTokens: s.cache_read_input_tokens ?? 0,
|
|
62
|
+
costUSD: 0,
|
|
63
|
+
webSearchRequests: 0,
|
|
64
|
+
})
|
|
65
|
+
projectMap.set(slug, {
|
|
66
|
+
cost: existing.cost + cost,
|
|
67
|
+
input: existing.input + (s.input_tokens ?? 0),
|
|
68
|
+
output: existing.output + (s.output_tokens ?? 0),
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const by_project: ProjectCost[] = [...projectMap.entries()]
|
|
73
|
+
.map(([slug, data]) => {
|
|
74
|
+
const projectPath = slug
|
|
75
|
+
return {
|
|
76
|
+
slug,
|
|
77
|
+
display_name: projectDisplayName(projectPath),
|
|
78
|
+
estimated_cost: data.cost,
|
|
79
|
+
input_tokens: data.input,
|
|
80
|
+
output_tokens: data.output,
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
.sort((a, b) => b.estimated_cost - a.estimated_cost)
|
|
84
|
+
.slice(0, 20)
|
|
85
|
+
|
|
86
|
+
const result: CostAnalytics = { total_cost: totalCost, total_savings: totalSavings, models, daily, by_project }
|
|
87
|
+
return NextResponse.json(result)
|
|
88
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { readStatsCache, getSessions, readAllFacets, readHistory } from '@/lib/claude-reader'
|
|
3
|
+
import type { ExportPayload, Facet, SessionMeta } from '@/types/claude'
|
|
4
|
+
|
|
5
|
+
export const dynamic = 'force-dynamic'
|
|
6
|
+
|
|
7
|
+
function filterSessionsByDateRange(
|
|
8
|
+
sessions: SessionMeta[],
|
|
9
|
+
dateRange?: { from?: string; to?: string }
|
|
10
|
+
) {
|
|
11
|
+
const fromMs = dateRange?.from ? new Date(dateRange.from).getTime() : null
|
|
12
|
+
const toMs = dateRange?.to ? new Date(dateRange.to + 'T23:59:59.999Z').getTime() : null
|
|
13
|
+
return sessions.filter(s => {
|
|
14
|
+
if (!s.start_time) return true
|
|
15
|
+
const t = new Date(s.start_time).getTime()
|
|
16
|
+
if (fromMs !== null && t < fromMs) return false
|
|
17
|
+
if (toMs !== null && t > toMs) return false
|
|
18
|
+
return true
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function facetsForSessions(facets: Facet[], sessions: SessionMeta[]) {
|
|
23
|
+
const sessionIds = new Set(sessions.map(s => s.session_id))
|
|
24
|
+
return facets.filter(f => sessionIds.has(f.session_id))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Preview counts for the export UI (optional date filter via query params). */
|
|
28
|
+
export async function GET(req: Request) {
|
|
29
|
+
const url = new URL(req.url)
|
|
30
|
+
const from = url.searchParams.get('from') || undefined
|
|
31
|
+
const to = url.searchParams.get('to') || undefined
|
|
32
|
+
const dateRange = from || to ? { from, to } : undefined
|
|
33
|
+
|
|
34
|
+
const [stats, sessions, facets, history] = await Promise.all([
|
|
35
|
+
readStatsCache(),
|
|
36
|
+
getSessions(),
|
|
37
|
+
readAllFacets(),
|
|
38
|
+
readHistory(10_000),
|
|
39
|
+
])
|
|
40
|
+
|
|
41
|
+
const filteredSessions = filterSessionsByDateRange(sessions, dateRange)
|
|
42
|
+
const filteredFacets = facetsForSessions(facets, filteredSessions)
|
|
43
|
+
|
|
44
|
+
return NextResponse.json({
|
|
45
|
+
sessionCount: filteredSessions.length,
|
|
46
|
+
facetCount: filteredFacets.length,
|
|
47
|
+
historyEntries: history.length,
|
|
48
|
+
hasStatsCache: stats !== null,
|
|
49
|
+
totalSessionsIndexed: sessions.length,
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function POST(req: Request) {
|
|
54
|
+
const body = await req.json().catch(() => ({}))
|
|
55
|
+
const { dateRange } = body as { dateRange?: { from?: string; to?: string } }
|
|
56
|
+
|
|
57
|
+
const [stats, sessions, facets, history] = await Promise.all([
|
|
58
|
+
readStatsCache(),
|
|
59
|
+
getSessions(),
|
|
60
|
+
readAllFacets(),
|
|
61
|
+
readHistory(10_000),
|
|
62
|
+
])
|
|
63
|
+
|
|
64
|
+
const filteredSessions = filterSessionsByDateRange(sessions, dateRange)
|
|
65
|
+
const filteredFacets = facetsForSessions(facets, filteredSessions)
|
|
66
|
+
|
|
67
|
+
const payload: ExportPayload = {
|
|
68
|
+
exportedAt: new Date().toISOString(),
|
|
69
|
+
version: '1.0.0',
|
|
70
|
+
stats,
|
|
71
|
+
sessions: filteredSessions,
|
|
72
|
+
facets: filteredFacets,
|
|
73
|
+
history,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return NextResponse.json(payload)
|
|
77
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { readHistory } from '@/lib/claude-reader'
|
|
3
|
+
|
|
4
|
+
export const dynamic = 'force-dynamic'
|
|
5
|
+
|
|
6
|
+
export async function GET(req: Request) {
|
|
7
|
+
const { searchParams } = new URL(req.url)
|
|
8
|
+
const limit = Math.min(parseInt(searchParams.get('limit') ?? '200', 10), 10_000)
|
|
9
|
+
const history = await readHistory(limit)
|
|
10
|
+
return NextResponse.json({ history })
|
|
11
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { getSessions } from '@/lib/claude-reader'
|
|
3
|
+
import type { ExportPayload, ImportDiff } from '@/types/claude'
|
|
4
|
+
|
|
5
|
+
export const dynamic = 'force-dynamic'
|
|
6
|
+
|
|
7
|
+
export async function POST(req: Request) {
|
|
8
|
+
const payload = await req.json().catch(() => null) as ExportPayload | null
|
|
9
|
+
|
|
10
|
+
if (!payload || !payload.sessions) {
|
|
11
|
+
return NextResponse.json({ error: 'Invalid import payload' }, { status: 400 })
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const existing = await getSessions()
|
|
15
|
+
const existingIds = new Set(existing.map(s => s.session_id))
|
|
16
|
+
|
|
17
|
+
const sessions_to_add = payload.sessions.filter(s => !existingIds.has(s.session_id))
|
|
18
|
+
|
|
19
|
+
const diff: ImportDiff = {
|
|
20
|
+
total_in_export: payload.sessions.length,
|
|
21
|
+
already_present: payload.sessions.length - sessions_to_add.length,
|
|
22
|
+
new_sessions: sessions_to_add.length,
|
|
23
|
+
sessions_to_add,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Note: actual file writing is intentionally not implemented here
|
|
27
|
+
// to prevent accidental corruption of ~/.bahulam/ live data.
|
|
28
|
+
// The import feature shows a diff preview only.
|
|
29
|
+
|
|
30
|
+
return NextResponse.json(diff)
|
|
31
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import fs from 'fs/promises'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { readMemories } from '@/lib/claude-reader'
|
|
5
|
+
import { bahulamPath } from '@/lib/bahulam-paths'
|
|
6
|
+
|
|
7
|
+
export const dynamic = 'force-dynamic'
|
|
8
|
+
|
|
9
|
+
export async function GET() {
|
|
10
|
+
const memories = await readMemories()
|
|
11
|
+
return NextResponse.json({ memories })
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function PATCH(req: Request) {
|
|
15
|
+
try {
|
|
16
|
+
const { projectSlug, file, content } = await req.json() as {
|
|
17
|
+
projectSlug?: string
|
|
18
|
+
file?: string
|
|
19
|
+
content?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!projectSlug || !file || typeof content !== 'string') {
|
|
23
|
+
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 })
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Only allow .md files
|
|
27
|
+
if (!file.endsWith('.md')) {
|
|
28
|
+
return NextResponse.json({ error: 'Only .md files allowed' }, { status: 400 })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Prevent path traversal — slug and file must be plain names (no slashes or dots)
|
|
32
|
+
if (/[/\\]/.test(projectSlug) || /[/\\]/.test(file)) {
|
|
33
|
+
return NextResponse.json({ error: 'Invalid path' }, { status: 400 })
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const filePath = bahulamPath('projects', projectSlug, 'memory', file)
|
|
37
|
+
|
|
38
|
+
// Ensure the resolved path stays within ~/.bahulam/projects/ (or the configured legacy dir).
|
|
39
|
+
const allowedRoot = bahulamPath('projects')
|
|
40
|
+
if (!filePath.startsWith(allowedRoot + path.sep)) {
|
|
41
|
+
return NextResponse.json({ error: 'Path outside allowed directory' }, { status: 403 })
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
|
45
|
+
await fs.writeFile(filePath, content, 'utf-8')
|
|
46
|
+
return NextResponse.json({ ok: true })
|
|
47
|
+
} catch (err) {
|
|
48
|
+
return NextResponse.json({ error: String(err) }, { status: 500 })
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import { NextResponse } from 'next/server'
|
|
3
|
+
import { getSessions, listProjectJSONLFiles, readJSONLLines, resolveProjectPath } from '@/lib/claude-reader'
|
|
4
|
+
import { estimateCostFromUsage } from '@/lib/pricing'
|
|
5
|
+
import { projectDisplayName } from '@/lib/decode'
|
|
6
|
+
import type { SessionWithFacet } from '@/types/claude'
|
|
7
|
+
|
|
8
|
+
export const dynamic = 'force-dynamic'
|
|
9
|
+
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
|
+
type AnyLine = Record<string, any>
|
|
12
|
+
|
|
13
|
+
export async function GET(
|
|
14
|
+
_req: Request,
|
|
15
|
+
{ params }: { params: Promise<{ slug: string }> }
|
|
16
|
+
) {
|
|
17
|
+
const { slug } = await params
|
|
18
|
+
const projectPath = await resolveProjectPath(slug)
|
|
19
|
+
const allSessions = await getSessions()
|
|
20
|
+
let sessions = allSessions.filter(s => s.project_path === projectPath)
|
|
21
|
+
|
|
22
|
+
if (sessions.length === 0) {
|
|
23
|
+
const lastSegment = projectPath.split('/').filter(Boolean).pop() ?? ''
|
|
24
|
+
sessions = allSessions.filter(s =>
|
|
25
|
+
s.project_path?.endsWith('/' + lastSegment)
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Gather per-session branch data from JSONL
|
|
30
|
+
const files = await listProjectJSONLFiles(slug)
|
|
31
|
+
const branchTurns = new Map<string, number>()
|
|
32
|
+
const sessionMeta = new Map<string, { slug?: string; version?: string; has_compaction?: boolean }>()
|
|
33
|
+
|
|
34
|
+
await Promise.all(
|
|
35
|
+
files.map(async (f) => {
|
|
36
|
+
const sessionId = path.basename(f, '.jsonl')
|
|
37
|
+
const meta: { slug?: string; version?: string; has_compaction?: boolean } = {}
|
|
38
|
+
|
|
39
|
+
await readJSONLLines(f, (line: AnyLine) => {
|
|
40
|
+
if (!meta.slug && line.slug) meta.slug = line.slug
|
|
41
|
+
if (!meta.version && line.version) meta.version = line.version
|
|
42
|
+
if (line.type === 'system' && line.subtype === 'compact_boundary') meta.has_compaction = true
|
|
43
|
+
if (line.gitBranch && line.gitBranch !== 'HEAD') {
|
|
44
|
+
branchTurns.set(line.gitBranch, (branchTurns.get(line.gitBranch) ?? 0) + 1)
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
sessionMeta.set(sessionId, meta)
|
|
49
|
+
})
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
const enrichedSessions: SessionWithFacet[] = sessions.map(s => {
|
|
53
|
+
const enrich = sessionMeta.get(s.session_id) ?? {}
|
|
54
|
+
return {
|
|
55
|
+
...s,
|
|
56
|
+
estimated_cost: estimateCostFromUsage('claude-opus-4-6', {
|
|
57
|
+
input_tokens: s.input_tokens ?? 0,
|
|
58
|
+
output_tokens: s.output_tokens ?? 0,
|
|
59
|
+
cache_creation_input_tokens: s.cache_creation_input_tokens ?? 0,
|
|
60
|
+
cache_read_input_tokens: s.cache_read_input_tokens ?? 0,
|
|
61
|
+
}),
|
|
62
|
+
slug: enrich.slug,
|
|
63
|
+
version: enrich.version,
|
|
64
|
+
has_compaction: enrich.has_compaction,
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
// Aggregate tools
|
|
69
|
+
const toolCounts: Record<string, number> = {}
|
|
70
|
+
for (const s of sessions) {
|
|
71
|
+
for (const [t, c] of Object.entries(s.tool_counts ?? {})) {
|
|
72
|
+
toolCounts[t] = (toolCounts[t] ?? 0) + c
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Cost per session (for chart)
|
|
77
|
+
const costBySession = enrichedSessions.map(s => ({
|
|
78
|
+
session_id: s.session_id,
|
|
79
|
+
start_time: s.start_time,
|
|
80
|
+
cost: s.estimated_cost,
|
|
81
|
+
messages: (s.user_message_count ?? 0) + (s.assistant_message_count ?? 0),
|
|
82
|
+
}))
|
|
83
|
+
|
|
84
|
+
const branches = [...branchTurns.entries()]
|
|
85
|
+
.map(([branch, turns]) => ({ branch, turns }))
|
|
86
|
+
.sort((a, b) => b.turns - a.turns)
|
|
87
|
+
|
|
88
|
+
return NextResponse.json({
|
|
89
|
+
project_path: projectPath,
|
|
90
|
+
display_name: projectDisplayName(projectPath),
|
|
91
|
+
sessions: enrichedSessions.sort((a, b) => b.start_time.localeCompare(a.start_time)),
|
|
92
|
+
tool_counts: toolCounts,
|
|
93
|
+
cost_by_session: costBySession,
|
|
94
|
+
branches,
|
|
95
|
+
})
|
|
96
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { getSessions, listProjectSlugs, listProjectJSONLFiles, readJSONLLines, resolveProjectPath } from '@/lib/claude-reader'
|
|
3
|
+
import { estimateCostFromUsage } from '@/lib/pricing'
|
|
4
|
+
import { projectDisplayName } from '@/lib/decode'
|
|
5
|
+
import type { ProjectSummary } from '@/types/claude'
|
|
6
|
+
|
|
7
|
+
export const dynamic = 'force-dynamic'
|
|
8
|
+
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
|
+
type AnyLine = Record<string, any>
|
|
11
|
+
|
|
12
|
+
export async function GET() {
|
|
13
|
+
const [sessions, slugDirs] = await Promise.all([getSessions(), listProjectSlugs()])
|
|
14
|
+
|
|
15
|
+
// Build path→slug lookup from actual project directories
|
|
16
|
+
const pathToSlugMap = new Map<string, string>()
|
|
17
|
+
await Promise.all(
|
|
18
|
+
slugDirs.map(async (slug) => {
|
|
19
|
+
const resolved = await resolveProjectPath(slug)
|
|
20
|
+
pathToSlugMap.set(resolved, slug)
|
|
21
|
+
})
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
// Group sessions by project_path
|
|
25
|
+
const byPath = new Map<string, typeof sessions>()
|
|
26
|
+
for (const s of sessions) {
|
|
27
|
+
const pp = s.project_path ?? ''
|
|
28
|
+
if (!byPath.has(pp)) byPath.set(pp, [])
|
|
29
|
+
byPath.get(pp)!.push(s)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Gather branches per slug from JSONL
|
|
33
|
+
const slugBranches = new Map<string, Set<string>>()
|
|
34
|
+
await Promise.all(
|
|
35
|
+
slugDirs.map(async (slug) => {
|
|
36
|
+
const files = await listProjectJSONLFiles(slug)
|
|
37
|
+
const branches = new Set<string>()
|
|
38
|
+
await Promise.all(
|
|
39
|
+
files.map(async (f) => {
|
|
40
|
+
await readJSONLLines(f, (line: AnyLine) => {
|
|
41
|
+
if (line.gitBranch && line.gitBranch !== 'HEAD') {
|
|
42
|
+
branches.add(line.gitBranch)
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
)
|
|
47
|
+
slugBranches.set(slug, branches)
|
|
48
|
+
})
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
const projects: ProjectSummary[] = []
|
|
52
|
+
|
|
53
|
+
for (const [projectPath, sessionList] of byPath.entries()) {
|
|
54
|
+
const slug = pathToSlugMap.get(projectPath) ?? projectPath.replace(/\//g, '-')
|
|
55
|
+
|
|
56
|
+
const totalMessages = sessionList.reduce(
|
|
57
|
+
(s, m) => s + (m.user_message_count ?? 0) + (m.assistant_message_count ?? 0), 0
|
|
58
|
+
)
|
|
59
|
+
const totalDuration = sessionList.reduce((s, m) => s + (m.duration_minutes ?? 0), 0)
|
|
60
|
+
const totalLinesAdded = sessionList.reduce((s, m) => s + (m.lines_added ?? 0), 0)
|
|
61
|
+
const totalLinesRemoved = sessionList.reduce((s, m) => s + (m.lines_removed ?? 0), 0)
|
|
62
|
+
const totalFilesModified = sessionList.reduce((s, m) => s + (m.files_modified ?? 0), 0)
|
|
63
|
+
const gitCommits = sessionList.reduce((s, m) => s + (m.git_commits ?? 0), 0)
|
|
64
|
+
const gitPushes = sessionList.reduce((s, m) => s + (m.git_pushes ?? 0), 0)
|
|
65
|
+
const inputTokens = sessionList.reduce((s, m) => s + (m.input_tokens ?? 0), 0)
|
|
66
|
+
const outputTokens = sessionList.reduce((s, m) => s + (m.output_tokens ?? 0), 0)
|
|
67
|
+
|
|
68
|
+
const estimatedCost = sessionList.reduce((sum, s) => {
|
|
69
|
+
return sum + estimateCostFromUsage('claude-opus-4-6', {
|
|
70
|
+
input_tokens: s.input_tokens ?? 0,
|
|
71
|
+
output_tokens: s.output_tokens ?? 0,
|
|
72
|
+
cache_creation_input_tokens: s.cache_creation_input_tokens ?? 0,
|
|
73
|
+
cache_read_input_tokens: s.cache_read_input_tokens ?? 0,
|
|
74
|
+
})
|
|
75
|
+
}, 0)
|
|
76
|
+
|
|
77
|
+
const languages: Record<string, number> = {}
|
|
78
|
+
for (const s of sessionList) {
|
|
79
|
+
for (const [lang, count] of Object.entries(s.languages ?? {})) {
|
|
80
|
+
languages[lang] = (languages[lang] ?? 0) + count
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const toolCounts: Record<string, number> = {}
|
|
85
|
+
for (const s of sessionList) {
|
|
86
|
+
for (const [tool, count] of Object.entries(s.tool_counts ?? {})) {
|
|
87
|
+
toolCounts[tool] = (toolCounts[tool] ?? 0) + count
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const sortedDates = sessionList.map(s => s.start_time).sort()
|
|
92
|
+
|
|
93
|
+
projects.push({
|
|
94
|
+
slug,
|
|
95
|
+
project_path: projectPath,
|
|
96
|
+
display_name: projectDisplayName(projectPath),
|
|
97
|
+
session_count: sessionList.length,
|
|
98
|
+
total_messages: totalMessages,
|
|
99
|
+
total_duration_minutes: totalDuration,
|
|
100
|
+
total_lines_added: totalLinesAdded,
|
|
101
|
+
total_lines_removed: totalLinesRemoved,
|
|
102
|
+
total_files_modified: totalFilesModified,
|
|
103
|
+
git_commits: gitCommits,
|
|
104
|
+
git_pushes: gitPushes,
|
|
105
|
+
estimated_cost: estimatedCost,
|
|
106
|
+
input_tokens: inputTokens,
|
|
107
|
+
output_tokens: outputTokens,
|
|
108
|
+
languages,
|
|
109
|
+
tool_counts: toolCounts,
|
|
110
|
+
last_active: sortedDates[sortedDates.length - 1] ?? '',
|
|
111
|
+
first_active: sortedDates[0] ?? '',
|
|
112
|
+
uses_mcp: sessionList.some(s => s.uses_mcp),
|
|
113
|
+
uses_task_agent: sessionList.some(s => s.uses_task_agent),
|
|
114
|
+
branches: [...(slugBranches.get(slug) ?? new Set())].slice(0, 10),
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return NextResponse.json({
|
|
119
|
+
projects: projects.sort((a, b) => b.last_active.localeCompare(a.last_active)),
|
|
120
|
+
})
|
|
121
|
+
}
|