@axplusb/kepler 0.0.1 → 1.0.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 (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 +101 -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
package/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # @devtarang/orca
2
+
3
+ Orca — AI coding agent that plans, builds, and ships software from your terminal. Multi-agent orchestration with explore, review, and architect sub-agents.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g @devtarang/orca
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```bash
14
+ orca login # Sign in via browser (GitHub/Google)
15
+ orca # Start interactive REPL
16
+ orca "add user authentication" # Run a single instruction
17
+ ```
18
+
19
+ ## Commands
20
+
21
+ ```
22
+ orca Start interactive REPL
23
+ orca "instruction" Run a single instruction and exit
24
+ orca login Sign in via browser
25
+ orca dashboard Open Orca Pulse analytics dashboard
26
+ orca sessions List recent local sessions
27
+ orca stats Aggregate session stats (tokens, cost, tools)
28
+ orca history Recent prompt history
29
+ orca version Show version
30
+ ```
31
+
32
+ ## REPL Commands
33
+
34
+ ```
35
+ /help Show available commands
36
+ /stats Session metrics (tokens, cost, tools)
37
+ /cost Detailed cost breakdown by model
38
+ /history Conversation history
39
+ /clear Clear conversation history
40
+ /explore <query> Spawn read-only codebase explorer
41
+ /review <query> Spawn code review agent
42
+ /architect <query> Spawn architecture planning agent
43
+ /safety Show safety guardrail status
44
+ /revoke Revoke auto-approvals
45
+ /exit Exit the REPL
46
+ ```
47
+
48
+ ## Keyboard
49
+
50
+ ```
51
+ Esc Cancel current execution
52
+ Space Pause / resume execution
53
+ Ctrl+C Exit
54
+ ```
55
+
56
+ ## Configuration
57
+
58
+ Settings are managed via the web dashboard at [devtarang.ai/dashboard/settings](https://devtarang.ai/dashboard/settings) and synced to the CLI automatically.
59
+
60
+ - **API Key**: Add your OpenRouter/Anthropic/OpenAI key in Settings
61
+ - **Model**: Choose your preferred model (40+ supported)
62
+ - **Gateway**: Select provider (OpenRouter, Anthropic, OpenAI, Bedrock, Google AI, etc.)
63
+
64
+ Config directory: `~/.orca/`
65
+
66
+ ## Environment Variables
67
+
68
+ ```
69
+ TARANG_ENV Set backend (local, development, production)
70
+ ANTHROPIC_API_KEY Direct Anthropic API key
71
+ OPENROUTER_API_KEY OpenRouter API key
72
+ ORCA_CONFIG_DIR Override config directory (default: ~/.orca)
73
+ ```
74
+
75
+ ## Links
76
+
77
+ - [Tarang Platform](https://devtarang.ai)
78
+ - [Documentation](https://devtarang.ai/docs)
79
+
80
+ ## License
81
+
82
+ MIT
package/package.json CHANGED
@@ -1,7 +1,39 @@
1
1
  {
2
2
  "name": "@axplusb/kepler",
3
- "version": "0.0.1",
4
- "description": "CodeKepler — AI coding agent by axplusb.tech",
5
- "main": "index.js",
6
- "license": "MIT"
3
+ "version": "1.0.1",
4
+ "description": "Kepler — AI coding agent CLI. 30.7% on SWE-bench Lite. Powered by axplusb.tech",
5
+ "type": "module",
6
+ "bin": {
7
+ "kepler": "src/terminal/main.mjs"
8
+ },
9
+ "files": [
10
+ "src/",
11
+ "pulse/"
12
+ ],
13
+ "scripts": {
14
+ "start": "node src/terminal/main.mjs",
15
+ "test": "node test/test-sse-client.mjs && node test/test-tool-executor.mjs && node test/test-callback.mjs && node test/test-formatter.mjs && node test/test-terminal-rendering.mjs && node test/test-slash-commands.mjs && node test/test-approval.mjs && node test/test-session-manager.mjs && node test/test-safety.mjs && node test/test-jsonl-writer.mjs && node test/test-analytics.mjs && node test/test-stagnation.mjs"
16
+ },
17
+ "engines": {
18
+ "node": ">=18.0.0"
19
+ },
20
+ "keywords": [
21
+ "kepler",
22
+ "codekepler",
23
+ "ai",
24
+ "coding-agent",
25
+ "swe-bench",
26
+ "cli",
27
+ "code-generation",
28
+ "deepseek",
29
+ "axplusb"
30
+ ],
31
+ "license": "MIT",
32
+ "author": "axplusb.tech <hello@axplusb.tech>",
33
+ "homepage": "https://codekepler.ai",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/axplusb/kepler.git"
37
+ },
38
+ "dependencies": {}
7
39
  }
@@ -0,0 +1,190 @@
1
+ 'use client'
2
+
3
+ import useSWR from 'swr'
4
+ import { TopBar } from '@/components/layout/top-bar'
5
+ import { ActivityHeatmap } from '@/components/overview/activity-heatmap'
6
+ import { PeakHoursChart } from '@/components/overview/peak-hours-chart'
7
+ import { DayOfWeekChart } from '@/components/activity/day-of-week-chart'
8
+ import { UsageOverTimeChart } from '@/components/overview/usage-over-time-chart'
9
+ import type { DailyActivity } from '@/types/claude'
10
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
11
+ import { Skeleton } from '@/components/ui/skeleton'
12
+ import { Alert, AlertDescription } from '@/components/ui/alert'
13
+ import { AlertTriangle, Flame, CalendarDays, BarChart3, Clock, Zap, TrendingUp, Star } from 'lucide-react'
14
+
15
+ const fetcher = (url: string) =>
16
+ fetch(url).then(r => { if (!r.ok) throw new Error(`API error ${r.status}`); return r.json() })
17
+
18
+ interface ActivityData {
19
+ daily_activity: DailyActivity[]
20
+ hour_counts: Array<{ hour: number; count: number }>
21
+ dow_counts: Array<{ day: string; count: number }>
22
+ streaks: { current: number; longest: number }
23
+ most_active_day: string
24
+ most_active_day_msgs: number
25
+ total_active_days: number
26
+ }
27
+
28
+ function StatTile({
29
+ label,
30
+ value,
31
+ sub,
32
+ icon: Icon,
33
+ color,
34
+ }: {
35
+ label: string
36
+ value: string | number
37
+ sub: string
38
+ icon: React.ElementType
39
+ color: string
40
+ }) {
41
+ return (
42
+ <Card className="gap-0">
43
+ <CardHeader className="pb-2">
44
+ <CardDescription className="flex items-center gap-2 text-sm font-medium">
45
+ <Icon className="h-4 w-4 shrink-0 text-muted-foreground" />
46
+ {label}
47
+ </CardDescription>
48
+ <CardTitle className="text-3xl font-bold tabular-nums leading-none" style={{ color }}>
49
+ {value}
50
+ </CardTitle>
51
+ </CardHeader>
52
+ <CardContent className="pt-0">
53
+ <p className="text-xs text-muted-foreground">{sub}</p>
54
+ </CardContent>
55
+ </Card>
56
+ )
57
+ }
58
+
59
+ export default function ActivityPage() {
60
+ const { data, error, isLoading } = useSWR<ActivityData>('/api/activity', fetcher, { refreshInterval: 5_000 })
61
+
62
+ const hourCounts = data
63
+ ? Object.fromEntries(data.hour_counts.map(h => [String(h.hour), h.count]))
64
+ : {}
65
+
66
+ return (
67
+ <div className="flex flex-col min-h-screen">
68
+ <TopBar title="Activity" subtitle="Patterns, streaks, and peak hours" />
69
+ <div className="w-full space-y-4 p-4 md:p-6">
70
+
71
+ {error && (
72
+ <Alert variant="destructive">
73
+ <AlertTriangle className="h-4 w-4" />
74
+ <AlertDescription>Error loading data: {String(error)}</AlertDescription>
75
+ </Alert>
76
+ )}
77
+
78
+ {isLoading && (
79
+ <>
80
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
81
+ {Array.from({ length: 4 }).map((_, i) => <Skeleton key={i} className="h-28 rounded-xl" />)}
82
+ </div>
83
+ <div className="grid grid-cols-1 gap-4 md:grid-cols-2 md:items-stretch">
84
+ <Skeleton className="h-72 min-h-72 rounded-xl md:min-h-80" />
85
+ <Skeleton className="h-56 min-h-56 rounded-xl md:min-h-80" />
86
+ </div>
87
+ <div className="grid grid-cols-1 gap-4 md:grid-cols-2 md:items-stretch">
88
+ <Skeleton className="h-64 rounded-xl md:min-h-72" />
89
+ <Skeleton className="h-56 rounded-xl md:min-h-72" />
90
+ </div>
91
+ </>
92
+ )}
93
+
94
+ {data && (
95
+ <>
96
+ {/* Stat tiles */}
97
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
98
+ <StatTile
99
+ label="Current Streak"
100
+ value={data.streaks.current}
101
+ sub="consecutive days"
102
+ icon={Flame}
103
+ color="#f97316"
104
+ />
105
+ <StatTile
106
+ label="Longest Streak"
107
+ value={data.streaks.longest}
108
+ sub="personal best"
109
+ icon={Zap}
110
+ color="var(--viz-sky)"
111
+ />
112
+ <StatTile
113
+ label="Active Days"
114
+ value={data.total_active_days}
115
+ sub="total days with activity"
116
+ icon={TrendingUp}
117
+ color="#a78bfa"
118
+ />
119
+ <StatTile
120
+ label="Most Active Day"
121
+ value={data.most_active_day ? data.most_active_day.slice(5) : '—'}
122
+ sub={data.most_active_day_msgs ? `${data.most_active_day_msgs.toLocaleString()} messages` : 'no data'}
123
+ icon={Star}
124
+ color="#34d399"
125
+ />
126
+ </div>
127
+
128
+ {/* Row 1: Activity calendar | Peak hours */}
129
+ <div className="grid grid-cols-1 gap-4 md:grid-cols-2 md:items-stretch">
130
+ <Card className="flex h-full min-h-0 min-w-0 flex-col gap-2 py-4">
131
+ <CardHeader className="space-y-1 pb-0">
132
+ <CardTitle className="flex items-center gap-2 text-base font-semibold">
133
+ <CalendarDays className="h-4 w-4 shrink-0 text-muted-foreground" />
134
+ Activity Calendar
135
+ </CardTitle>
136
+ <CardDescription>GitHub-style contribution heatmap</CardDescription>
137
+ </CardHeader>
138
+ <CardContent className="flex flex-1 flex-col pt-3 pb-0">
139
+ <ActivityHeatmap data={data.daily_activity} />
140
+ </CardContent>
141
+ </Card>
142
+
143
+ <Card className="flex h-full min-h-0 min-w-0 flex-col gap-2 py-4">
144
+ <CardHeader className="space-y-1 pb-0">
145
+ <CardTitle className="flex items-center gap-2 text-base font-semibold">
146
+ <Clock className="h-4 w-4 shrink-0 text-muted-foreground" />
147
+ Peak Hours
148
+ </CardTitle>
149
+ <CardDescription>Activity by hour of day</CardDescription>
150
+ </CardHeader>
151
+ <CardContent className="flex flex-1 flex-col justify-between pt-3 pb-0">
152
+ <PeakHoursChart hourCounts={hourCounts} />
153
+ </CardContent>
154
+ </Card>
155
+ </div>
156
+
157
+ {/* Row 2: Usage over time | Day of week */}
158
+ <div className="grid grid-cols-1 gap-4 md:grid-cols-2 md:items-stretch">
159
+ <Card className="flex h-full min-h-0 min-w-0 flex-col gap-2 py-4">
160
+ <CardHeader className="space-y-1 pb-0">
161
+ <CardTitle className="flex items-center gap-2 text-base font-semibold">
162
+ <BarChart3 className="h-4 w-4 shrink-0 text-muted-foreground" />
163
+ Usage Over Time
164
+ </CardTitle>
165
+ <CardDescription>Messages and sessions over the last 90 days</CardDescription>
166
+ </CardHeader>
167
+ <CardContent className="flex flex-1 flex-col pt-3 pb-0">
168
+ <UsageOverTimeChart data={data.daily_activity} days={90} />
169
+ </CardContent>
170
+ </Card>
171
+
172
+ <Card className="flex h-full min-h-0 min-w-0 flex-col gap-2 py-4">
173
+ <CardHeader className="space-y-1 pb-0">
174
+ <CardTitle className="flex items-center gap-2 text-base font-semibold">
175
+ <CalendarDays className="h-4 w-4 shrink-0 text-muted-foreground" />
176
+ Day of Week
177
+ </CardTitle>
178
+ <CardDescription>Which days you use Orca most</CardDescription>
179
+ </CardHeader>
180
+ <CardContent className="flex flex-1 flex-col justify-between pt-3 pb-0">
181
+ <DayOfWeekChart data={data.dow_counts} />
182
+ </CardContent>
183
+ </Card>
184
+ </div>
185
+ </>
186
+ )}
187
+ </div>
188
+ </div>
189
+ )
190
+ }
@@ -0,0 +1,138 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { getSessions, readStatsCache } from '@/lib/claude-reader'
3
+ import type { DailyActivity, SessionMeta } from '@/types/claude'
4
+
5
+ export const dynamic = 'force-dynamic'
6
+
7
+ function computeStreaks(dates: Set<string>): { current: number; longest: number } {
8
+ const sorted = [...dates].sort()
9
+ if (sorted.length === 0) return { current: 0, longest: 0 }
10
+
11
+ let longest = 1
12
+ let streak = 1
13
+ for (let i = 1; i < sorted.length; i++) {
14
+ const prev = new Date(sorted[i - 1])
15
+ const curr = new Date(sorted[i])
16
+ const diff = (curr.getTime() - prev.getTime()) / 86_400_000
17
+ if (diff === 1) {
18
+ streak++
19
+ if (streak > longest) longest = streak
20
+ } else {
21
+ streak = 1
22
+ }
23
+ }
24
+
25
+ // Current streak (from today backwards)
26
+ const today = new Date().toISOString().slice(0, 10)
27
+ let current = 0
28
+ const d = new Date(today)
29
+ while (dates.has(d.toISOString().slice(0, 10))) {
30
+ current++
31
+ d.setDate(d.getDate() - 1)
32
+ }
33
+
34
+ return { current, longest }
35
+ }
36
+
37
+ function computeDailyActivityFromSessions(sessions: SessionMeta[]): DailyActivity[] {
38
+ const byDate = new Map<string, { messages: number; sessions: number; tools: number }>()
39
+
40
+ for (const s of sessions) {
41
+ if (!s.start_time) continue
42
+ const date = s.start_time.slice(0, 10)
43
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) continue
44
+
45
+ const existing = byDate.get(date) ?? { messages: 0, sessions: 0, tools: 0 }
46
+ existing.messages += (s.user_message_count ?? 0) + (s.assistant_message_count ?? 0)
47
+ existing.sessions += 1
48
+ existing.tools += Object.values(s.tool_counts ?? {}).reduce((sum, count) => sum + count, 0)
49
+ byDate.set(date, existing)
50
+ }
51
+
52
+ return Array.from(byDate.entries())
53
+ .map(([date, { messages, sessions: sessionCount, tools }]) => ({
54
+ date,
55
+ messageCount: messages,
56
+ sessionCount,
57
+ toolCallCount: tools,
58
+ }))
59
+ .sort((a, b) => a.date.localeCompare(b.date))
60
+ }
61
+
62
+ function mergeDailyActivity(fromStats: DailyActivity[], fromSessions: DailyActivity[]): DailyActivity[] {
63
+ const byDate = new Map<string, DailyActivity>()
64
+ for (const d of fromStats) byDate.set(d.date, d)
65
+ for (const d of fromSessions) byDate.set(d.date, d)
66
+ return Array.from(byDate.values()).sort((a, b) => a.date.localeCompare(b.date))
67
+ }
68
+
69
+ function computeHourCounts(sessions: SessionMeta[]): Array<{ hour: number; count: number }> {
70
+ const hourCounts = Array.from({ length: 24 }, () => 0)
71
+
72
+ for (const s of sessions) {
73
+ if (s.user_message_timestamps?.length) {
74
+ for (const ts of s.user_message_timestamps) {
75
+ const d = new Date(ts)
76
+ if (!isNaN(d.getTime())) hourCounts[d.getHours()]++
77
+ }
78
+ continue
79
+ }
80
+
81
+ if (s.message_hours?.length) {
82
+ for (const hour of s.message_hours) {
83
+ if (Number.isInteger(hour) && hour >= 0 && hour < 24) hourCounts[hour]++
84
+ }
85
+ continue
86
+ }
87
+
88
+ const d = new Date(s.start_time)
89
+ if (!isNaN(d.getTime())) hourCounts[d.getHours()]++
90
+ }
91
+
92
+ return hourCounts.map((count, hour) => ({ hour, count }))
93
+ }
94
+
95
+ export async function GET() {
96
+ const [stats, sessions] = await Promise.all([readStatsCache(), getSessions()])
97
+ const dailyFromSessions = computeDailyActivityFromSessions(sessions)
98
+ const dailyActivity = stats
99
+ ? mergeDailyActivity(stats.dailyActivity ?? [], dailyFromSessions)
100
+ : dailyFromSessions
101
+
102
+ // Day-of-week counts from session timestamps
103
+ const dowCounts: number[] = [0, 0, 0, 0, 0, 0, 0] // Sun=0..Sat=6
104
+ const activeDates = new Set<string>()
105
+
106
+ for (const s of sessions) {
107
+ if (!s.start_time) continue
108
+ const d = new Date(s.start_time)
109
+ if (isNaN(d.getTime())) continue
110
+ dowCounts[d.getDay()]++
111
+ activeDates.add(s.start_time.slice(0, 10))
112
+ }
113
+
114
+ const streaks = computeStreaks(activeDates)
115
+
116
+ // Most active day
117
+ let mostActiveDay = ''
118
+ let mostActiveMsgs = 0
119
+ for (const da of dailyActivity) {
120
+ if (da.messageCount > mostActiveMsgs) {
121
+ mostActiveMsgs = da.messageCount
122
+ mostActiveDay = da.date
123
+ }
124
+ }
125
+
126
+ return NextResponse.json({
127
+ daily_activity: dailyActivity,
128
+ hour_counts: computeHourCounts(sessions),
129
+ dow_counts: dowCounts.map((count, i) => ({
130
+ day: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][i],
131
+ count,
132
+ })),
133
+ streaks,
134
+ most_active_day: mostActiveDay,
135
+ most_active_day_msgs: mostActiveMsgs,
136
+ total_active_days: activeDates.size,
137
+ })
138
+ }
@@ -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 ~/.orca/ live data.
28
+ // The import feature shows a diff preview only.
29
+
30
+ return NextResponse.json(diff)
31
+ }