@axplusb/kepler 0.0.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/README.md +82 -0
  2. package/package.json +36 -4
  3. package/pulse/app/activity/page.tsx +190 -0
  4. package/pulse/app/api/activity/route.ts +138 -0
  5. package/pulse/app/api/costs/route.ts +88 -0
  6. package/pulse/app/api/export/route.ts +77 -0
  7. package/pulse/app/api/history/route.ts +11 -0
  8. package/pulse/app/api/import/route.ts +31 -0
  9. package/pulse/app/api/memory/route.ts +52 -0
  10. package/pulse/app/api/plans/route.ts +9 -0
  11. package/pulse/app/api/projects/[slug]/route.ts +96 -0
  12. package/pulse/app/api/projects/route.ts +121 -0
  13. package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
  14. package/pulse/app/api/sessions/[id]/route.ts +31 -0
  15. package/pulse/app/api/sessions/route.ts +112 -0
  16. package/pulse/app/api/settings/route.ts +14 -0
  17. package/pulse/app/api/stats/route.ts +143 -0
  18. package/pulse/app/api/todos/route.ts +9 -0
  19. package/pulse/app/api/tools/route.ts +160 -0
  20. package/pulse/app/costs/page.tsx +179 -0
  21. package/pulse/app/export/page.tsx +465 -0
  22. package/pulse/app/favicon.ico +0 -0
  23. package/pulse/app/globals.css +263 -0
  24. package/pulse/app/help/page.tsx +142 -0
  25. package/pulse/app/history/page.tsx +157 -0
  26. package/pulse/app/layout.tsx +46 -0
  27. package/pulse/app/memory/page.tsx +365 -0
  28. package/pulse/app/overview-client.tsx +393 -0
  29. package/pulse/app/page.tsx +14 -0
  30. package/pulse/app/plans/page.tsx +308 -0
  31. package/pulse/app/projects/[slug]/page.tsx +390 -0
  32. package/pulse/app/projects/page.tsx +110 -0
  33. package/pulse/app/sessions/[id]/page.tsx +243 -0
  34. package/pulse/app/sessions/page.tsx +39 -0
  35. package/pulse/app/settings/page.tsx +188 -0
  36. package/pulse/app/todos/page.tsx +211 -0
  37. package/pulse/app/tools/page.tsx +249 -0
  38. package/pulse/cli.js +159 -0
  39. package/pulse/components/activity/day-of-week-chart.tsx +35 -0
  40. package/pulse/components/activity/streak-card.tsx +36 -0
  41. package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
  42. package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
  43. package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
  44. package/pulse/components/costs/model-token-table.tsx +60 -0
  45. package/pulse/components/global-search.tsx +193 -0
  46. package/pulse/components/keyboard-nav-provider.tsx +23 -0
  47. package/pulse/components/layout/bottom-nav.tsx +52 -0
  48. package/pulse/components/layout/client-layout.tsx +31 -0
  49. package/pulse/components/layout/sidebar-context.tsx +50 -0
  50. package/pulse/components/layout/sidebar.tsx +182 -0
  51. package/pulse/components/layout/top-bar.tsx +121 -0
  52. package/pulse/components/overview/activity-heatmap.tsx +107 -0
  53. package/pulse/components/overview/conversation-table.tsx +148 -0
  54. package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
  55. package/pulse/components/overview/peak-hours-chart.tsx +87 -0
  56. package/pulse/components/overview/project-activity-donut.tsx +96 -0
  57. package/pulse/components/overview/stat-card.tsx +102 -0
  58. package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
  59. package/pulse/components/projects/project-card.tsx +175 -0
  60. package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
  61. package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
  62. package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
  63. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
  64. package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
  65. package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
  66. package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
  67. package/pulse/components/sessions/session-badges.tsx +49 -0
  68. package/pulse/components/sessions/session-table.tsx +299 -0
  69. package/pulse/components/theme-provider.tsx +44 -0
  70. package/pulse/components/tools/feature-adoption-table.tsx +58 -0
  71. package/pulse/components/tools/mcp-server-panel.tsx +45 -0
  72. package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
  73. package/pulse/components/tools/version-history-table.tsx +32 -0
  74. package/pulse/components/ui/alert.tsx +66 -0
  75. package/pulse/components/ui/badge.tsx +48 -0
  76. package/pulse/components/ui/breadcrumb.tsx +109 -0
  77. package/pulse/components/ui/button.tsx +64 -0
  78. package/pulse/components/ui/calendar.tsx +220 -0
  79. package/pulse/components/ui/card.tsx +92 -0
  80. package/pulse/components/ui/command.tsx +158 -0
  81. package/pulse/components/ui/dialog.tsx +158 -0
  82. package/pulse/components/ui/input.tsx +21 -0
  83. package/pulse/components/ui/popover.tsx +89 -0
  84. package/pulse/components/ui/progress.tsx +31 -0
  85. package/pulse/components/ui/select.tsx +190 -0
  86. package/pulse/components/ui/separator.tsx +28 -0
  87. package/pulse/components/ui/sheet.tsx +143 -0
  88. package/pulse/components/ui/skeleton.tsx +13 -0
  89. package/pulse/components/ui/table.tsx +116 -0
  90. package/pulse/components/ui/tabs.tsx +91 -0
  91. package/pulse/components/ui/tooltip.tsx +57 -0
  92. package/pulse/components/use-global-keyboard-nav.ts +79 -0
  93. package/pulse/components.json +23 -0
  94. package/pulse/eslint.config.mjs +18 -0
  95. package/pulse/lib/claude-reader.ts +594 -0
  96. package/pulse/lib/decode.ts +129 -0
  97. package/pulse/lib/pricing.ts +102 -0
  98. package/pulse/lib/replay-parser.ts +165 -0
  99. package/pulse/lib/tool-categories.ts +127 -0
  100. package/pulse/lib/utils.ts +6 -0
  101. package/pulse/next-env.d.ts +6 -0
  102. package/pulse/next.config.ts +16 -0
  103. package/pulse/package.json +45 -0
  104. package/pulse/postcss.config.mjs +7 -0
  105. package/pulse/public/activity.png +0 -0
  106. package/pulse/public/cc-lens.png +0 -0
  107. package/pulse/public/command-k.png +0 -0
  108. package/pulse/public/costs.png +0 -0
  109. package/pulse/public/dashboard-dark.png +0 -0
  110. package/pulse/public/dashboard-white.png +0 -0
  111. package/pulse/public/export.png +0 -0
  112. package/pulse/public/file.svg +1 -0
  113. package/pulse/public/globe.svg +1 -0
  114. package/pulse/public/next.svg +1 -0
  115. package/pulse/public/projects.png +0 -0
  116. package/pulse/public/session-chat.png +0 -0
  117. package/pulse/public/todos.png +0 -0
  118. package/pulse/public/tools.png +0 -0
  119. package/pulse/public/vercel.svg +1 -0
  120. package/pulse/public/window.svg +1 -0
  121. package/pulse/tsconfig.json +34 -0
  122. package/pulse/types/claude.ts +294 -0
  123. package/src/agents/loader.mjs +89 -0
  124. package/src/agents/parser.mjs +98 -0
  125. package/src/agents/teams.mjs +123 -0
  126. package/src/auth/oauth.mjs +220 -0
  127. package/src/auth/tarang-auth.mjs +277 -0
  128. package/src/config/cli-args.mjs +173 -0
  129. package/src/config/env.mjs +263 -0
  130. package/src/config/settings.mjs +132 -0
  131. package/src/context/ast-parser.mjs +298 -0
  132. package/src/context/bm25.mjs +85 -0
  133. package/src/context/retriever.mjs +270 -0
  134. package/src/context/skeleton.mjs +134 -0
  135. package/src/core/agent-loop.mjs +480 -0
  136. package/src/core/approval.mjs +273 -0
  137. package/src/core/backend-url.mjs +57 -0
  138. package/src/core/cache.mjs +105 -0
  139. package/src/core/callback-client.mjs +149 -0
  140. package/src/core/checkpoints.mjs +142 -0
  141. package/src/core/context-manager.mjs +198 -0
  142. package/src/core/headless.mjs +168 -0
  143. package/src/core/hooks-manager.mjs +87 -0
  144. package/src/core/jsonl-writer.mjs +351 -0
  145. package/src/core/local-agent.mjs +429 -0
  146. package/src/core/local-store.mjs +325 -0
  147. package/src/core/mode-selector.mjs +51 -0
  148. package/src/core/output-filter.mjs +177 -0
  149. package/src/core/paths.mjs +98 -0
  150. package/src/core/pricing.mjs +314 -0
  151. package/src/core/providers.mjs +219 -0
  152. package/src/core/rate-limiter.mjs +119 -0
  153. package/src/core/safety.mjs +200 -0
  154. package/src/core/scheduler.mjs +173 -0
  155. package/src/core/session-manager.mjs +317 -0
  156. package/src/core/session.mjs +143 -0
  157. package/src/core/settings-sync.mjs +85 -0
  158. package/src/core/stagnation.mjs +57 -0
  159. package/src/core/stream-client.mjs +367 -0
  160. package/src/core/streaming.mjs +182 -0
  161. package/src/core/system-prompt.mjs +135 -0
  162. package/src/core/tool-executor.mjs +725 -0
  163. package/src/hooks/engine.mjs +162 -0
  164. package/src/index.mjs +370 -0
  165. package/src/mcp/client.mjs +253 -0
  166. package/src/mcp/transport-shttp.mjs +130 -0
  167. package/src/mcp/transport-sse.mjs +131 -0
  168. package/src/mcp/transport-ws.mjs +134 -0
  169. package/src/permissions/checker.mjs +57 -0
  170. package/src/permissions/command-classifier.mjs +573 -0
  171. package/src/permissions/injection-check.mjs +60 -0
  172. package/src/permissions/path-check.mjs +102 -0
  173. package/src/permissions/prompt.mjs +73 -0
  174. package/src/permissions/sandbox.mjs +112 -0
  175. package/src/plugins/loader.mjs +138 -0
  176. package/src/skills/loader.mjs +147 -0
  177. package/src/skills/runner.mjs +55 -0
  178. package/src/telemetry/index.mjs +96 -0
  179. package/src/terminal/agents.mjs +177 -0
  180. package/src/terminal/analytics.mjs +292 -0
  181. package/src/terminal/ansi.mjs +421 -0
  182. package/src/terminal/main.mjs +150 -0
  183. package/src/terminal/repl.mjs +1484 -0
  184. package/src/terminal/tool-display.mjs +58 -0
  185. package/src/tools/agent.mjs +137 -0
  186. package/src/tools/ask-user.mjs +61 -0
  187. package/src/tools/bash.mjs +148 -0
  188. package/src/tools/cron-create.mjs +120 -0
  189. package/src/tools/cron-delete.mjs +49 -0
  190. package/src/tools/cron-list.mjs +37 -0
  191. package/src/tools/edit.mjs +82 -0
  192. package/src/tools/enter-worktree.mjs +69 -0
  193. package/src/tools/exit-worktree.mjs +57 -0
  194. package/src/tools/glob.mjs +117 -0
  195. package/src/tools/grep.mjs +129 -0
  196. package/src/tools/lint.mjs +71 -0
  197. package/src/tools/ls.mjs +58 -0
  198. package/src/tools/lsp.mjs +115 -0
  199. package/src/tools/multi-edit.mjs +94 -0
  200. package/src/tools/notebook-edit.mjs +96 -0
  201. package/src/tools/read-mcp-resource.mjs +57 -0
  202. package/src/tools/read.mjs +138 -0
  203. package/src/tools/registry.mjs +132 -0
  204. package/src/tools/remote-trigger.mjs +84 -0
  205. package/src/tools/send-message.mjs +64 -0
  206. package/src/tools/skill.mjs +52 -0
  207. package/src/tools/test-runner.mjs +49 -0
  208. package/src/tools/todo-write.mjs +68 -0
  209. package/src/tools/tool-search.mjs +77 -0
  210. package/src/tools/web-fetch.mjs +65 -0
  211. package/src/tools/web-search.mjs +89 -0
  212. package/src/tools/write.mjs +55 -0
  213. package/src/ui/banner.mjs +237 -0
  214. package/src/ui/commands.mjs +499 -0
  215. package/src/ui/formatter.mjs +379 -0
  216. package/src/ui/markdown.mjs +278 -0
  217. package/src/ui/slash-commands.mjs +258 -0
  218. package/index.js +0 -1
