@bahulam/code 2.6.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 (273) hide show
  1. package/README.md +80 -0
  2. package/package.json +49 -0
  3. package/pulse/app/activity/page.tsx +190 -0
  4. package/pulse/app/api/activity/route.ts +138 -0
  5. package/pulse/app/api/benchmark/route.ts +113 -0
  6. package/pulse/app/api/benchmarks/route.ts +195 -0
  7. package/pulse/app/api/costs/route.ts +88 -0
  8. package/pulse/app/api/export/route.ts +77 -0
  9. package/pulse/app/api/history/route.ts +11 -0
  10. package/pulse/app/api/import/route.ts +31 -0
  11. package/pulse/app/api/memory/route.ts +50 -0
  12. package/pulse/app/api/plans/route.ts +9 -0
  13. package/pulse/app/api/projects/[slug]/route.ts +96 -0
  14. package/pulse/app/api/projects/route.ts +121 -0
  15. package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
  16. package/pulse/app/api/sessions/[id]/route.ts +31 -0
  17. package/pulse/app/api/sessions/route.ts +112 -0
  18. package/pulse/app/api/settings/route.ts +14 -0
  19. package/pulse/app/api/stats/route.ts +143 -0
  20. package/pulse/app/api/todos/route.ts +9 -0
  21. package/pulse/app/api/tools/route.ts +160 -0
  22. package/pulse/app/benchmarks/page.tsx +224 -0
  23. package/pulse/app/costs/page.tsx +179 -0
  24. package/pulse/app/export/page.tsx +465 -0
  25. package/pulse/app/favicon.ico +0 -0
  26. package/pulse/app/globals.css +263 -0
  27. package/pulse/app/help/page.tsx +143 -0
  28. package/pulse/app/history/page.tsx +157 -0
  29. package/pulse/app/layout.tsx +46 -0
  30. package/pulse/app/memory/page.tsx +365 -0
  31. package/pulse/app/overview-client.tsx +393 -0
  32. package/pulse/app/page.tsx +14 -0
  33. package/pulse/app/plans/page.tsx +308 -0
  34. package/pulse/app/projects/[slug]/page.tsx +390 -0
  35. package/pulse/app/projects/page.tsx +110 -0
  36. package/pulse/app/sessions/[id]/page.tsx +243 -0
  37. package/pulse/app/sessions/page.tsx +39 -0
  38. package/pulse/app/settings/page.tsx +188 -0
  39. package/pulse/app/todos/page.tsx +211 -0
  40. package/pulse/app/tools/page.tsx +249 -0
  41. package/pulse/cli.js +164 -0
  42. package/pulse/components/activity/day-of-week-chart.tsx +35 -0
  43. package/pulse/components/activity/streak-card.tsx +36 -0
  44. package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
  45. package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
  46. package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
  47. package/pulse/components/costs/model-token-table.tsx +60 -0
  48. package/pulse/components/global-search.tsx +193 -0
  49. package/pulse/components/keyboard-nav-provider.tsx +23 -0
  50. package/pulse/components/layout/bottom-nav.tsx +53 -0
  51. package/pulse/components/layout/client-layout.tsx +31 -0
  52. package/pulse/components/layout/sidebar-context.tsx +50 -0
  53. package/pulse/components/layout/sidebar.tsx +183 -0
  54. package/pulse/components/layout/top-bar.tsx +121 -0
  55. package/pulse/components/overview/activity-heatmap.tsx +107 -0
  56. package/pulse/components/overview/conversation-table.tsx +148 -0
  57. package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
  58. package/pulse/components/overview/peak-hours-chart.tsx +87 -0
  59. package/pulse/components/overview/project-activity-donut.tsx +96 -0
  60. package/pulse/components/overview/stat-card.tsx +102 -0
  61. package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
  62. package/pulse/components/projects/project-card.tsx +175 -0
  63. package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
  64. package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
  65. package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
  66. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
  67. package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
  68. package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
  69. package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
  70. package/pulse/components/sessions/session-badges.tsx +49 -0
  71. package/pulse/components/sessions/session-table.tsx +299 -0
  72. package/pulse/components/theme-provider.tsx +44 -0
  73. package/pulse/components/tools/feature-adoption-table.tsx +58 -0
  74. package/pulse/components/tools/mcp-server-panel.tsx +45 -0
  75. package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
  76. package/pulse/components/tools/version-history-table.tsx +32 -0
  77. package/pulse/components/ui/alert.tsx +66 -0
  78. package/pulse/components/ui/badge.tsx +48 -0
  79. package/pulse/components/ui/breadcrumb.tsx +109 -0
  80. package/pulse/components/ui/button.tsx +64 -0
  81. package/pulse/components/ui/calendar.tsx +220 -0
  82. package/pulse/components/ui/card.tsx +92 -0
  83. package/pulse/components/ui/command.tsx +158 -0
  84. package/pulse/components/ui/dialog.tsx +158 -0
  85. package/pulse/components/ui/input.tsx +21 -0
  86. package/pulse/components/ui/popover.tsx +89 -0
  87. package/pulse/components/ui/progress.tsx +31 -0
  88. package/pulse/components/ui/select.tsx +190 -0
  89. package/pulse/components/ui/separator.tsx +28 -0
  90. package/pulse/components/ui/sheet.tsx +143 -0
  91. package/pulse/components/ui/skeleton.tsx +13 -0
  92. package/pulse/components/ui/table.tsx +116 -0
  93. package/pulse/components/ui/tabs.tsx +91 -0
  94. package/pulse/components/ui/tooltip.tsx +57 -0
  95. package/pulse/components/use-global-keyboard-nav.ts +79 -0
  96. package/pulse/components.json +23 -0
  97. package/pulse/eslint.config.mjs +18 -0
  98. package/pulse/lib/bahulam-paths.ts +23 -0
  99. package/pulse/lib/claude-reader.ts +592 -0
  100. package/pulse/lib/decode.ts +129 -0
  101. package/pulse/lib/pricing.ts +102 -0
  102. package/pulse/lib/replay-parser.ts +165 -0
  103. package/pulse/lib/tool-categories.ts +127 -0
  104. package/pulse/lib/utils.ts +6 -0
  105. package/pulse/next-env.d.ts +6 -0
  106. package/pulse/next.config.ts +16 -0
  107. package/pulse/package.json +45 -0
  108. package/pulse/postcss.config.mjs +7 -0
  109. package/pulse/public/activity.png +0 -0
  110. package/pulse/public/cc-lens.png +0 -0
  111. package/pulse/public/command-k.png +0 -0
  112. package/pulse/public/costs.png +0 -0
  113. package/pulse/public/dashboard-dark.png +0 -0
  114. package/pulse/public/dashboard-white.png +0 -0
  115. package/pulse/public/export.png +0 -0
  116. package/pulse/public/file.svg +1 -0
  117. package/pulse/public/globe.svg +1 -0
  118. package/pulse/public/next.svg +1 -0
  119. package/pulse/public/projects.png +0 -0
  120. package/pulse/public/session-chat.png +0 -0
  121. package/pulse/public/todos.png +0 -0
  122. package/pulse/public/tools.png +0 -0
  123. package/pulse/public/vercel.svg +1 -0
  124. package/pulse/public/window.svg +1 -0
  125. package/pulse/tsconfig.json +34 -0
  126. package/pulse/types/claude.ts +294 -0
  127. package/src/agents/loader.mjs +94 -0
  128. package/src/agents/multi_workflow_loader.mjs +330 -0
  129. package/src/agents/parser.mjs +205 -0
  130. package/src/agents/scaffold.mjs +222 -0
  131. package/src/agents/teams.mjs +123 -0
  132. package/src/agents/workflow_loader.mjs +122 -0
  133. package/src/agents/workflow_scaffold.mjs +249 -0
  134. package/src/auth/oauth.mjs +220 -0
  135. package/src/auth/tarang-auth.mjs +306 -0
  136. package/src/commands/agent.mjs +220 -0
  137. package/src/commands/workflow.mjs +581 -0
  138. package/src/config/cli-args.mjs +200 -0
  139. package/src/config/env.mjs +263 -0
  140. package/src/config/hook-runner.mjs +100 -0
  141. package/src/config/memory-loader.mjs +32 -0
  142. package/src/config/settings-loader.mjs +45 -0
  143. package/src/config/settings.mjs +132 -0
  144. package/src/context/ast-parser.mjs +298 -0
  145. package/src/context/bm25.mjs +85 -0
  146. package/src/context/retriever.mjs +308 -0
  147. package/src/context/skeleton.mjs +134 -0
  148. package/src/context/symbol-indexer.mjs +375 -0
  149. package/src/core/agent-history.mjs +111 -0
  150. package/src/core/agent-loop.mjs +486 -0
  151. package/src/core/approval-log.mjs +104 -0
  152. package/src/core/approval.mjs +476 -0
  153. package/src/core/attachments.mjs +380 -0
  154. package/src/core/backend-url.mjs +55 -0
  155. package/src/core/cache-control.mjs +92 -0
  156. package/src/core/cache.mjs +105 -0
  157. package/src/core/callback-client.mjs +180 -0
  158. package/src/core/checkpoints.mjs +142 -0
  159. package/src/core/compact-history.mjs +127 -0
  160. package/src/core/context-envelope.mjs +54 -0
  161. package/src/core/context-manager.mjs +198 -0
  162. package/src/core/error-guidance.mjs +311 -0
  163. package/src/core/file-diff.mjs +217 -0
  164. package/src/core/headless.mjs +448 -0
  165. package/src/core/hooks-manager.mjs +87 -0
  166. package/src/core/jsonl-writer.mjs +449 -0
  167. package/src/core/local-agent.mjs +537 -0
  168. package/src/core/local-store.mjs +836 -0
  169. package/src/core/mode-selector.mjs +51 -0
  170. package/src/core/output-filter.mjs +177 -0
  171. package/src/core/paths.mjs +190 -0
  172. package/src/core/policy-resolver.mjs +156 -0
  173. package/src/core/pricing.mjs +336 -0
  174. package/src/core/project-artifacts.mjs +39 -0
  175. package/src/core/project-context-loader.mjs +139 -0
  176. package/src/core/providers.mjs +219 -0
  177. package/src/core/rate-limit-display.mjs +121 -0
  178. package/src/core/rate-limiter.mjs +119 -0
  179. package/src/core/resume-mode.mjs +192 -0
  180. package/src/core/risk-tier.mjs +337 -0
  181. package/src/core/safety.mjs +203 -0
  182. package/src/core/scheduler.mjs +173 -0
  183. package/src/core/session-manager.mjs +360 -0
  184. package/src/core/session.mjs +143 -0
  185. package/src/core/settings-sync.mjs +85 -0
  186. package/src/core/stagnation.mjs +57 -0
  187. package/src/core/stream-client.mjs +829 -0
  188. package/src/core/streaming.mjs +182 -0
  189. package/src/core/system-prompt.mjs +140 -0
  190. package/src/core/tasks.mjs +196 -0
  191. package/src/core/tool-executor.mjs +1950 -0
  192. package/src/core/trust.mjs +158 -0
  193. package/src/core/work-scope.mjs +248 -0
  194. package/src/hooks/engine.mjs +162 -0
  195. package/src/index.mjs +426 -0
  196. package/src/mcp/client.mjs +253 -0
  197. package/src/mcp/transport-shttp.mjs +130 -0
  198. package/src/mcp/transport-sse.mjs +131 -0
  199. package/src/mcp/transport-ws.mjs +134 -0
  200. package/src/onboarding/preflight.mjs +360 -0
  201. package/src/permissions/checker.mjs +57 -0
  202. package/src/permissions/command-classifier.mjs +652 -0
  203. package/src/permissions/injection-check.mjs +60 -0
  204. package/src/permissions/path-check.mjs +102 -0
  205. package/src/permissions/prompt.mjs +73 -0
  206. package/src/permissions/sandbox.mjs +112 -0
  207. package/src/plugins/loader.mjs +138 -0
  208. package/src/skills/installer.mjs +188 -0
  209. package/src/skills/loader.mjs +252 -0
  210. package/src/skills/runner.mjs +55 -0
  211. package/src/state/orbit.mjs +263 -0
  212. package/src/state/verbosity.mjs +99 -0
  213. package/src/telemetry/index.mjs +96 -0
  214. package/src/terminal/agents.mjs +177 -0
  215. package/src/terminal/analytics.mjs +292 -0
  216. package/src/terminal/ansi.mjs +695 -0
  217. package/src/terminal/init.mjs +145 -0
  218. package/src/terminal/main.mjs +269 -0
  219. package/src/terminal/repl-explore.mjs +35 -0
  220. package/src/terminal/repl-format.mjs +257 -0
  221. package/src/terminal/repl-render.mjs +561 -0
  222. package/src/terminal/repl-resume.mjs +625 -0
  223. package/src/terminal/repl-state.mjs +103 -0
  224. package/src/terminal/repl-utils.mjs +34 -0
  225. package/src/terminal/repl.mjs +3832 -0
  226. package/src/terminal/skills.mjs +54 -0
  227. package/src/terminal/tool-display.mjs +240 -0
  228. package/src/tools/agent.mjs +137 -0
  229. package/src/tools/ask-user.mjs +61 -0
  230. package/src/tools/bash.mjs +231 -0
  231. package/src/tools/cron-create.mjs +120 -0
  232. package/src/tools/cron-delete.mjs +49 -0
  233. package/src/tools/cron-list.mjs +37 -0
  234. package/src/tools/edit.mjs +82 -0
  235. package/src/tools/enter-worktree.mjs +69 -0
  236. package/src/tools/exit-worktree.mjs +57 -0
  237. package/src/tools/glob.mjs +117 -0
  238. package/src/tools/grep.mjs +129 -0
  239. package/src/tools/lint.mjs +71 -0
  240. package/src/tools/ls.mjs +58 -0
  241. package/src/tools/lsp.mjs +115 -0
  242. package/src/tools/multi-edit.mjs +94 -0
  243. package/src/tools/notebook-edit.mjs +96 -0
  244. package/src/tools/project-overview.mjs +641 -0
  245. package/src/tools/read-mcp-resource.mjs +57 -0
  246. package/src/tools/read.mjs +138 -0
  247. package/src/tools/registry.mjs +116 -0
  248. package/src/tools/remote-trigger.mjs +84 -0
  249. package/src/tools/send-message.mjs +64 -0
  250. package/src/tools/skill.mjs +52 -0
  251. package/src/tools/test-runner.mjs +49 -0
  252. package/src/tools/todo-write.mjs +68 -0
  253. package/src/tools/tool-search.mjs +77 -0
  254. package/src/tools/web-fetch.mjs +65 -0
  255. package/src/tools/web-search.mjs +89 -0
  256. package/src/tools/write.mjs +55 -0
  257. package/src/ui/approval.mjs +263 -0
  258. package/src/ui/banner.mjs +235 -0
  259. package/src/ui/commands.mjs +537 -0
  260. package/src/ui/formatter.mjs +409 -0
  261. package/src/ui/icons.mjs +164 -0
  262. package/src/ui/input-dock.mjs +444 -0
  263. package/src/ui/markdown.mjs +278 -0
  264. package/src/ui/mission-report.mjs +296 -0
  265. package/src/ui/palette.mjs +189 -0
  266. package/src/ui/slash-commands.mjs +245 -0
  267. package/src/ui/spinner.mjs +116 -0
  268. package/src/ui/sub-agent.mjs +152 -0
  269. package/src/ui/term.mjs +159 -0
  270. package/src/ui/text-layout.mjs +127 -0
  271. package/src/ui/tool-card.mjs +463 -0
  272. package/src/ui/tool-details.mjs +312 -0
  273. package/src/ui/transcript-block.mjs +21 -0
