@axplusb/kepler 0.0.1 → 1.0.0

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