@bahulam/code 0.1.6 → 0.1.7

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 (178) hide show
  1. package/LICENSE +1 -1
  2. package/NOTICE +6 -6
  3. package/README.md +10 -17
  4. package/package.json +9 -9
  5. package/src/agents/loader.mjs +1 -1
  6. package/src/agents/workflow_loader.mjs +2 -2
  7. package/src/auth/tarang-auth.mjs +3 -2
  8. package/src/config/model-catalog-default.json +4 -3
  9. package/src/config/model-catalog.mjs +2 -0
  10. package/src/context/prose-chunker.mjs +162 -5
  11. package/src/core/attachments.mjs +65 -3
  12. package/src/core/bundled-runtime.mjs +1 -1
  13. package/src/core/headless.mjs +14 -0
  14. package/src/core/jsonl-writer.mjs +3 -3
  15. package/src/core/lint-resolver.mjs +472 -0
  16. package/src/core/local-agent.mjs +15 -4
  17. package/src/core/local-store.mjs +4 -0
  18. package/src/core/output-filter.mjs +21 -25
  19. package/src/core/pricing.mjs +10 -3
  20. package/src/core/stream-client.mjs +9 -4
  21. package/src/core/tool-executor.mjs +105 -29
  22. package/src/daemon/http-dashboard.mjs +400 -0
  23. package/src/daemon/session-core.mjs +1 -0
  24. package/src/local-service/agent-relay.mjs +868 -0
  25. package/src/local-service/approval-bridge.mjs +222 -0
  26. package/src/local-service/browser.mjs +24 -0
  27. package/src/local-service/command.mjs +155 -0
  28. package/src/local-service/file-access.mjs +393 -0
  29. package/src/local-service/machine.mjs +86 -0
  30. package/src/local-service/paths.mjs +29 -0
  31. package/src/local-service/preview-converters.mjs +260 -0
  32. package/src/local-service/server.mjs +2644 -0
  33. package/src/local-service/session-store.mjs +221 -0
  34. package/src/onboarding/preflight.mjs +1 -1
  35. package/src/terminal/analytics.mjs +2 -6
  36. package/src/terminal/ansi.mjs +11 -3
  37. package/src/terminal/main.mjs +37 -11
  38. package/src/terminal/model-catalog-display.mjs +111 -0
  39. package/src/terminal/repl-format.mjs +4 -1
  40. package/src/terminal/repl-model-form.mjs +81 -14
  41. package/src/terminal/repl-render.mjs +35 -5
  42. package/src/terminal/repl-state.mjs +12 -0
  43. package/src/terminal/repl.mjs +231 -53
  44. package/src/terminal/tool-display.mjs +32 -0
  45. package/src/terminal/watch-state.mjs +118 -0
  46. package/src/tools/analyze-image.mjs +158 -0
  47. package/src/tools/bash.mjs +11 -9
  48. package/src/tools/generate-image.mjs +411 -0
  49. package/src/tools/lint.mjs +24 -43
  50. package/src/tools/registry.mjs +4 -0
  51. package/src/ui/icons.mjs +2 -0
  52. package/src/ui/palette.mjs +1 -1
  53. package/src/ui/slash-commands.mjs +4 -6
  54. package/src/ui/tool-card.mjs +7 -3
  55. package/pulse/app/activity/page.tsx +0 -190
  56. package/pulse/app/api/activity/route.ts +0 -138
  57. package/pulse/app/api/benchmark/route.ts +0 -113
  58. package/pulse/app/api/benchmarks/route.ts +0 -195
  59. package/pulse/app/api/costs/route.ts +0 -88
  60. package/pulse/app/api/export/route.ts +0 -77
  61. package/pulse/app/api/history/route.ts +0 -11
  62. package/pulse/app/api/import/route.ts +0 -31
  63. package/pulse/app/api/memory/route.ts +0 -50
  64. package/pulse/app/api/plans/route.ts +0 -9
  65. package/pulse/app/api/projects/[slug]/route.ts +0 -96
  66. package/pulse/app/api/projects/route.ts +0 -121
  67. package/pulse/app/api/sessions/[id]/replay/route.ts +0 -20
  68. package/pulse/app/api/sessions/[id]/route.ts +0 -31
  69. package/pulse/app/api/sessions/route.ts +0 -112
  70. package/pulse/app/api/settings/route.ts +0 -14
  71. package/pulse/app/api/stats/route.ts +0 -143
  72. package/pulse/app/api/todos/route.ts +0 -9
  73. package/pulse/app/api/tools/route.ts +0 -160
  74. package/pulse/app/benchmarks/page.tsx +0 -224
  75. package/pulse/app/costs/page.tsx +0 -179
  76. package/pulse/app/export/page.tsx +0 -465
  77. package/pulse/app/favicon.ico +0 -0
  78. package/pulse/app/globals.css +0 -263
  79. package/pulse/app/help/page.tsx +0 -143
  80. package/pulse/app/history/page.tsx +0 -157
  81. package/pulse/app/layout.tsx +0 -46
  82. package/pulse/app/memory/page.tsx +0 -365
  83. package/pulse/app/overview-client.tsx +0 -393
  84. package/pulse/app/page.tsx +0 -14
  85. package/pulse/app/plans/page.tsx +0 -308
  86. package/pulse/app/projects/[slug]/page.tsx +0 -390
  87. package/pulse/app/projects/page.tsx +0 -110
  88. package/pulse/app/sessions/[id]/page.tsx +0 -243
  89. package/pulse/app/sessions/page.tsx +0 -39
  90. package/pulse/app/settings/page.tsx +0 -188
  91. package/pulse/app/todos/page.tsx +0 -211
  92. package/pulse/app/tools/page.tsx +0 -249
  93. package/pulse/cli.js +0 -164
  94. package/pulse/components/activity/day-of-week-chart.tsx +0 -35
  95. package/pulse/components/activity/streak-card.tsx +0 -36
  96. package/pulse/components/costs/cache-efficiency-panel.tsx +0 -76
  97. package/pulse/components/costs/cost-by-project-chart.tsx +0 -48
  98. package/pulse/components/costs/cost-over-time-chart.tsx +0 -95
  99. package/pulse/components/costs/model-token-table.tsx +0 -60
  100. package/pulse/components/global-search.tsx +0 -193
  101. package/pulse/components/keyboard-nav-provider.tsx +0 -23
  102. package/pulse/components/layout/bottom-nav.tsx +0 -53
  103. package/pulse/components/layout/client-layout.tsx +0 -31
  104. package/pulse/components/layout/sidebar-context.tsx +0 -50
  105. package/pulse/components/layout/sidebar.tsx +0 -183
  106. package/pulse/components/layout/top-bar.tsx +0 -121
  107. package/pulse/components/overview/activity-heatmap.tsx +0 -107
  108. package/pulse/components/overview/conversation-table.tsx +0 -148
  109. package/pulse/components/overview/model-breakdown-donut.tsx +0 -95
  110. package/pulse/components/overview/peak-hours-chart.tsx +0 -87
  111. package/pulse/components/overview/project-activity-donut.tsx +0 -96
  112. package/pulse/components/overview/stat-card.tsx +0 -102
  113. package/pulse/components/overview/usage-over-time-chart.tsx +0 -166
  114. package/pulse/components/projects/project-card.tsx +0 -175
  115. package/pulse/components/sessions/replay/assistant-markdown.tsx +0 -94
  116. package/pulse/components/sessions/replay/compaction-card.tsx +0 -25
  117. package/pulse/components/sessions/replay/session-sidebar.tsx +0 -231
  118. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +0 -98
  119. package/pulse/components/sessions/replay/tool-call-badge.tsx +0 -127
  120. package/pulse/components/sessions/replay/turn-cards.tsx +0 -220
  121. package/pulse/components/sessions/replay/user-tool-result.tsx +0 -158
  122. package/pulse/components/sessions/session-badges.tsx +0 -49
  123. package/pulse/components/sessions/session-table.tsx +0 -299
  124. package/pulse/components/theme-provider.tsx +0 -44
  125. package/pulse/components/tools/feature-adoption-table.tsx +0 -58
  126. package/pulse/components/tools/mcp-server-panel.tsx +0 -45
  127. package/pulse/components/tools/tool-ranking-chart.tsx +0 -57
  128. package/pulse/components/tools/version-history-table.tsx +0 -32
  129. package/pulse/components/ui/alert.tsx +0 -66
  130. package/pulse/components/ui/badge.tsx +0 -48
  131. package/pulse/components/ui/breadcrumb.tsx +0 -109
  132. package/pulse/components/ui/button.tsx +0 -64
  133. package/pulse/components/ui/calendar.tsx +0 -220
  134. package/pulse/components/ui/card.tsx +0 -92
  135. package/pulse/components/ui/command.tsx +0 -158
  136. package/pulse/components/ui/dialog.tsx +0 -158
  137. package/pulse/components/ui/input.tsx +0 -21
  138. package/pulse/components/ui/popover.tsx +0 -89
  139. package/pulse/components/ui/progress.tsx +0 -31
  140. package/pulse/components/ui/select.tsx +0 -190
  141. package/pulse/components/ui/separator.tsx +0 -28
  142. package/pulse/components/ui/sheet.tsx +0 -143
  143. package/pulse/components/ui/skeleton.tsx +0 -13
  144. package/pulse/components/ui/table.tsx +0 -116
  145. package/pulse/components/ui/tabs.tsx +0 -91
  146. package/pulse/components/ui/tooltip.tsx +0 -57
  147. package/pulse/components/use-global-keyboard-nav.ts +0 -79
  148. package/pulse/components.json +0 -23
  149. package/pulse/eslint.config.mjs +0 -18
  150. package/pulse/lib/bahulam-paths.ts +0 -23
  151. package/pulse/lib/claude-reader.ts +0 -592
  152. package/pulse/lib/decode.ts +0 -129
  153. package/pulse/lib/pricing.ts +0 -102
  154. package/pulse/lib/replay-parser.ts +0 -165
  155. package/pulse/lib/tool-categories.ts +0 -140
  156. package/pulse/lib/utils.ts +0 -6
  157. package/pulse/next-env.d.ts +0 -6
  158. package/pulse/next.config.ts +0 -16
  159. package/pulse/package.json +0 -45
  160. package/pulse/postcss.config.mjs +0 -7
  161. package/pulse/public/activity.png +0 -0
  162. package/pulse/public/cc-lens.png +0 -0
  163. package/pulse/public/command-k.png +0 -0
  164. package/pulse/public/costs.png +0 -0
  165. package/pulse/public/dashboard-dark.png +0 -0
  166. package/pulse/public/dashboard-white.png +0 -0
  167. package/pulse/public/export.png +0 -0
  168. package/pulse/public/file.svg +0 -1
  169. package/pulse/public/globe.svg +0 -1
  170. package/pulse/public/next.svg +0 -1
  171. package/pulse/public/projects.png +0 -0
  172. package/pulse/public/session-chat.png +0 -0
  173. package/pulse/public/todos.png +0 -0
  174. package/pulse/public/tools.png +0 -0
  175. package/pulse/public/vercel.svg +0 -1
  176. package/pulse/public/window.svg +0 -1
  177. package/pulse/tsconfig.json +0 -34
  178. package/pulse/types/claude.ts +0 -294
