@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.
- package/README.md +82 -0
- package/package.json +36 -4
- package/pulse/app/activity/page.tsx +190 -0
- package/pulse/app/api/activity/route.ts +138 -0
- package/pulse/app/api/costs/route.ts +88 -0
- package/pulse/app/api/export/route.ts +77 -0
- package/pulse/app/api/history/route.ts +11 -0
- package/pulse/app/api/import/route.ts +31 -0
- package/pulse/app/api/memory/route.ts +52 -0
- package/pulse/app/api/plans/route.ts +9 -0
- package/pulse/app/api/projects/[slug]/route.ts +96 -0
- package/pulse/app/api/projects/route.ts +121 -0
- package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
- package/pulse/app/api/sessions/[id]/route.ts +31 -0
- package/pulse/app/api/sessions/route.ts +112 -0
- package/pulse/app/api/settings/route.ts +14 -0
- package/pulse/app/api/stats/route.ts +143 -0
- package/pulse/app/api/todos/route.ts +9 -0
- package/pulse/app/api/tools/route.ts +160 -0
- package/pulse/app/costs/page.tsx +179 -0
- package/pulse/app/export/page.tsx +465 -0
- package/pulse/app/favicon.ico +0 -0
- package/pulse/app/globals.css +263 -0
- package/pulse/app/help/page.tsx +142 -0
- package/pulse/app/history/page.tsx +157 -0
- package/pulse/app/layout.tsx +46 -0
- package/pulse/app/memory/page.tsx +365 -0
- package/pulse/app/overview-client.tsx +393 -0
- package/pulse/app/page.tsx +14 -0
- package/pulse/app/plans/page.tsx +308 -0
- package/pulse/app/projects/[slug]/page.tsx +390 -0
- package/pulse/app/projects/page.tsx +110 -0
- package/pulse/app/sessions/[id]/page.tsx +243 -0
- package/pulse/app/sessions/page.tsx +39 -0
- package/pulse/app/settings/page.tsx +188 -0
- package/pulse/app/todos/page.tsx +211 -0
- package/pulse/app/tools/page.tsx +249 -0
- package/pulse/cli.js +159 -0
- package/pulse/components/activity/day-of-week-chart.tsx +35 -0
- package/pulse/components/activity/streak-card.tsx +36 -0
- package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
- package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
- package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
- package/pulse/components/costs/model-token-table.tsx +60 -0
- package/pulse/components/global-search.tsx +193 -0
- package/pulse/components/keyboard-nav-provider.tsx +23 -0
- package/pulse/components/layout/bottom-nav.tsx +52 -0
- package/pulse/components/layout/client-layout.tsx +31 -0
- package/pulse/components/layout/sidebar-context.tsx +50 -0
- package/pulse/components/layout/sidebar.tsx +182 -0
- package/pulse/components/layout/top-bar.tsx +121 -0
- package/pulse/components/overview/activity-heatmap.tsx +107 -0
- package/pulse/components/overview/conversation-table.tsx +148 -0
- package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
- package/pulse/components/overview/peak-hours-chart.tsx +87 -0
- package/pulse/components/overview/project-activity-donut.tsx +96 -0
- package/pulse/components/overview/stat-card.tsx +102 -0
- package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
- package/pulse/components/projects/project-card.tsx +175 -0
- package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
- package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
- package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
- package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
- package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
- package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
- package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
- package/pulse/components/sessions/session-badges.tsx +49 -0
- package/pulse/components/sessions/session-table.tsx +299 -0
- package/pulse/components/theme-provider.tsx +44 -0
- package/pulse/components/tools/feature-adoption-table.tsx +58 -0
- package/pulse/components/tools/mcp-server-panel.tsx +45 -0
- package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
- package/pulse/components/tools/version-history-table.tsx +32 -0
- package/pulse/components/ui/alert.tsx +66 -0
- package/pulse/components/ui/badge.tsx +48 -0
- package/pulse/components/ui/breadcrumb.tsx +109 -0
- package/pulse/components/ui/button.tsx +64 -0
- package/pulse/components/ui/calendar.tsx +220 -0
- package/pulse/components/ui/card.tsx +92 -0
- package/pulse/components/ui/command.tsx +158 -0
- package/pulse/components/ui/dialog.tsx +158 -0
- package/pulse/components/ui/input.tsx +21 -0
- package/pulse/components/ui/popover.tsx +89 -0
- package/pulse/components/ui/progress.tsx +31 -0
- package/pulse/components/ui/select.tsx +190 -0
- package/pulse/components/ui/separator.tsx +28 -0
- package/pulse/components/ui/sheet.tsx +143 -0
- package/pulse/components/ui/skeleton.tsx +13 -0
- package/pulse/components/ui/table.tsx +116 -0
- package/pulse/components/ui/tabs.tsx +91 -0
- package/pulse/components/ui/tooltip.tsx +57 -0
- package/pulse/components/use-global-keyboard-nav.ts +79 -0
- package/pulse/components.json +23 -0
- package/pulse/eslint.config.mjs +18 -0
- package/pulse/lib/claude-reader.ts +594 -0
- package/pulse/lib/decode.ts +129 -0
- package/pulse/lib/pricing.ts +102 -0
- package/pulse/lib/replay-parser.ts +165 -0
- package/pulse/lib/tool-categories.ts +127 -0
- package/pulse/lib/utils.ts +6 -0
- package/pulse/next-env.d.ts +6 -0
- package/pulse/next.config.ts +16 -0
- package/pulse/package.json +45 -0
- package/pulse/postcss.config.mjs +7 -0
- package/pulse/public/activity.png +0 -0
- package/pulse/public/cc-lens.png +0 -0
- package/pulse/public/command-k.png +0 -0
- package/pulse/public/costs.png +0 -0
- package/pulse/public/dashboard-dark.png +0 -0
- package/pulse/public/dashboard-white.png +0 -0
- package/pulse/public/export.png +0 -0
- package/pulse/public/file.svg +1 -0
- package/pulse/public/globe.svg +1 -0
- package/pulse/public/next.svg +1 -0
- package/pulse/public/projects.png +0 -0
- package/pulse/public/session-chat.png +0 -0
- package/pulse/public/todos.png +0 -0
- package/pulse/public/tools.png +0 -0
- package/pulse/public/vercel.svg +1 -0
- package/pulse/public/window.svg +1 -0
- package/pulse/tsconfig.json +34 -0
- package/pulse/types/claude.ts +294 -0
- package/src/agents/loader.mjs +89 -0
- package/src/agents/parser.mjs +98 -0
- package/src/agents/teams.mjs +123 -0
- package/src/auth/oauth.mjs +220 -0
- package/src/auth/tarang-auth.mjs +277 -0
- package/src/config/cli-args.mjs +173 -0
- package/src/config/env.mjs +263 -0
- package/src/config/settings.mjs +132 -0
- package/src/context/ast-parser.mjs +298 -0
- package/src/context/bm25.mjs +85 -0
- package/src/context/retriever.mjs +270 -0
- package/src/context/skeleton.mjs +134 -0
- package/src/core/agent-loop.mjs +480 -0
- package/src/core/approval.mjs +273 -0
- package/src/core/backend-url.mjs +57 -0
- package/src/core/cache.mjs +105 -0
- package/src/core/callback-client.mjs +149 -0
- package/src/core/checkpoints.mjs +142 -0
- package/src/core/context-manager.mjs +198 -0
- package/src/core/headless.mjs +168 -0
- package/src/core/hooks-manager.mjs +87 -0
- package/src/core/jsonl-writer.mjs +351 -0
- package/src/core/local-agent.mjs +429 -0
- package/src/core/local-store.mjs +325 -0
- package/src/core/mode-selector.mjs +51 -0
- package/src/core/output-filter.mjs +177 -0
- package/src/core/paths.mjs +101 -0
- package/src/core/pricing.mjs +314 -0
- package/src/core/providers.mjs +219 -0
- package/src/core/rate-limiter.mjs +119 -0
- package/src/core/safety.mjs +200 -0
- package/src/core/scheduler.mjs +173 -0
- package/src/core/session-manager.mjs +317 -0
- package/src/core/session.mjs +143 -0
- package/src/core/settings-sync.mjs +85 -0
- package/src/core/stagnation.mjs +57 -0
- package/src/core/stream-client.mjs +367 -0
- package/src/core/streaming.mjs +182 -0
- package/src/core/system-prompt.mjs +135 -0
- package/src/core/tool-executor.mjs +725 -0
- package/src/hooks/engine.mjs +162 -0
- package/src/index.mjs +370 -0
- package/src/mcp/client.mjs +253 -0
- package/src/mcp/transport-shttp.mjs +130 -0
- package/src/mcp/transport-sse.mjs +131 -0
- package/src/mcp/transport-ws.mjs +134 -0
- package/src/permissions/checker.mjs +57 -0
- package/src/permissions/command-classifier.mjs +573 -0
- package/src/permissions/injection-check.mjs +60 -0
- package/src/permissions/path-check.mjs +102 -0
- package/src/permissions/prompt.mjs +73 -0
- package/src/permissions/sandbox.mjs +112 -0
- package/src/plugins/loader.mjs +138 -0
- package/src/skills/loader.mjs +147 -0
- package/src/skills/runner.mjs +55 -0
- package/src/telemetry/index.mjs +96 -0
- package/src/terminal/agents.mjs +177 -0
- package/src/terminal/analytics.mjs +292 -0
- package/src/terminal/ansi.mjs +421 -0
- package/src/terminal/main.mjs +150 -0
- package/src/terminal/repl.mjs +1484 -0
- package/src/terminal/tool-display.mjs +58 -0
- package/src/tools/agent.mjs +137 -0
- package/src/tools/ask-user.mjs +61 -0
- package/src/tools/bash.mjs +148 -0
- package/src/tools/cron-create.mjs +120 -0
- package/src/tools/cron-delete.mjs +49 -0
- package/src/tools/cron-list.mjs +37 -0
- package/src/tools/edit.mjs +82 -0
- package/src/tools/enter-worktree.mjs +69 -0
- package/src/tools/exit-worktree.mjs +57 -0
- package/src/tools/glob.mjs +117 -0
- package/src/tools/grep.mjs +129 -0
- package/src/tools/lint.mjs +71 -0
- package/src/tools/ls.mjs +58 -0
- package/src/tools/lsp.mjs +115 -0
- package/src/tools/multi-edit.mjs +94 -0
- package/src/tools/notebook-edit.mjs +96 -0
- package/src/tools/read-mcp-resource.mjs +57 -0
- package/src/tools/read.mjs +138 -0
- package/src/tools/registry.mjs +132 -0
- package/src/tools/remote-trigger.mjs +84 -0
- package/src/tools/send-message.mjs +64 -0
- package/src/tools/skill.mjs +52 -0
- package/src/tools/test-runner.mjs +49 -0
- package/src/tools/todo-write.mjs +68 -0
- package/src/tools/tool-search.mjs +77 -0
- package/src/tools/web-fetch.mjs +65 -0
- package/src/tools/web-search.mjs +89 -0
- package/src/tools/write.mjs +55 -0
- package/src/ui/banner.mjs +237 -0
- package/src/ui/commands.mjs +499 -0
- package/src/ui/formatter.mjs +379 -0
- package/src/ui/markdown.mjs +278 -0
- package/src/ui/slash-commands.mjs +258 -0
- 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
|
+
}
|