@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
@@ -0,0 +1,249 @@
1
+ 'use client'
2
+
3
+ import useSWR from 'swr'
4
+ import { TopBar } from '@/components/layout/top-bar'
5
+ import { ToolRankingChart } from '@/components/tools/tool-ranking-chart'
6
+ import { McpServerPanel } from '@/components/tools/mcp-server-panel'
7
+ import { FeatureAdoptionTable } from '@/components/tools/feature-adoption-table'
8
+ import { VersionHistoryTable } from '@/components/tools/version-history-table'
9
+ import { CATEGORY_COLORS, CATEGORY_LABELS } from '@/lib/tool-categories'
10
+ import type { ToolsAnalytics } from '@/types/claude'
11
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
12
+ import { Skeleton } from '@/components/ui/skeleton'
13
+ import { Alert, AlertDescription } from '@/components/ui/alert'
14
+ import { Badge } from '@/components/ui/badge'
15
+ import { AlertTriangle, Wrench, Server, Zap, GitBranch } from 'lucide-react'
16
+
17
+ const fetcher = (url: string) =>
18
+ fetch(url).then(r => { if (!r.ok) throw new Error(`API error ${r.status}`); return r.json() })
19
+
20
+ export default function ToolsPage() {
21
+ const { data, error, isLoading } = useSWR<ToolsAnalytics>('/api/tools', fetcher, { refreshInterval: 5_000 })
22
+
23
+ return (
24
+ <div className="flex flex-col min-h-screen">
25
+ <TopBar title="Tools & Features" subtitle="Every tool call, MCP server, and feature" />
26
+ <div className="p-6 space-y-6">
27
+
28
+ {error && (
29
+ <Alert variant="destructive">
30
+ <AlertTriangle className="h-4 w-4" />
31
+ <AlertDescription>Error loading data: {String(error)}</AlertDescription>
32
+ </Alert>
33
+ )}
34
+
35
+ {isLoading && (
36
+ <div className="space-y-4">
37
+ <div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
38
+ {Array.from({ length: 4 }).map((_, i) => <Skeleton key={i} className="h-28 rounded-xl" />)}
39
+ </div>
40
+ {Array.from({ length: 3 }).map((_, i) => <Skeleton key={i} className="h-48 rounded-xl" />)}
41
+ </div>
42
+ )}
43
+
44
+ {data && (
45
+ <>
46
+ {/* Hero stat cards */}
47
+ <div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
48
+ <Card>
49
+ <CardHeader className="pb-2">
50
+ <CardDescription className="flex items-center gap-2">
51
+ <Zap className="w-4 h-4" /> Tool Calls
52
+ </CardDescription>
53
+ <CardTitle className="text-3xl font-bold tabular-nums text-[#d97706]">
54
+ {data.total_tool_calls.toLocaleString()}
55
+ </CardTitle>
56
+ </CardHeader>
57
+ <CardContent>
58
+ <p className="text-xs text-muted-foreground">Total all time</p>
59
+ </CardContent>
60
+ </Card>
61
+
62
+ <Card>
63
+ <CardHeader className="pb-2">
64
+ <CardDescription className="flex items-center gap-2">
65
+ <Wrench className="w-4 h-4" /> Unique Tools
66
+ </CardDescription>
67
+ <CardTitle className="text-3xl font-bold tabular-nums">
68
+ {data.tools.length}
69
+ </CardTitle>
70
+ </CardHeader>
71
+ <CardContent>
72
+ <p className="text-xs text-muted-foreground">Distinct tools used</p>
73
+ </CardContent>
74
+ </Card>
75
+
76
+ <Card>
77
+ <CardHeader className="pb-2">
78
+ <CardDescription className="flex items-center gap-2">
79
+ <Server className="w-4 h-4" /> MCP Servers
80
+ </CardDescription>
81
+ <CardTitle className="text-3xl font-bold tabular-nums text-[#34d399]">
82
+ {data.mcp_servers.length}
83
+ </CardTitle>
84
+ </CardHeader>
85
+ <CardContent>
86
+ <p className="text-xs text-muted-foreground">Connected servers</p>
87
+ </CardContent>
88
+ </Card>
89
+
90
+ <Card>
91
+ <CardHeader className="pb-2">
92
+ <CardDescription className="flex items-center gap-2">
93
+ <AlertTriangle className="w-4 h-4" /> Errors
94
+ </CardDescription>
95
+ <CardTitle className={`text-3xl font-bold tabular-nums ${data.total_errors > 0 ? 'text-red-400' : 'text-muted-foreground'}`}>
96
+ {data.total_errors}
97
+ </CardTitle>
98
+ </CardHeader>
99
+ <CardContent>
100
+ <p className="text-xs text-muted-foreground">
101
+ {data.total_tool_calls > 0
102
+ ? `${((data.total_errors / data.total_tool_calls) * 100).toFixed(1)}% error rate`
103
+ : 'No errors'}
104
+ </p>
105
+ </CardContent>
106
+ </Card>
107
+ </div>
108
+
109
+ {/* Category legend */}
110
+ <div className="flex flex-wrap gap-2">
111
+ {Object.entries(CATEGORY_COLORS).map(([cat, color]) => (
112
+ <Badge key={cat} variant="outline" className="gap-1.5">
113
+ <span className="w-2 h-2 rounded-full shrink-0" style={{ backgroundColor: color }} />
114
+ {CATEGORY_LABELS[cat as keyof typeof CATEGORY_LABELS]}
115
+ </Badge>
116
+ ))}
117
+ </div>
118
+
119
+ {/* Tool ranking */}
120
+ <Card>
121
+ <CardHeader>
122
+ <div className="flex items-start justify-between">
123
+ <div>
124
+ <CardTitle>Tool Ranking</CardTitle>
125
+ <CardDescription>All tools ranked by total calls</CardDescription>
126
+ </div>
127
+ <Wrench className="w-4 h-4 text-muted-foreground mt-0.5" />
128
+ </div>
129
+ </CardHeader>
130
+ <CardContent>
131
+ <ToolRankingChart tools={data.tools} />
132
+ </CardContent>
133
+ </Card>
134
+
135
+ {/* MCP server details */}
136
+ {data.mcp_servers.length > 0 && (
137
+ <Card>
138
+ <CardHeader>
139
+ <div className="flex items-start justify-between">
140
+ <div>
141
+ <CardTitle>MCP Server Details</CardTitle>
142
+ <CardDescription>Connected MCP servers and their tools</CardDescription>
143
+ </div>
144
+ <Server className="w-4 h-4 text-muted-foreground mt-0.5" />
145
+ </div>
146
+ </CardHeader>
147
+ <CardContent>
148
+ <McpServerPanel servers={data.mcp_servers} />
149
+ </CardContent>
150
+ </Card>
151
+ )}
152
+
153
+ {/* Feature adoption */}
154
+ <Card>
155
+ <CardHeader>
156
+ <CardTitle>Feature Adoption</CardTitle>
157
+ <CardDescription>How often advanced features are used across sessions</CardDescription>
158
+ </CardHeader>
159
+ <CardContent>
160
+ <FeatureAdoptionTable
161
+ adoption={data.feature_adoption}
162
+ totalSessions={(() => {
163
+ const first = Object.values(data.feature_adoption ?? {})[0]
164
+ return first ? Math.round(first.sessions / Math.max(0.001, first.pct)) : 0
165
+ })()}
166
+ />
167
+ </CardContent>
168
+ </Card>
169
+
170
+ {/* Error analysis */}
171
+ {data.total_errors > 0 && (
172
+ <Card>
173
+ <CardHeader>
174
+ <CardTitle>Tool Error Analysis</CardTitle>
175
+ <CardDescription>
176
+ {data.total_errors} errors ·{' '}
177
+ {((data.total_errors / data.total_tool_calls) * 100).toFixed(1)}% error rate
178
+ </CardDescription>
179
+ </CardHeader>
180
+ <CardContent>
181
+ <div className="space-y-2.5">
182
+ {Object.entries(data.error_categories).sort(([, a], [, b]) => b - a).map(([cat, count]) => {
183
+ const max = Math.max(...Object.values(data.error_categories))
184
+ const width = Math.max(4, Math.round((count / max) * 100))
185
+ return (
186
+ <div key={cat} className="flex items-center gap-3">
187
+ <span className="text-sm text-muted-foreground w-36 truncate">{cat}</span>
188
+ <div className="flex-1 h-2 bg-muted rounded-full overflow-hidden">
189
+ <div className="h-full rounded-full bg-red-400/50" style={{ width: `${width}%` }} />
190
+ </div>
191
+ <span className="text-sm text-red-400 tabular-nums w-8 text-right">{count}</span>
192
+ </div>
193
+ )
194
+ })}
195
+ </div>
196
+ </CardContent>
197
+ </Card>
198
+ )}
199
+
200
+ {/* Version history */}
201
+ {data.versions.length > 0 && (
202
+ <Card>
203
+ <CardHeader>
204
+ <CardTitle>Orca Version History</CardTitle>
205
+ <CardDescription>Versions seen across your sessions</CardDescription>
206
+ </CardHeader>
207
+ <CardContent>
208
+ <VersionHistoryTable versions={data.versions} />
209
+ </CardContent>
210
+ </Card>
211
+ )}
212
+
213
+ {/* Git branch analytics */}
214
+ {data.branches.length > 0 && (
215
+ <Card>
216
+ <CardHeader>
217
+ <div className="flex items-start justify-between">
218
+ <div>
219
+ <CardTitle>Git Branch Analytics</CardTitle>
220
+ <CardDescription>Most active branches by turn count</CardDescription>
221
+ </div>
222
+ <GitBranch className="w-4 h-4 text-muted-foreground mt-0.5" />
223
+ </div>
224
+ </CardHeader>
225
+ <CardContent>
226
+ <div className="space-y-2">
227
+ {data.branches.map(({ branch, turns }) => {
228
+ const max = data.branches[0]?.turns ?? 1
229
+ const width = Math.max(4, Math.round((turns / max) * 100))
230
+ return (
231
+ <div key={branch} className="flex items-center gap-3">
232
+ <span className="text-sm text-muted-foreground w-28 truncate font-mono">{branch}</span>
233
+ <div className="flex-1 h-2 bg-muted rounded-full overflow-hidden">
234
+ <div className="h-full rounded-full bg-[#34d399]/50" style={{ width: `${width}%` }} />
235
+ </div>
236
+ <span className="text-xs text-muted-foreground tabular-nums w-24 text-right">{turns.toLocaleString()} turns</span>
237
+ </div>
238
+ )
239
+ })}
240
+ </div>
241
+ </CardContent>
242
+ </Card>
243
+ )}
244
+ </>
245
+ )}
246
+ </div>
247
+ </div>
248
+ )
249
+ }
package/pulse/cli.js ADDED
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Orca Pulse — local analytics dashboard launcher.
4
+ * Launches a Next.js dev server from ~/.orca-pulse/ cache directory.
5
+ */
6
+
7
+ const { spawn, exec } = require('child_process')
8
+ const net = require('net')
9
+ const os = require('os')
10
+ const path = require('path')
11
+ const fs = require('fs')
12
+
13
+ const PKG_DIR = __dirname
14
+ const CACHE_DIR = path.join(os.homedir(), '.orca-pulse')
15
+
16
+ // ANSI helpers — Orca cyan palette
17
+ const C = '\x1b[36m' // cyan
18
+ const C2 = '\x1b[96m' // bright cyan
19
+ const DIM = '\x1b[2m'
20
+ const B = '\x1b[1m'
21
+ const R = '\x1b[0m'
22
+ const G = '\x1b[32m'
23
+
24
+ function printBanner() {
25
+ const art = [
26
+ `${C}${B} ██████╗ ██████╗ ██████╗ █████╗ ██████╗ ██╗ ██╗██╗ ███████╗███████╗${R}`,
27
+ `${C}${B}██╔═══██╗██╔══██╗██╔════╝██╔══██╗ ██╔══██╗██║ ██║██║ ██╔════╝██╔════╝${R}`,
28
+ `${C2}${B}██║ ██║██████╔╝██║ ███████║ ██████╔╝██║ ██║██║ ███████╗█████╗ ${R}`,
29
+ `${C2}${B}██║ ██║██╔══██╗██║ ██╔══██║ ██╔═══╝ ██║ ██║██║ ╚════██║██╔══╝ ${R}`,
30
+ `${C}${B}╚██████╔╝██║ ██║╚██████╗██║ ██║ ██║ ╚██████╔╝███████╗███████║███████╗${R}`,
31
+ `${C}${B} ╚═════╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝${R}`,
32
+ ]
33
+
34
+ console.log()
35
+ art.forEach((line) => console.log(' ' + line))
36
+ console.log()
37
+ const configDir = process.env.ORCA_CONFIG_DIR ?? path.join(os.homedir(), '.orca')
38
+ console.log(` ${B}${C}Orca Pulse${R} ${DIM}— real-time analytics for your Orca sessions${R}`)
39
+ console.log()
40
+ console.log(` ${DIM}Data dir:${R} ${C2}${configDir}${R}`)
41
+ console.log()
42
+ }
43
+
44
+ function findFreePort(port = 3000) {
45
+ return new Promise((resolve) => {
46
+ const server = net.createServer()
47
+ server.unref()
48
+ server.on('error', () => resolve(findFreePort(port + 1)))
49
+ server.listen(port, () => server.close(() => resolve(port)))
50
+ })
51
+ }
52
+
53
+ function openBrowser(url) {
54
+ const cmd =
55
+ process.platform === 'darwin' ? `open "${url}"` :
56
+ process.platform === 'win32' ? `start "" "${url}"` :
57
+ `xdg-open "${url}"`
58
+ exec(cmd)
59
+ }
60
+
61
+ // Source dirs/files to mirror into ~/.orca-pulse/
62
+ const SRC_DIRS = ['app', 'components', 'lib', 'types', 'public']
63
+ const SRC_FILES = ['next.config.ts', 'tsconfig.json', 'postcss.config.mjs', 'components.json']
64
+
65
+ function syncSource(pkg) {
66
+ fs.mkdirSync(CACHE_DIR, { recursive: true })
67
+ for (const dir of SRC_DIRS) {
68
+ const src = path.join(PKG_DIR, dir)
69
+ if (fs.existsSync(src)) {
70
+ fs.cpSync(src, path.join(CACHE_DIR, dir), { recursive: true, force: true })
71
+ }
72
+ }
73
+ for (const file of SRC_FILES) {
74
+ const src = path.join(PKG_DIR, file)
75
+ if (fs.existsSync(src)) {
76
+ fs.copyFileSync(src, path.join(CACHE_DIR, file))
77
+ }
78
+ }
79
+ // Write a minimal package.json with only runtime dependencies
80
+ fs.writeFileSync(path.join(CACHE_DIR, 'package.json'), JSON.stringify({
81
+ name: 'orca-pulse-runtime',
82
+ version: pkg.version,
83
+ dependencies: pkg.dependencies,
84
+ }, null, 2))
85
+ }
86
+
87
+ async function main() {
88
+ printBanner()
89
+
90
+ const pkg = require(path.join(PKG_DIR, 'package.json'))
91
+
92
+ // Check whether ~/.orca-pulse/ is up-to-date for this version
93
+ const versionFile = path.join(CACHE_DIR, '.orca-pulse-version')
94
+ const cachedVersion = fs.existsSync(versionFile)
95
+ ? fs.readFileSync(versionFile, 'utf8').trim()
96
+ : null
97
+
98
+ const nextCli = path.join(CACHE_DIR, 'node_modules', 'next', 'dist', 'bin', 'next')
99
+ const needsSetup = cachedVersion !== pkg.version || !fs.existsSync(nextCli)
100
+
101
+ if (needsSetup) {
102
+ console.log(` ${DIM}Setting up (first run, may take a minute)...${R}\n`)
103
+
104
+ syncSource(pkg)
105
+
106
+ await new Promise((resolve, reject) => {
107
+ const install = spawn('npm', ['install', '--prefer-offline', '--no-package-lock'], {
108
+ cwd: CACHE_DIR,
109
+ stdio: 'inherit',
110
+ shell: true,
111
+ })
112
+ install.on('exit', (code) =>
113
+ code === 0 ? resolve() : reject(new Error(`npm install failed (exit ${code})`))
114
+ )
115
+ })
116
+
117
+ fs.writeFileSync(versionFile, pkg.version)
118
+ }
119
+
120
+ const port = await findFreePort(3000)
121
+ const url = `http://localhost:${port}`
122
+
123
+ // Pass ORCA_CONFIG_DIR to the Next.js process so it reads from ~/.orca/
124
+ const orcaDir = process.env.ORCA_CONFIG_DIR ?? path.join(os.homedir(), '.orca')
125
+ const env = {
126
+ ...process.env,
127
+ PORT: String(port),
128
+ ORCA_CONFIG_DIR: orcaDir,
129
+ CLAUDE_CONFIG_DIR: orcaDir,
130
+ }
131
+
132
+ console.log(` ${DIM}Starting server on${R} ${C2}${B}${url}${R}\n`)
133
+
134
+ const child = spawn(process.execPath, [nextCli, 'dev', '--port', String(port)], {
135
+ cwd: CACHE_DIR,
136
+ stdio: [process.platform === 'win32' ? 'ignore' : 'inherit', 'pipe', 'pipe'],
137
+ env,
138
+ })
139
+
140
+ let opened = false
141
+
142
+ function checkReady(text) {
143
+ if (!opened && /Local:|ready|started server/i.test(text)) {
144
+ opened = true
145
+ console.log(`\n ${G}${B}Ready${R} ${C}${url}${R}\n`)
146
+ openBrowser(url)
147
+ }
148
+ }
149
+
150
+ child.stdout.on('data', (d) => { process.stdout.write(d); checkReady(d.toString()) })
151
+ child.stderr.on('data', (d) => { process.stderr.write(d); checkReady(d.toString()) })
152
+
153
+ child.on('exit', (code) => process.exit(code ?? 0))
154
+
155
+ process.on('SIGINT', () => { child.kill(); process.exit(0) })
156
+ process.on('SIGTERM', () => { child.kill(); process.exit(0) })
157
+ }
158
+
159
+ main().catch((err) => { console.error(err); process.exit(1) })
@@ -0,0 +1,35 @@
1
+ 'use client'
2
+
3
+ import { BarChart, Bar, XAxis, YAxis, Tooltip, CartesianGrid, Cell, ResponsiveContainer } from 'recharts'
4
+
5
+ interface Props {
6
+ data: Array<{ day: string; count: number }>
7
+ }
8
+
9
+ export function DayOfWeekChart({ data }: Props) {
10
+ const max = Math.max(...data.map(d => d.count), 1)
11
+ return (
12
+ <div>
13
+ <h3 className="text-[13px] font-bold text-muted-foreground uppercase tracking-widest mb-3">Day of Week</h3>
14
+ <ResponsiveContainer width="100%" height={140}>
15
+ <BarChart data={data} margin={{ top: 4, right: 8, bottom: 0, left: 0 }}>
16
+ <CartesianGrid strokeDasharray="3 3" stroke="var(--border)" vertical={false} />
17
+ <XAxis dataKey="day" tick={{ fontSize: 12, fill: 'var(--muted-foreground)' }} tickLine={false} axisLine={false} />
18
+ <YAxis tick={{ fontSize: 11, fill: 'var(--muted-foreground)' }} tickLine={false} axisLine={false} width={28} />
19
+ <Tooltip
20
+ contentStyle={{ background: 'var(--card)', border: '1px solid var(--border)', borderRadius: 4, fontSize: 12 }}
21
+ formatter={(val: number | undefined) => [(val ?? 0).toLocaleString(), 'messages']}
22
+ />
23
+ <Bar dataKey="count" radius={[3, 3, 0, 0]}>
24
+ {data.map((d, i) => (
25
+ <Cell
26
+ key={i}
27
+ fill={d.count === max ? '#d97706' : d.count > max * 0.6 ? '#d97706aa' : '#d9770640'}
28
+ />
29
+ ))}
30
+ </Bar>
31
+ </BarChart>
32
+ </ResponsiveContainer>
33
+ </div>
34
+ )
35
+ }
@@ -0,0 +1,36 @@
1
+ interface Props {
2
+ current: number
3
+ longest: number
4
+ totalActiveDays: number
5
+ mostActiveDay: string
6
+ mostActiveDayMsgs: number
7
+ }
8
+
9
+ export function StreakCard({ current, longest, totalActiveDays, mostActiveDay, mostActiveDayMsgs }: Props) {
10
+ return (
11
+ <div className="grid grid-cols-2 gap-3 text-[13px]">
12
+ <div className="border border-border rounded p-3 bg-card">
13
+ <p className="text-muted-foreground uppercase tracking-wider text-[12px] mb-1">Current Streak</p>
14
+ <p className="text-2xl font-bold text-primary">{current}</p>
15
+ <p className="text-muted-foreground/60 text-[12px]">consecutive days</p>
16
+ </div>
17
+ <div className="border border-border rounded p-3 bg-card">
18
+ <p className="text-muted-foreground uppercase tracking-wider text-[12px] mb-1">Longest Streak</p>
19
+ <p className="text-2xl font-bold text-blue-700 dark:text-[#60a5fa]">{longest}</p>
20
+ <p className="text-muted-foreground/60 text-[12px]">consecutive days</p>
21
+ </div>
22
+ <div className="border border-border rounded p-3 bg-card">
23
+ <p className="text-muted-foreground uppercase tracking-wider text-[12px] mb-1">Active Days</p>
24
+ <p className="text-2xl font-bold text-foreground">{totalActiveDays}</p>
25
+ <p className="text-muted-foreground/60 text-[12px]">total days with activity</p>
26
+ </div>
27
+ {mostActiveDay && (
28
+ <div className="border border-border rounded p-3 bg-card">
29
+ <p className="text-muted-foreground uppercase tracking-wider text-[12px] mb-1">Most Active Day</p>
30
+ <p className="text-sm font-bold text-[#34d399]">{mostActiveDay}</p>
31
+ <p className="text-muted-foreground/60 text-[12px]">{mostActiveDayMsgs.toLocaleString()} messages</p>
32
+ </div>
33
+ )}
34
+ </div>
35
+ )
36
+ }
@@ -0,0 +1,76 @@
1
+ import { formatCost, formatPct, formatTokens } from '@/lib/decode'
2
+ import type { ModelCostBreakdown } from '@/types/claude'
3
+ import { PieChart, Pie, Cell, Tooltip, ResponsiveContainer } from 'recharts'
4
+
5
+ interface Props {
6
+ models: ModelCostBreakdown[]
7
+ totalSavings: number
8
+ }
9
+
10
+ export function CacheEfficiencyPanel({ models, totalSavings }: Props) {
11
+ const totalCacheRead = models.reduce((s, m) => s + m.cache_read_tokens, 0)
12
+ const totalInput = models.reduce((s, m) => s + m.input_tokens, 0)
13
+ const totalContext = totalInput + totalCacheRead
14
+ const hitRate = totalContext > 0 ? totalCacheRead / totalContext : 0
15
+ const totalCost = models.reduce((s, m) => s + m.estimated_cost, 0)
16
+ const wouldHavePaid = totalCost + totalSavings
17
+
18
+ const pieData = [
19
+ { name: 'Cache Read', value: totalCacheRead, color: '#34d399' },
20
+ { name: 'Direct Input', value: totalInput, color: 'var(--viz-sky)' },
21
+ ]
22
+
23
+ return (
24
+ <div className="grid grid-cols-[1fr_160px] gap-4 items-start">
25
+ <div className="space-y-2 text-[13px]">
26
+ <div className="flex items-center justify-between">
27
+ <span className="text-muted-foreground">Cache hit rate</span>
28
+ <span className="text-[#34d399] font-bold text-lg">{(hitRate * 100).toFixed(1)}%</span>
29
+ </div>
30
+ <div className="flex items-center justify-between">
31
+ <span className="text-muted-foreground">Context from cache</span>
32
+ <span className="text-foreground font-mono">{formatTokens(totalCacheRead)}</span>
33
+ </div>
34
+ <div className="flex items-center justify-between">
35
+ <span className="text-muted-foreground">Context from input</span>
36
+ <span className="text-foreground font-mono">{formatTokens(totalInput)}</span>
37
+ </div>
38
+ <div className="border-t border-border/50 pt-2 mt-2 space-y-1.5">
39
+ <div className="flex items-center justify-between">
40
+ <span className="text-muted-foreground">Without cache</span>
41
+ <span className="text-red-400 font-mono">{formatCost(wouldHavePaid)}</span>
42
+ </div>
43
+ <div className="flex items-center justify-between">
44
+ <span className="text-muted-foreground">You paid</span>
45
+ <span className="text-foreground font-mono">{formatCost(totalCost)}</span>
46
+ </div>
47
+ <div className="flex items-center justify-between font-bold">
48
+ <span className="text-[#34d399]">Savings</span>
49
+ <span className="text-[#34d399] font-mono">{formatCost(totalSavings)}</span>
50
+ </div>
51
+ </div>
52
+ </div>
53
+ <ResponsiveContainer width="100%" height={140}>
54
+ <PieChart>
55
+ <Pie
56
+ data={pieData}
57
+ cx="50%"
58
+ cy="50%"
59
+ innerRadius={35}
60
+ outerRadius={60}
61
+ dataKey="value"
62
+ strokeWidth={0}
63
+ >
64
+ {pieData.map((entry, i) => (
65
+ <Cell key={i} fill={entry.color} />
66
+ ))}
67
+ </Pie>
68
+ <Tooltip
69
+ contentStyle={{ background: 'var(--card)', border: '1px solid var(--border)', borderRadius: 4, fontSize: 12 }}
70
+ formatter={(val: number | undefined, name?: string) => [formatTokens(val ?? 0), name ?? '']}
71
+ />
72
+ </PieChart>
73
+ </ResponsiveContainer>
74
+ </div>
75
+ )
76
+ }
@@ -0,0 +1,48 @@
1
+ 'use client'
2
+
3
+ import { BarChart, Bar, XAxis, YAxis, Tooltip, CartesianGrid, Cell, ResponsiveContainer } from 'recharts'
4
+ import { formatCost } from '@/lib/decode'
5
+ import type { ProjectCost } from '@/types/claude'
6
+
7
+ interface Props {
8
+ projects: ProjectCost[]
9
+ }
10
+
11
+ export function CostByProjectChart({ projects }: Props) {
12
+ const top = projects.slice(0, 12)
13
+
14
+ return (
15
+ <div>
16
+ <h3 className="text-[13px] font-bold text-muted-foreground uppercase tracking-widest mb-3">Cost by Project</h3>
17
+ <ResponsiveContainer width="100%" height={Math.max(120, top.length * 28)}>
18
+ <BarChart data={top} layout="vertical" margin={{ top: 0, right: 60, bottom: 0, left: 8 }}>
19
+ <CartesianGrid strokeDasharray="3 3" stroke="var(--border)" horizontal={false} />
20
+ <XAxis
21
+ type="number"
22
+ tick={{ fontSize: 11, fill: 'var(--muted-foreground)' }}
23
+ tickLine={false}
24
+ axisLine={false}
25
+ tickFormatter={v => `$${v.toFixed(2)}`}
26
+ />
27
+ <YAxis
28
+ type="category"
29
+ dataKey="display_name"
30
+ tick={{ fontSize: 12, fill: 'var(--muted-foreground)' }}
31
+ tickLine={false}
32
+ axisLine={false}
33
+ width={90}
34
+ />
35
+ <Tooltip
36
+ contentStyle={{ background: 'var(--card)', border: '1px solid var(--border)', borderRadius: 4, fontSize: 12 }}
37
+ formatter={(val: number | undefined) => [formatCost(val ?? 0), 'Estimated cost']}
38
+ />
39
+ <Bar dataKey="estimated_cost" radius={[0, 3, 3, 0]}>
40
+ {top.map((_, i) => (
41
+ <Cell key={i} fill={i === 0 ? '#d97706' : '#d97706' + Math.max(30, 100 - i * 7).toString(16)} />
42
+ ))}
43
+ </Bar>
44
+ </BarChart>
45
+ </ResponsiveContainer>
46
+ </div>
47
+ )
48
+ }