@@ -0,0 +1,52 @@
1
+ import { NextResponse } from 'next/server'
2
+ import fs from 'fs/promises'
3
+ import path from 'path'
4
+ import os from 'os'
5
+ import { readMemories } from '@/lib/claude-reader'
6
+
7
+ export const dynamic = 'force-dynamic'
8
+
9
+ const CLAUDE_DIR = path.join(os.homedir(), '.orca')
10
+
11
+ export async function GET() {
12
+ const memories = await readMemories()
13
+ return NextResponse.json({ memories })
14
+ }
15
+
16
+ export async function PATCH(req: Request) {
17
+ try {
18
+ const { projectSlug, file, content } = await req.json() as {
19
+ projectSlug?: string
20
+ file?: string
21
+ content?: string
22
+ }
23
+
24
+ if (!projectSlug || !file || typeof content !== 'string') {
25
+ return NextResponse.json({ error: 'Missing required fields' }, { status: 400 })
26
+ }
27
+
28
+ // Only allow .md files
29
+ if (!file.endsWith('.md')) {
30
+ return NextResponse.json({ error: 'Only .md files allowed' }, { status: 400 })
31
+ }
32
+
33
+ // Prevent path traversal — slug and file must be plain names (no slashes or dots)
34
+ if (/[/\\]/.test(projectSlug) || /[/\\]/.test(file)) {
35
+ return NextResponse.json({ error: 'Invalid path' }, { status: 400 })
36
+ }
37
+
38
+ const filePath = path.join(CLAUDE_DIR, 'projects', projectSlug, 'memory', file)
39
+
40
+ // Ensure the resolved path stays within ~/.orca/projects/
41
+ const allowedRoot = path.join(CLAUDE_DIR, 'projects')
42
+ if (!filePath.startsWith(allowedRoot + path.sep)) {
43
+ return NextResponse.json({ error: 'Path outside allowed directory' }, { status: 403 })
44
+ }
45
+
46
+ await fs.mkdir(path.dirname(filePath), { recursive: true })
47
+ await fs.writeFile(filePath, content, 'utf-8')
48
+ return NextResponse.json({ ok: true })
49
+ } catch (err) {
50
+ return NextResponse.json({ error: String(err) }, { status: 500 })
51
+ }
52
+ }
@@ -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
+ }
@@ -0,0 +1,20 @@
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
+ }
@@ -0,0 +1,31 @@
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
+
@@ -0,0 +1,112 @@
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
+ }
@@ -0,0 +1,14 @@
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
+ }
@@ -0,0 +1,143 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { readStatsCache, getSessions, getClaudeStorageBytes } from '@/lib/claude-reader'
3
+ import { estimateTotalCostFromModel, getPricing } from '@/lib/pricing'
4
+ import type { DailyActivity, SessionMeta } from '@/types/claude'
5
+
6
+ export const dynamic = 'force-dynamic'
7
+
8
+ /** Compute daily activity from session JSONL — fresher than stats-cache */
9
+ function computeDailyActivityFromSessions(sessions: SessionMeta[]): DailyActivity[] {
10
+ const byDate = new Map<string, { messages: number; sessions: number; tools: number }>()
11
+ for (const s of sessions) {
12
+ const date = s.start_time.slice(0, 10)
13
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) continue
14
+ const existing = byDate.get(date) ?? { messages: 0, sessions: 0, tools: 0 }
15
+ existing.messages += (s.user_message_count ?? 0) + (s.assistant_message_count ?? 0)
16
+ existing.sessions += 1
17
+ existing.tools += Object.values(s.tool_counts ?? {}).reduce((a, b) => a + b, 0)
18
+ byDate.set(date, existing)
19
+ }
20
+ return Array.from(byDate.entries())
21
+ .map(([date, { messages, sessions: count, tools }]) => ({
22
+ date,
23
+ messageCount: messages,
24
+ sessionCount: count,
25
+ toolCallCount: tools,
26
+ }))
27
+ .sort((a, b) => a.date.localeCompare(b.date))
28
+ }
29
+
30
+ /** Merge stats dailyActivity with session-derived data; session data overrides for same dates */
31
+ function mergeDailyActivity(
32
+ fromStats: DailyActivity[],
33
+ fromSessions: DailyActivity[]
34
+ ): DailyActivity[] {
35
+ const map = new Map<string, DailyActivity>()
36
+ for (const d of fromStats) map.set(d.date, d)
37
+ for (const d of fromSessions) map.set(d.date, d)
38
+ return Array.from(map.values()).sort((a, b) => a.date.localeCompare(b.date))
39
+ }
40
+
41
+ export async function GET() {
42
+ const [stats, sessions, storageBytes] = await Promise.all([
43
+ readStatsCache(),
44
+ getSessions(),
45
+ getClaudeStorageBytes(),
46
+ ])
47
+
48
+ const dailyFromSessions = computeDailyActivityFromSessions(sessions)
49
+ const dailyActivity = stats
50
+ ? mergeDailyActivity(stats.dailyActivity ?? [], dailyFromSessions)
51
+ : dailyFromSessions
52
+
53
+ const modelUsage = stats?.modelUsage ?? {}
54
+
55
+ // Compute estimated total cost from modelUsage
56
+ let totalCost = 0
57
+ let totalCacheSavings = 0
58
+ for (const [model, usage] of Object.entries(modelUsage)) {
59
+ const cost = estimateTotalCostFromModel(model, usage)
60
+ totalCost += cost
61
+ const p = getPricing(model)
62
+ totalCacheSavings += (usage.cacheReadInputTokens ?? 0) * (p.input - p.cacheRead)
63
+ }
64
+
65
+ // Compute total tokens
66
+ let totalInputTokens = 0
67
+ let totalOutputTokens = 0
68
+ let totalCacheReadTokens = 0
69
+ let totalCacheWriteTokens = 0
70
+ for (const usage of Object.values(modelUsage)) {
71
+ totalInputTokens += usage.inputTokens ?? 0
72
+ totalOutputTokens += usage.outputTokens ?? 0
73
+ totalCacheReadTokens += usage.cacheReadInputTokens ?? 0
74
+ totalCacheWriteTokens += usage.cacheCreationInputTokens ?? 0
75
+ }
76
+ const totalTokens = totalInputTokens + totalOutputTokens + totalCacheReadTokens + totalCacheWriteTokens
77
+
78
+ // Aggregate tool calls total
79
+ let totalToolCalls = 0
80
+ for (const s of sessions) {
81
+ for (const count of Object.values(s.tool_counts ?? {})) {
82
+ totalToolCalls += count
83
+ }
84
+ }
85
+
86
+ // Active days (days with at least 1 session)
87
+ const activeDays = dailyActivity.filter(d => d.sessionCount > 0).length
88
+
89
+ // Average session length
90
+ const avgSessionMinutes =
91
+ sessions.length > 0
92
+ ? sessions.reduce((sum, s) => sum + (s.duration_minutes ?? 0), 0) / sessions.length
93
+ : 0
94
+
95
+ // Sessions this month & week
96
+ const now = new Date()
97
+ const monthStart = new Date(now.getFullYear(), now.getMonth(), 1)
98
+ const weekStart = new Date(now)
99
+ weekStart.setDate(now.getDate() - 7)
100
+
101
+ const sessionsThisMonth = sessions.filter(
102
+ s => new Date(s.start_time) >= monthStart
103
+ ).length
104
+ const sessionsThisWeek = sessions.filter(
105
+ s => new Date(s.start_time) >= weekStart
106
+ ).length
107
+
108
+ const statsOut = stats
109
+ ? { ...stats, dailyActivity }
110
+ : {
111
+ version: 0,
112
+ lastComputedDate: '',
113
+ dailyActivity,
114
+ tokensByDate: [],
115
+ modelUsage: {},
116
+ totalSessions: sessions.length,
117
+ totalMessages: sessions.reduce((s, m) => s + (m.user_message_count ?? 0) + (m.assistant_message_count ?? 0), 0),
118
+ longestSession: { sessionId: '', duration: 0, messageCount: 0, timestamp: '' },
119
+ firstSessionDate: sessions[sessions.length - 1]?.start_time ?? '',
120
+ hourCounts: {},
121
+ totalSpeculationTimeSavedMs: 0,
122
+ }
123
+
124
+ return NextResponse.json({
125
+ stats: statsOut,
126
+ computed: {
127
+ totalCost,
128
+ totalCacheSavings,
129
+ totalTokens,
130
+ totalInputTokens,
131
+ totalOutputTokens,
132
+ totalCacheReadTokens,
133
+ totalCacheWriteTokens,
134
+ totalToolCalls,
135
+ activeDays,
136
+ avgSessionMinutes,
137
+ sessionsThisMonth,
138
+ sessionsThisWeek,
139
+ storageBytes,
140
+ sessionCount: sessions.length,
141
+ },
142
+ })
143
+ }
@@ -0,0 +1,9 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { readTodos } from '@/lib/claude-reader'
3
+
4
+ export const dynamic = 'force-dynamic'
5
+
6
+ export async function GET() {
7
+ const todos = await readTodos()
8
+ return NextResponse.json({ todos })
9
+ }