@@ -1,88 +0,0 @@
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
- }
@@ -1,77 +0,0 @@
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
- }
@@ -1,11 +0,0 @@
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
- }
@@ -1,31 +0,0 @@
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
- }
@@ -1,50 +0,0 @@
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
- }
@@ -1,9 +0,0 @@
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
- }
@@ -1,96 +0,0 @@
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
- }
@@ -1,121 +0,0 @@
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
- }
@@ -1,20 +0,0 @@
1
- import { NextResponse } from 'next/server'
2
- import { findSessionJSONL } from '@/lib/claude-reader'
3
- import { parseSessionReplay } from '@/lib/replay-parser'
4
-
5
- export const dynamic = 'force-dynamic'
6
-
7
- export async function GET(
8
- _req: Request,
9
- { params }: { params: Promise<{ id: string }> }
10
- ) {
11
- const { id } = await params
12
- const jsonlPath = await findSessionJSONL(id)
13
-
14
- if (!jsonlPath) {
15
- return NextResponse.json({ error: 'Session JSONL not found' }, { status: 404 })
16
- }
17
-
18
- const replay = await parseSessionReplay(jsonlPath, id)
19
- return NextResponse.json(replay)
20
- }
@@ -1,31 +0,0 @@
1
- import { NextResponse } from 'next/server'
2
- import { readSessionMeta, readFacet, getSessions } from '@/lib/claude-reader'
3
- import { estimateCostFromUsage } from '@/lib/pricing'
4
-
5
- export const dynamic = 'force-dynamic'
6
-
7
- export async function GET(
8
- _req: Request,
9
- { params }: { params: Promise<{ id: string }> }
10
- ) {
11
- const { id } = await params
12
- const [meta, facet] = await Promise.all([readSessionMeta(id), readFacet(id)])
13
-
14
- // readSessionMeta only finds session-meta/*.json files (legacy path).
15
- // Fall back to JSONL-derived sessions for machines without that directory.
16
- const resolved = meta ?? (await getSessions()).find(s => s.session_id === id) ?? null
17
-
18
- if (!resolved) {
19
- return NextResponse.json({ error: 'Session not found' }, { status: 404 })
20
- }
21
-
22
- const estimated_cost = estimateCostFromUsage('claude-opus-4-6', {
23
- input_tokens: resolved.input_tokens ?? 0,
24
- output_tokens: resolved.output_tokens ?? 0,
25
- cache_creation_input_tokens: resolved.cache_creation_input_tokens ?? 0,
26
- cache_read_input_tokens: resolved.cache_read_input_tokens ?? 0,
27
- })
28
-
29
- return NextResponse.json({ session: { ...resolved, facet, estimated_cost } })
30
- }
31
-
@@ -1,112 +0,0 @@
1
- import path from 'path'
2
- import { NextResponse } from 'next/server'
3
- import {
4
- getSessions,
5
- readAllSessionMeta,
6
- readAllFacets,
7
- listProjectSlugs,
8
- listProjectJSONLFiles,
9
- readJSONLLines,
10
- } from '@/lib/claude-reader'
11
- import { estimateCostFromUsage } from '@/lib/pricing'
12
- import type { SessionWithFacet } from '@/types/claude'
13
-
14
- export const dynamic = 'force-dynamic'
15
-
16
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
- type AnyLine = Record<string, any>
18
-
19
- async function enrichSessions(sessions: { session_id: string }[]) {
20
- // Build a map of sessionId -> { slug, version, gitBranch, has_compaction, has_thinking }
21
- const enrichment: Record<string, {
22
- slug?: string
23
- version?: string
24
- git_branch?: string
25
- has_compaction?: boolean
26
- has_thinking?: boolean
27
- }> = {}
28
-
29
- const slugs = await listProjectSlugs()
30
- await Promise.all(
31
- slugs.map(async (slug) => {
32
- const files = await listProjectJSONLFiles(slug)
33
- await Promise.all(
34
- files.map(async (f) => {
35
- const sessionId = path.basename(f, '.jsonl')
36
- const data: {
37
- slug?: string; version?: string; gitBranch?: string
38
- has_compaction?: boolean; has_thinking?: boolean
39
- } = {}
40
-
41
- await readJSONLLines(f, (line: AnyLine) => {
42
- if (!data.slug && line.slug) data.slug = line.slug
43
- if (!data.version && line.version) data.version = line.version
44
- if (!data.gitBranch && line.gitBranch && line.gitBranch !== 'HEAD') {
45
- data.gitBranch = line.gitBranch
46
- }
47
- if (line.type === 'system' && line.subtype === 'compact_boundary') {
48
- data.has_compaction = true
49
- }
50
- if (line.type === 'assistant' && Array.isArray(line.message?.content)) {
51
- if (line.message.content.some((c: AnyLine) => c.type === 'thinking')) {
52
- data.has_thinking = true
53
- }
54
- }
55
- })
56
-
57
- enrichment[sessionId] = {
58
- slug: data.slug,
59
- version: data.version,
60
- git_branch: data.gitBranch,
61
- has_compaction: data.has_compaction,
62
- has_thinking: data.has_thinking,
63
- }
64
- })
65
- )
66
- })
67
- )
68
- return enrichment
69
- }
70
-
71
- export async function GET() {
72
- const [sessions, metaSessions, facets] = await Promise.all([
73
- getSessions(),
74
- readAllSessionMeta(),
75
- readAllFacets(),
76
- ])
77
- const metaMap = new Map(metaSessions.map((s) => [s.session_id, s]))
78
- const merged = sessions.map((s) => {
79
- const meta = metaMap.get(s.session_id)
80
- if (meta) return { ...meta, ...s } as typeof s
81
- return s
82
- })
83
- const enrichment = await enrichSessions(merged)
84
-
85
- const facetMap = new Map(facets.map(f => [f.session_id, f]))
86
-
87
- const result: SessionWithFacet[] = merged.map(s => {
88
- const facet = facetMap.get(s.session_id)
89
- const enrich = enrichment[s.session_id] ?? {}
90
-
91
- // Estimate cost from session tokens (rough: treat all as opus)
92
- const estimated_cost = estimateCostFromUsage('claude-opus-4-6', {
93
- input_tokens: s.input_tokens ?? 0,
94
- output_tokens: s.output_tokens ?? 0,
95
- cache_creation_input_tokens: s.cache_creation_input_tokens ?? 0,
96
- cache_read_input_tokens: s.cache_read_input_tokens ?? 0,
97
- })
98
-
99
- return {
100
- ...s,
101
- facet,
102
- estimated_cost,
103
- slug: enrich.slug,
104
- version: enrich.version,
105
- git_branch: enrich.git_branch,
106
- has_compaction: enrich.has_compaction ?? false,
107
- has_thinking: enrich.has_thinking ?? false,
108
- }
109
- })
110
-
111
- return NextResponse.json({ sessions: result, total: result.length })
112
- }
@@ -1,14 +0,0 @@
1
- import { NextResponse } from 'next/server'
2
- import { readSettings, getClaudeStorageBytes, readSkills, readInstalledPlugins } from '@/lib/claude-reader'
3
-
4
- export const dynamic = 'force-dynamic'
5
-
6
- export async function GET() {
7
- const [settings, storageBytes, skills, plugins] = await Promise.all([
8
- readSettings(),
9
- getClaudeStorageBytes(),
10
- readSkills(),
11
- readInstalledPlugins(),
12
- ])
13
- return NextResponse.json({ settings, storageBytes, skills, plugins })
14
- }