@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.
Files changed (278) hide show
  1. package/README.md +93 -0
  2. package/package.json +56 -0
  3. package/pulse/app/activity/page.tsx +190 -0
  4. package/pulse/app/api/activity/route.ts +138 -0
  5. package/pulse/app/api/benchmark/route.ts +113 -0
  6. package/pulse/app/api/benchmarks/route.ts +195 -0
  7. package/pulse/app/api/costs/route.ts +88 -0
  8. package/pulse/app/api/export/route.ts +77 -0
  9. package/pulse/app/api/history/route.ts +11 -0
  10. package/pulse/app/api/import/route.ts +31 -0
  11. package/pulse/app/api/memory/route.ts +50 -0
  12. package/pulse/app/api/plans/route.ts +9 -0
  13. package/pulse/app/api/projects/[slug]/route.ts +96 -0
  14. package/pulse/app/api/projects/route.ts +121 -0
  15. package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
  16. package/pulse/app/api/sessions/[id]/route.ts +31 -0
  17. package/pulse/app/api/sessions/route.ts +112 -0
  18. package/pulse/app/api/settings/route.ts +14 -0
  19. package/pulse/app/api/stats/route.ts +143 -0
  20. package/pulse/app/api/todos/route.ts +9 -0
  21. package/pulse/app/api/tools/route.ts +160 -0
  22. package/pulse/app/benchmarks/page.tsx +224 -0
  23. package/pulse/app/costs/page.tsx +179 -0
  24. package/pulse/app/export/page.tsx +465 -0
  25. package/pulse/app/favicon.ico +0 -0
  26. package/pulse/app/globals.css +263 -0
  27. package/pulse/app/help/page.tsx +143 -0
  28. package/pulse/app/history/page.tsx +157 -0
  29. package/pulse/app/layout.tsx +46 -0
  30. package/pulse/app/memory/page.tsx +365 -0
  31. package/pulse/app/overview-client.tsx +393 -0
  32. package/pulse/app/page.tsx +14 -0
  33. package/pulse/app/plans/page.tsx +308 -0
  34. package/pulse/app/projects/[slug]/page.tsx +390 -0
  35. package/pulse/app/projects/page.tsx +110 -0
  36. package/pulse/app/sessions/[id]/page.tsx +243 -0
  37. package/pulse/app/sessions/page.tsx +39 -0
  38. package/pulse/app/settings/page.tsx +188 -0
  39. package/pulse/app/todos/page.tsx +211 -0
  40. package/pulse/app/tools/page.tsx +249 -0
  41. package/pulse/cli.js +164 -0
  42. package/pulse/components/activity/day-of-week-chart.tsx +35 -0
  43. package/pulse/components/activity/streak-card.tsx +36 -0
  44. package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
  45. package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
  46. package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
  47. package/pulse/components/costs/model-token-table.tsx +60 -0
  48. package/pulse/components/global-search.tsx +193 -0
  49. package/pulse/components/keyboard-nav-provider.tsx +23 -0
  50. package/pulse/components/layout/bottom-nav.tsx +53 -0
  51. package/pulse/components/layout/client-layout.tsx +31 -0
  52. package/pulse/components/layout/sidebar-context.tsx +50 -0
  53. package/pulse/components/layout/sidebar.tsx +183 -0
  54. package/pulse/components/layout/top-bar.tsx +121 -0
  55. package/pulse/components/overview/activity-heatmap.tsx +107 -0
  56. package/pulse/components/overview/conversation-table.tsx +148 -0
  57. package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
  58. package/pulse/components/overview/peak-hours-chart.tsx +87 -0
  59. package/pulse/components/overview/project-activity-donut.tsx +96 -0
  60. package/pulse/components/overview/stat-card.tsx +102 -0
  61. package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
  62. package/pulse/components/projects/project-card.tsx +175 -0
  63. package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
  64. package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
  65. package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
  66. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
  67. package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
  68. package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
  69. package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
  70. package/pulse/components/sessions/session-badges.tsx +49 -0
  71. package/pulse/components/sessions/session-table.tsx +299 -0
  72. package/pulse/components/theme-provider.tsx +44 -0
  73. package/pulse/components/tools/feature-adoption-table.tsx +58 -0
  74. package/pulse/components/tools/mcp-server-panel.tsx +45 -0
  75. package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
  76. package/pulse/components/tools/version-history-table.tsx +32 -0
  77. package/pulse/components/ui/alert.tsx +66 -0
  78. package/pulse/components/ui/badge.tsx +48 -0
  79. package/pulse/components/ui/breadcrumb.tsx +109 -0
  80. package/pulse/components/ui/button.tsx +64 -0
  81. package/pulse/components/ui/calendar.tsx +220 -0
  82. package/pulse/components/ui/card.tsx +92 -0
  83. package/pulse/components/ui/command.tsx +158 -0
  84. package/pulse/components/ui/dialog.tsx +158 -0
  85. package/pulse/components/ui/input.tsx +21 -0
  86. package/pulse/components/ui/popover.tsx +89 -0
  87. package/pulse/components/ui/progress.tsx +31 -0
  88. package/pulse/components/ui/select.tsx +190 -0
  89. package/pulse/components/ui/separator.tsx +28 -0
  90. package/pulse/components/ui/sheet.tsx +143 -0
  91. package/pulse/components/ui/skeleton.tsx +13 -0
  92. package/pulse/components/ui/table.tsx +116 -0
  93. package/pulse/components/ui/tabs.tsx +91 -0
  94. package/pulse/components/ui/tooltip.tsx +57 -0
  95. package/pulse/components/use-global-keyboard-nav.ts +79 -0
  96. package/pulse/components.json +23 -0
  97. package/pulse/eslint.config.mjs +18 -0
  98. package/pulse/lib/bahulam-paths.ts +23 -0
  99. package/pulse/lib/claude-reader.ts +592 -0
  100. package/pulse/lib/decode.ts +129 -0
  101. package/pulse/lib/pricing.ts +102 -0
  102. package/pulse/lib/replay-parser.ts +165 -0
  103. package/pulse/lib/tool-categories.ts +127 -0
  104. package/pulse/lib/utils.ts +6 -0
  105. package/pulse/next-env.d.ts +6 -0
  106. package/pulse/next.config.ts +16 -0
  107. package/pulse/package.json +45 -0
  108. package/pulse/postcss.config.mjs +7 -0
  109. package/pulse/public/activity.png +0 -0
  110. package/pulse/public/cc-lens.png +0 -0
  111. package/pulse/public/command-k.png +0 -0
  112. package/pulse/public/costs.png +0 -0
  113. package/pulse/public/dashboard-dark.png +0 -0
  114. package/pulse/public/dashboard-white.png +0 -0
  115. package/pulse/public/export.png +0 -0
  116. package/pulse/public/file.svg +1 -0
  117. package/pulse/public/globe.svg +1 -0
  118. package/pulse/public/next.svg +1 -0
  119. package/pulse/public/projects.png +0 -0
  120. package/pulse/public/session-chat.png +0 -0
  121. package/pulse/public/todos.png +0 -0
  122. package/pulse/public/tools.png +0 -0
  123. package/pulse/public/vercel.svg +1 -0
  124. package/pulse/public/window.svg +1 -0
  125. package/pulse/tsconfig.json +34 -0
  126. package/pulse/types/claude.ts +294 -0
  127. package/src/agents/loader.mjs +94 -0
  128. package/src/agents/multi_workflow_loader.mjs +330 -0
  129. package/src/agents/parser.mjs +205 -0
  130. package/src/agents/scaffold.mjs +223 -0
  131. package/src/agents/teams.mjs +123 -0
  132. package/src/agents/workflow_loader.mjs +122 -0
  133. package/src/agents/workflow_scaffold.mjs +249 -0
  134. package/src/auth/oauth.mjs +220 -0
  135. package/src/auth/tarang-auth.mjs +312 -0
  136. package/src/commands/agent.mjs +221 -0
  137. package/src/commands/workflow.mjs +581 -0
  138. package/src/config/cli-args.mjs +202 -0
  139. package/src/config/env.mjs +263 -0
  140. package/src/config/hook-runner.mjs +100 -0
  141. package/src/config/memory-loader.mjs +32 -0
  142. package/src/config/model-catalog.mjs +57 -0
  143. package/src/config/settings-loader.mjs +45 -0
  144. package/src/config/settings.mjs +132 -0
  145. package/src/context/ast-parser.mjs +298 -0
  146. package/src/context/bm25.mjs +85 -0
  147. package/src/context/prose-chunker.mjs +255 -0
  148. package/src/context/retriever.mjs +425 -0
  149. package/src/context/skeleton.mjs +134 -0
  150. package/src/context/symbol-indexer.mjs +375 -0
  151. package/src/core/agent-history.mjs +111 -0
  152. package/src/core/agent-loop.mjs +486 -0
  153. package/src/core/approval-log.mjs +145 -0
  154. package/src/core/approval.mjs +700 -0
  155. package/src/core/attachments.mjs +666 -0
  156. package/src/core/backend-url.mjs +68 -0
  157. package/src/core/bundled-runtime.mjs +418 -0
  158. package/src/core/cache-control.mjs +92 -0
  159. package/src/core/cache.mjs +105 -0
  160. package/src/core/callback-client.mjs +180 -0
  161. package/src/core/checkpoints.mjs +142 -0
  162. package/src/core/compact-history.mjs +127 -0
  163. package/src/core/context-envelope.mjs +54 -0
  164. package/src/core/context-manager.mjs +198 -0
  165. package/src/core/error-guidance.mjs +331 -0
  166. package/src/core/file-diff.mjs +217 -0
  167. package/src/core/headless.mjs +460 -0
  168. package/src/core/hooks-manager.mjs +87 -0
  169. package/src/core/jsonl-writer.mjs +449 -0
  170. package/src/core/local-agent.mjs +538 -0
  171. package/src/core/local-store.mjs +836 -0
  172. package/src/core/mode-selector.mjs +51 -0
  173. package/src/core/output-filter.mjs +177 -0
  174. package/src/core/paths.mjs +190 -0
  175. package/src/core/policy-resolver.mjs +156 -0
  176. package/src/core/pricing.mjs +336 -0
  177. package/src/core/project-artifacts.mjs +39 -0
  178. package/src/core/project-context-loader.mjs +139 -0
  179. package/src/core/providers.mjs +219 -0
  180. package/src/core/rate-limit-display.mjs +121 -0
  181. package/src/core/rate-limiter.mjs +119 -0
  182. package/src/core/resume-mode.mjs +192 -0
  183. package/src/core/risk-tier.mjs +388 -0
  184. package/src/core/safety.mjs +260 -0
  185. package/src/core/scheduler.mjs +173 -0
  186. package/src/core/session-manager.mjs +360 -0
  187. package/src/core/session.mjs +143 -0
  188. package/src/core/settings-sync.mjs +85 -0
  189. package/src/core/stagnation.mjs +57 -0
  190. package/src/core/stream-client.mjs +957 -0
  191. package/src/core/streaming.mjs +182 -0
  192. package/src/core/system-prompt.mjs +140 -0
  193. package/src/core/tasks.mjs +196 -0
  194. package/src/core/tool-executor.mjs +2231 -0
  195. package/src/core/trust.mjs +160 -0
  196. package/src/core/work-scope.mjs +248 -0
  197. package/src/hooks/engine.mjs +162 -0
  198. package/src/mcp/client.mjs +253 -0
  199. package/src/mcp/transport-shttp.mjs +130 -0
  200. package/src/mcp/transport-sse.mjs +131 -0
  201. package/src/mcp/transport-ws.mjs +134 -0
  202. package/src/onboarding/preflight.mjs +374 -0
  203. package/src/permissions/checker.mjs +57 -0
  204. package/src/permissions/command-classifier.mjs +700 -0
  205. package/src/permissions/injection-check.mjs +60 -0
  206. package/src/permissions/path-check.mjs +102 -0
  207. package/src/permissions/prompt.mjs +73 -0
  208. package/src/permissions/sandbox.mjs +112 -0
  209. package/src/plugins/loader.mjs +138 -0
  210. package/src/skills/installer.mjs +188 -0
  211. package/src/skills/loader.mjs +252 -0
  212. package/src/skills/runner.mjs +55 -0
  213. package/src/state/orbit.mjs +263 -0
  214. package/src/state/verbosity.mjs +99 -0
  215. package/src/telemetry/index.mjs +122 -0
  216. package/src/terminal/agents.mjs +353 -0
  217. package/src/terminal/analytics.mjs +292 -0
  218. package/src/terminal/ansi.mjs +695 -0
  219. package/src/terminal/init.mjs +145 -0
  220. package/src/terminal/main.mjs +310 -0
  221. package/src/terminal/repl-ask-form.mjs +120 -0
  222. package/src/terminal/repl-explore.mjs +44 -0
  223. package/src/terminal/repl-format.mjs +317 -0
  224. package/src/terminal/repl-model-form.mjs +132 -0
  225. package/src/terminal/repl-render.mjs +833 -0
  226. package/src/terminal/repl-resume.mjs +640 -0
  227. package/src/terminal/repl-state.mjs +120 -0
  228. package/src/terminal/repl-utils.mjs +34 -0
  229. package/src/terminal/repl.mjs +5032 -0
  230. package/src/terminal/skills.mjs +54 -0
  231. package/src/terminal/tool-display.mjs +392 -0
  232. package/src/tools/agent.mjs +137 -0
  233. package/src/tools/ask-user.mjs +61 -0
  234. package/src/tools/bash.mjs +231 -0
  235. package/src/tools/cron-create.mjs +120 -0
  236. package/src/tools/cron-delete.mjs +49 -0
  237. package/src/tools/cron-list.mjs +37 -0
  238. package/src/tools/edit.mjs +82 -0
  239. package/src/tools/enter-worktree.mjs +69 -0
  240. package/src/tools/exit-worktree.mjs +57 -0
  241. package/src/tools/glob.mjs +117 -0
  242. package/src/tools/grep.mjs +129 -0
  243. package/src/tools/lint.mjs +71 -0
  244. package/src/tools/ls.mjs +58 -0
  245. package/src/tools/lsp.mjs +115 -0
  246. package/src/tools/multi-edit.mjs +94 -0
  247. package/src/tools/notebook-edit.mjs +96 -0
  248. package/src/tools/project-overview.mjs +703 -0
  249. package/src/tools/read-mcp-resource.mjs +57 -0
  250. package/src/tools/read.mjs +138 -0
  251. package/src/tools/registry.mjs +116 -0
  252. package/src/tools/remote-trigger.mjs +84 -0
  253. package/src/tools/send-message.mjs +64 -0
  254. package/src/tools/skill.mjs +52 -0
  255. package/src/tools/test-runner.mjs +49 -0
  256. package/src/tools/todo-write.mjs +68 -0
  257. package/src/tools/tool-search.mjs +77 -0
  258. package/src/tools/web-fetch.mjs +65 -0
  259. package/src/tools/web-search.mjs +89 -0
  260. package/src/tools/write.mjs +55 -0
  261. package/src/ui/approval.mjs +510 -0
  262. package/src/ui/banner.mjs +232 -0
  263. package/src/ui/commands.mjs +537 -0
  264. package/src/ui/formatter.mjs +409 -0
  265. package/src/ui/icons.mjs +170 -0
  266. package/src/ui/input-dock.mjs +772 -0
  267. package/src/ui/markdown.mjs +278 -0
  268. package/src/ui/mission-report.mjs +296 -0
  269. package/src/ui/palette.mjs +189 -0
  270. package/src/ui/render-queue.mjs +500 -0
  271. package/src/ui/slash-commands.mjs +257 -0
  272. package/src/ui/spinner.mjs +116 -0
  273. package/src/ui/sub-agent.mjs +167 -0
  274. package/src/ui/term.mjs +174 -0
  275. package/src/ui/text-layout.mjs +127 -0
  276. package/src/ui/tool-card.mjs +740 -0
  277. package/src/ui/tool-details.mjs +504 -0
  278. 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,9 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { readPlans } from '@/lib/claude-reader'
3
+
4
+ export const dynamic = 'force-dynamic'
5
+
6
+ export async function GET() {
7
+ const plans = await readPlans()
8
+ return NextResponse.json({ plans })
9
+ }
@@ -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
+ }