@@ -0,0 +1,592 @@
1
+ import fs from 'fs/promises'
2
+ import path from 'path'
3
+ import type {
4
+ StatsCache,
5
+ SessionMeta,
6
+ Facet,
7
+ HistoryEntry,
8
+ } from '@/types/claude'
9
+ import { slugToPath } from '@/lib/decode'
10
+ import { bahulamPath } from '@/lib/bahulam-paths'
11
+
12
+ function stripXmlTags(text: string): string {
13
+ return text.replace(/<[^>]+>[\s\S]*?<\/[^>]+>/g, '').replace(/<[^>]+\/>/g, '').replace(/<[^>]+>/g, '').trim()
14
+ }
15
+
16
+ /** Resolve the real filesystem path for a project slug by reading `cwd` from its JSONL files */
17
+ export async function resolveProjectPath(slug: string): Promise<string> {
18
+ const files = await listProjectJSONLFiles(slug)
19
+ for (const f of files) {
20
+ try {
21
+ const raw = await fs.readFile(f, 'utf-8')
22
+ const lines = raw.split(/\r?\n/)
23
+ for (const line of lines.slice(0, 50)) {
24
+ if (!line.trim()) continue
25
+ try {
26
+ const obj = JSON.parse(line)
27
+ if (obj.cwd && typeof obj.cwd === 'string') return obj.cwd
28
+ } catch { /* skip malformed line */ }
29
+ }
30
+ } catch { /* try next file */ }
31
+ }
32
+ return slugToPath(slug)
33
+ }
34
+
35
+ export function claudePath(...segments: string[]): string {
36
+ return bahulamPath(...segments)
37
+ }
38
+
39
+ // ─── Stats Cache ─────────────────────────────────────────────────────────────
40
+
41
+ export async function readStatsCache(): Promise<StatsCache | null> {
42
+ try {
43
+ const raw = await fs.readFile(claudePath('stats-cache.json'), 'utf-8')
44
+ return JSON.parse(raw) as StatsCache
45
+ } catch {
46
+ return null
47
+ }
48
+ }
49
+
50
+ // ─── Sessions from Project JSONL (primary source) ──────────────────────────────
51
+
52
+ /** Derive session metadata directly from ~/.claude/projects/<project>/<session>.jsonl */
53
+ export async function readSessionsFromProjectJSONL(): Promise<SessionMeta[]> {
54
+ const results: SessionMeta[] = []
55
+ try {
56
+ const slugs = await listProjectSlugs()
57
+ for (const slug of slugs) {
58
+ const projectPath = await resolveProjectPath(slug)
59
+ const files = await listProjectJSONLFiles(slug)
60
+ for (const filePath of files) {
61
+ const sessionId = path.basename(filePath, '.jsonl')
62
+ const meta = await deriveSessionMetaFromJSONL(filePath, sessionId, projectPath)
63
+ if (meta) results.push(meta)
64
+ }
65
+ }
66
+ return results.sort(
67
+ (a, b) => new Date(b.start_time).getTime() - new Date(a.start_time).getTime()
68
+ )
69
+ } catch {
70
+ return []
71
+ }
72
+ }
73
+
74
+ async function deriveSessionMetaFromJSONL(
75
+ filePath: string,
76
+ sessionId: string,
77
+ projectPath: string
78
+ ): Promise<SessionMeta | null> {
79
+ let startTime = ''
80
+ let lastTime = ''
81
+ let userCount = 0
82
+ let assistantCount = 0
83
+ const toolCounts: Record<string, number> = {}
84
+ let inputTokens = 0
85
+ let outputTokens = 0
86
+ let cacheRead = 0
87
+ let cacheWrite = 0
88
+ let firstPrompt = ''
89
+ let hasTaskAgent = false
90
+ let hasMcp = false
91
+ let hasWebSearch = false
92
+ let hasWebFetch = false
93
+ const messageHours: number[] = []
94
+ const userMessageTimestamps: string[] = []
95
+
96
+ try {
97
+ const raw = await fs.readFile(filePath, 'utf-8')
98
+ const lines = raw.split(/\r?\n/).filter(Boolean)
99
+ for (const line of lines) {
100
+ try {
101
+ const obj = JSON.parse(line) as Record<string, unknown>
102
+ const ts = obj.timestamp as string
103
+ if (ts) {
104
+ if (!startTime) startTime = ts
105
+ lastTime = ts
106
+ }
107
+ if (obj.type === 'user') {
108
+ userCount++
109
+ if (ts) {
110
+ const d = new Date(ts)
111
+ if (!isNaN(d.getTime())) {
112
+ messageHours.push(d.getHours())
113
+ userMessageTimestamps.push(ts)
114
+ }
115
+ }
116
+ const content = (obj as { message?: { content?: string | unknown[] } }).message?.content
117
+ if (typeof content === 'string' && !firstPrompt) firstPrompt = stripXmlTags(content).slice(0, 500)
118
+ else if (Array.isArray(content)) {
119
+ const text = content.find((c: unknown) => typeof c === 'object' && c !== null && (c as { type?: string }).type === 'text')
120
+ if (text && typeof (text as { text?: string }).text === 'string' && !firstPrompt) {
121
+ firstPrompt = stripXmlTags((text as { text: string }).text).slice(0, 500)
122
+ }
123
+ }
124
+ }
125
+ if (obj.type === 'assistant') {
126
+ assistantCount++
127
+ const msg = (obj as { message?: { usage?: Record<string, number>; content?: unknown[] } }).message
128
+ if (msg?.usage) {
129
+ inputTokens += msg.usage.input_tokens ?? 0
130
+ outputTokens += msg.usage.output_tokens ?? 0
131
+ cacheRead += msg.usage.cache_read_input_tokens ?? 0
132
+ cacheWrite += msg.usage.cache_creation_input_tokens ?? 0
133
+ }
134
+ const content = msg?.content
135
+ if (Array.isArray(content)) {
136
+ for (const c of content) {
137
+ const item = c as { type?: string; name?: string }
138
+ if (item.type === 'tool_use' && item.name) {
139
+ toolCounts[item.name] = (toolCounts[item.name] ?? 0) + 1
140
+ if (item.name.startsWith('Task') || item.name === 'TodoWrite' || item.name === 'Agent') hasTaskAgent = true
141
+ if (item.name.startsWith('mcp__')) hasMcp = true
142
+ if (item.name === 'WebSearch') hasWebSearch = true
143
+ if (item.name === 'WebFetch') hasWebFetch = true
144
+ }
145
+ }
146
+ }
147
+ }
148
+ } catch { /* skip malformed line */ }
149
+ }
150
+ } catch {
151
+ return null
152
+ }
153
+
154
+ if (!startTime) return null
155
+
156
+ const start = new Date(startTime).getTime()
157
+ const end = lastTime ? new Date(lastTime).getTime() : start
158
+ const durationMinutes = (end - start) / 60_000
159
+
160
+ return {
161
+ session_id: sessionId,
162
+ project_path: projectPath,
163
+ start_time: startTime,
164
+ duration_minutes: durationMinutes,
165
+ user_message_count: userCount,
166
+ assistant_message_count: assistantCount,
167
+ tool_counts: toolCounts,
168
+ languages: {},
169
+ git_commits: 0,
170
+ git_pushes: 0,
171
+ input_tokens: inputTokens,
172
+ output_tokens: outputTokens,
173
+ cache_creation_input_tokens: cacheWrite,
174
+ cache_read_input_tokens: cacheRead,
175
+ first_prompt: firstPrompt,
176
+ user_interruptions: 0,
177
+ user_response_times: [],
178
+ tool_errors: 0,
179
+ tool_error_categories: {},
180
+ uses_task_agent: hasTaskAgent,
181
+ uses_mcp: hasMcp,
182
+ uses_web_search: hasWebSearch,
183
+ uses_web_fetch: hasWebFetch,
184
+ lines_added: 0,
185
+ lines_removed: 0,
186
+ files_modified: 0,
187
+ message_hours: messageHours,
188
+ user_message_timestamps: userMessageTimestamps,
189
+ }
190
+ }
191
+
192
+ /** Get sessions: prefers JSONL (projects/*.jsonl), falls back to usage-data/session-meta */
193
+ export async function getSessions(): Promise<SessionMeta[]> {
194
+ const [jsonl, meta] = await Promise.all([
195
+ readSessionsFromProjectJSONL(),
196
+ readAllSessionMeta(),
197
+ ])
198
+ if (jsonl.length > 0) return jsonl
199
+ return meta
200
+ }
201
+
202
+ // ─── Session Meta (usage-data/session-meta — fallback) ────────────────────────
203
+
204
+ export async function readAllSessionMeta(): Promise<SessionMeta[]> {
205
+ const dir = claudePath('usage-data', 'session-meta')
206
+ try {
207
+ const files = await fs.readdir(dir)
208
+ const results: SessionMeta[] = []
209
+ await Promise.all(
210
+ files
211
+ .filter(f => f.endsWith('.json'))
212
+ .map(async f => {
213
+ try {
214
+ const raw = await fs.readFile(path.join(dir, f), 'utf-8')
215
+ const parsed = JSON.parse(raw) as SessionMeta
216
+ results.push(parsed)
217
+ } catch { /* skip malformed */ }
218
+ })
219
+ )
220
+ return results.sort(
221
+ (a, b) => new Date(b.start_time).getTime() - new Date(a.start_time).getTime()
222
+ )
223
+ } catch {
224
+ return []
225
+ }
226
+ }
227
+
228
+ export async function readSessionMeta(sessionId: string): Promise<SessionMeta | null> {
229
+ try {
230
+ const raw = await fs.readFile(
231
+ claudePath('usage-data', 'session-meta', `${sessionId}.json`),
232
+ 'utf-8'
233
+ )
234
+ return JSON.parse(raw) as SessionMeta
235
+ } catch {
236
+ return null
237
+ }
238
+ }
239
+
240
+ // ─── Facets ──────────────────────────────────────────────────────────────────
241
+
242
+ export async function readAllFacets(): Promise<Facet[]> {
243
+ const dir = claudePath('usage-data', 'facets')
244
+ try {
245
+ const files = await fs.readdir(dir)
246
+ const results: Facet[] = []
247
+ await Promise.all(
248
+ files
249
+ .filter(f => f.endsWith('.json'))
250
+ .map(async f => {
251
+ try {
252
+ const raw = await fs.readFile(path.join(dir, f), 'utf-8')
253
+ results.push(JSON.parse(raw) as Facet)
254
+ } catch { /* skip */ }
255
+ })
256
+ )
257
+ return results
258
+ } catch {
259
+ return []
260
+ }
261
+ }
262
+
263
+ export async function readFacet(sessionId: string): Promise<Facet | null> {
264
+ try {
265
+ const raw = await fs.readFile(
266
+ claudePath('usage-data', 'facets', `${sessionId}.json`),
267
+ 'utf-8'
268
+ )
269
+ return JSON.parse(raw) as Facet
270
+ } catch {
271
+ return null
272
+ }
273
+ }
274
+
275
+ // ─── Projects ─────────────────────────────────────────────────────────────────
276
+
277
+ export async function listProjectSlugs(): Promise<string[]> {
278
+ try {
279
+ const entries = await fs.readdir(claudePath('projects'), { withFileTypes: true })
280
+ return entries
281
+ .filter(e => e.isDirectory())
282
+ .map(e => e.name)
283
+ } catch {
284
+ return []
285
+ }
286
+ }
287
+
288
+ export async function listProjectJSONLFiles(slug: string): Promise<string[]> {
289
+ try {
290
+ const dir = claudePath('projects', slug)
291
+ const files = await fs.readdir(dir)
292
+ return files
293
+ .filter(f => f.endsWith('.jsonl'))
294
+ .map(f => path.join(dir, f))
295
+ } catch {
296
+ return []
297
+ }
298
+ }
299
+
300
+ /** Stream a JSONL file line by line, calling cb for each parsed line */
301
+ export async function readJSONLLines(
302
+ filePath: string,
303
+ cb: (line: Record<string, unknown>) => void
304
+ ): Promise<void> {
305
+ try {
306
+ const raw = await fs.readFile(filePath, 'utf-8')
307
+ for (const line of raw.split(/\r?\n/)) {
308
+ if (!line.trim()) continue
309
+ try {
310
+ cb(JSON.parse(line))
311
+ } catch { /* skip malformed */ }
312
+ }
313
+ } catch { /* file missing */ }
314
+ }
315
+
316
+ /** Find which project slug contains a given session ID */
317
+ export async function findSessionSlug(sessionId: string): Promise<string | null> {
318
+ const slugs = await listProjectSlugs()
319
+ for (const slug of slugs) {
320
+ const files = await listProjectJSONLFiles(slug)
321
+ for (const f of files) {
322
+ if (path.basename(f).startsWith(sessionId)) return slug
323
+ }
324
+ }
325
+ return null
326
+ }
327
+
328
+ /** Find the JSONL file path for a given session ID */
329
+ export async function findSessionJSONL(sessionId: string): Promise<string | null> {
330
+ const slugs = await listProjectSlugs()
331
+ for (const slug of slugs) {
332
+ const files = await listProjectJSONLFiles(slug)
333
+ for (const f of files) {
334
+ if (path.basename(f, '.jsonl') === sessionId) return f
335
+ }
336
+ }
337
+ return null
338
+ }
339
+
340
+ // ─── Plans ───────────────────────────────────────────────────────────────────
341
+
342
+ export interface PlanFile {
343
+ path: string
344
+ name: string
345
+ content: string
346
+ mtime: string
347
+ }
348
+
349
+ export async function readPlans(): Promise<PlanFile[]> {
350
+ const results: PlanFile[] = []
351
+ try {
352
+ const dir = claudePath('plans')
353
+ const files = await fs.readdir(dir)
354
+ for (const f of files.filter((x) => x.endsWith('.md'))) {
355
+ try {
356
+ const fullPath = path.join(dir, f)
357
+ const [raw, stat] = await Promise.all([
358
+ fs.readFile(fullPath, 'utf-8'),
359
+ fs.stat(fullPath),
360
+ ])
361
+ results.push({
362
+ path: fullPath,
363
+ name: f.replace(/\.md$/, ''),
364
+ content: raw,
365
+ mtime: stat.mtime.toISOString(),
366
+ })
367
+ } catch { /* skip */ }
368
+ }
369
+ return results.sort((a, b) => b.mtime.localeCompare(a.mtime))
370
+ } catch {
371
+ return []
372
+ }
373
+ }
374
+
375
+ // ─── Todos ───────────────────────────────────────────────────────────────────
376
+
377
+ export interface TodoFile {
378
+ path: string
379
+ name: string
380
+ data: unknown
381
+ mtime: string
382
+ }
383
+
384
+ export async function readTodos(): Promise<TodoFile[]> {
385
+ const results: TodoFile[] = []
386
+ try {
387
+ const dir = claudePath('todos')
388
+ const files = await fs.readdir(dir)
389
+ for (const f of files.filter((x) => x.endsWith('.json'))) {
390
+ try {
391
+ const fullPath = path.join(dir, f)
392
+ const [raw, stat] = await Promise.all([
393
+ fs.readFile(fullPath, 'utf-8'),
394
+ fs.stat(fullPath),
395
+ ])
396
+ results.push({
397
+ path: fullPath,
398
+ name: f.replace(/\.json$/, ''),
399
+ data: JSON.parse(raw),
400
+ mtime: stat.mtime.toISOString(),
401
+ })
402
+ } catch { /* skip */ }
403
+ }
404
+ return results.sort((a, b) => b.mtime.localeCompare(a.mtime))
405
+ } catch {
406
+ return []
407
+ }
408
+ }
409
+
410
+ // ─── History ─────────────────────────────────────────────────────────────────
411
+
412
+ export async function readHistory(limit = 200): Promise<HistoryEntry[]> {
413
+ const entries: HistoryEntry[] = []
414
+ try {
415
+ const raw = await fs.readFile(claudePath('history.jsonl'), 'utf-8')
416
+ const lines = raw.split(/\r?\n/).filter(Boolean)
417
+ for (const line of lines.slice(-limit)) {
418
+ try {
419
+ entries.push(JSON.parse(line) as HistoryEntry)
420
+ } catch { /* skip */ }
421
+ }
422
+ } catch { /* file missing */ }
423
+ return entries
424
+ }
425
+
426
+ // ─── Skills ───────────────────────────────────────────────────────────────────
427
+
428
+ export interface SkillInfo {
429
+ name: string
430
+ description: string
431
+ triggers: string
432
+ hasSkillMd: boolean
433
+ }
434
+
435
+ export async function readSkills(): Promise<SkillInfo[]> {
436
+ const skillsDir = claudePath('skills')
437
+ try {
438
+ const entries = await fs.readdir(skillsDir, { withFileTypes: true })
439
+ const dirs = entries.filter(e => e.isDirectory() && !e.name.startsWith('.') && e.name !== 'nebius-skills-workspace')
440
+ const results: SkillInfo[] = []
441
+ for (const dir of dirs) {
442
+ const skillMdPath = path.join(skillsDir, dir.name, 'SKILL.md')
443
+ let description = ''
444
+ let triggers = ''
445
+ let hasSkillMd = false
446
+ try {
447
+ const raw = await fs.readFile(skillMdPath, 'utf-8')
448
+ hasSkillMd = true
449
+ const descMatch = raw.match(/^#\s+(.+)$/m)
450
+ if (descMatch) description = descMatch[1].trim()
451
+ const triggerMatch = raw.match(/(?:TRIGGER|trigger)[^\n]*\n([\s\S]*?)(?:\n#{1,3}\s|\n---|\n\*\*DO NOT|$)/m)
452
+ if (triggerMatch) triggers = triggerMatch[1].replace(/\s+/g, ' ').trim().slice(0, 200)
453
+ } catch { /* no SKILL.md */ }
454
+ results.push({ name: dir.name, description, triggers, hasSkillMd })
455
+ }
456
+ return results.sort((a, b) => a.name.localeCompare(b.name))
457
+ } catch {
458
+ return []
459
+ }
460
+ }
461
+
462
+ // ─── Plugins ──────────────────────────────────────────────────────────────────
463
+
464
+ export interface PluginInfo {
465
+ id: string
466
+ scope: string
467
+ version: string
468
+ installedAt: string
469
+ }
470
+
471
+ export async function readInstalledPlugins(): Promise<PluginInfo[]> {
472
+ try {
473
+ const raw = await fs.readFile(claudePath('plugins', 'installed_plugins.json'), 'utf-8')
474
+ const json = JSON.parse(raw) as { plugins: Record<string, Array<{ scope: string; version: string; installedAt: string }>> }
475
+ return Object.entries(json.plugins).flatMap(([id, installs]) =>
476
+ installs.map(inst => ({ id, scope: inst.scope, version: inst.version, installedAt: inst.installedAt }))
477
+ )
478
+ } catch {
479
+ return []
480
+ }
481
+ }
482
+
483
+ // ─── Settings ─────────────────────────────────────────────────────────────────
484
+
485
+ export async function readSettings(): Promise<Record<string, unknown>> {
486
+ try {
487
+ const raw = await fs.readFile(claudePath('settings.json'), 'utf-8')
488
+ return JSON.parse(raw)
489
+ } catch {
490
+ return {}
491
+ }
492
+ }
493
+
494
+ // ─── Memory ───────────────────────────────────────────────────────────────────
495
+
496
+ export type MemoryType = 'user' | 'feedback' | 'project' | 'reference' | 'index' | 'unknown'
497
+
498
+ export interface MemoryEntry {
499
+ file: string
500
+ projectSlug: string
501
+ projectPath: string
502
+ name: string
503
+ type: MemoryType
504
+ description: string
505
+ body: string
506
+ mtime: string
507
+ isIndex: boolean
508
+ }
509
+
510
+ function parseFrontmatter(raw: string): { meta: Record<string, string>; body: string } {
511
+ const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/)
512
+ if (!match) return { meta: {}, body: raw }
513
+ const meta: Record<string, string> = {}
514
+ for (const line of match[1].split('\n')) {
515
+ const colon = line.indexOf(':')
516
+ if (colon === -1) continue
517
+ const key = line.slice(0, colon).trim()
518
+ const val = line.slice(colon + 1).trim()
519
+ if (key) meta[key] = val
520
+ }
521
+ return { meta, body: match[2].trim() }
522
+ }
523
+
524
+ export async function readMemories(): Promise<MemoryEntry[]> {
525
+ const results: MemoryEntry[] = []
526
+ try {
527
+ const slugs = await listProjectSlugs()
528
+ await Promise.all(
529
+ slugs.map(async slug => {
530
+ const memDir = claudePath('projects', slug, 'memory')
531
+ try {
532
+ const files = await fs.readdir(memDir)
533
+ const mdFiles = files.filter(f => f.endsWith('.md'))
534
+ await Promise.all(
535
+ mdFiles.map(async file => {
536
+ try {
537
+ const fullPath = path.join(memDir, file)
538
+ const [raw, stat] = await Promise.all([
539
+ fs.readFile(fullPath, 'utf-8'),
540
+ fs.stat(fullPath),
541
+ ])
542
+ const isIndex = file === 'MEMORY.md'
543
+ const { meta, body } = parseFrontmatter(raw)
544
+ const projectPath = slugToPath(slug)
545
+ const h1Match = body.match(/^#\s+(.+)$/m)
546
+ const titleFromBody = h1Match ? h1Match[1].trim() : null
547
+ results.push({
548
+ file,
549
+ projectSlug: slug,
550
+ projectPath,
551
+ name: meta.name ?? titleFromBody ?? (isIndex ? 'Memory Index' : file.replace(/\.md$/, '')),
552
+ type: (meta.type as MemoryType) ?? (isIndex ? 'index' : 'unknown'),
553
+ description: meta.description ?? '',
554
+ body,
555
+ mtime: stat.mtime.toISOString(),
556
+ isIndex,
557
+ })
558
+ } catch { /* skip */ }
559
+ })
560
+ )
561
+ } catch { /* no memory dir */ }
562
+ })
563
+ )
564
+ } catch { /* skip */ }
565
+ return results.sort((a, b) => b.mtime.localeCompare(a.mtime))
566
+ }
567
+
568
+ // ─── Storage size ─────────────────────────────────────────────────────────────
569
+
570
+ export async function getClaudeStorageBytes(): Promise<number> {
571
+ async function dirSize(dirPath: string): Promise<number> {
572
+ let total = 0
573
+ try {
574
+ const entries = await fs.readdir(dirPath, { withFileTypes: true })
575
+ await Promise.all(
576
+ entries.map(async e => {
577
+ const full = path.join(dirPath, e.name)
578
+ if (e.isDirectory()) {
579
+ total += await dirSize(full)
580
+ } else {
581
+ try {
582
+ const stat = await fs.stat(full)
583
+ total += stat.size
584
+ } catch { /* skip */ }
585
+ }
586
+ })
587
+ )
588
+ } catch { /* skip inaccessible dirs */ }
589
+ return total
590
+ }
591
+ return dirSize(CLAUDE_DIR)
592
+ }