@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.
- 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 +98 -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,52 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import fs from 'fs/promises'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import os from 'os'
|
|
5
|
+
import { readMemories } from '@/lib/claude-reader'
|
|
6
|
+
|
|
7
|
+
export const dynamic = 'force-dynamic'
|
|
8
|
+
|
|
9
|
+
const CLAUDE_DIR = path.join(os.homedir(), '.orca')
|
|
10
|
+
|
|
11
|
+
export async function GET() {
|
|
12
|
+
const memories = await readMemories()
|
|
13
|
+
return NextResponse.json({ memories })
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function PATCH(req: Request) {
|
|
17
|
+
try {
|
|
18
|
+
const { projectSlug, file, content } = await req.json() as {
|
|
19
|
+
projectSlug?: string
|
|
20
|
+
file?: string
|
|
21
|
+
content?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!projectSlug || !file || typeof content !== 'string') {
|
|
25
|
+
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 })
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Only allow .md files
|
|
29
|
+
if (!file.endsWith('.md')) {
|
|
30
|
+
return NextResponse.json({ error: 'Only .md files allowed' }, { status: 400 })
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Prevent path traversal — slug and file must be plain names (no slashes or dots)
|
|
34
|
+
if (/[/\\]/.test(projectSlug) || /[/\\]/.test(file)) {
|
|
35
|
+
return NextResponse.json({ error: 'Invalid path' }, { status: 400 })
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const filePath = path.join(CLAUDE_DIR, 'projects', projectSlug, 'memory', file)
|
|
39
|
+
|
|
40
|
+
// Ensure the resolved path stays within ~/.orca/projects/
|
|
41
|
+
const allowedRoot = path.join(CLAUDE_DIR, 'projects')
|
|
42
|
+
if (!filePath.startsWith(allowedRoot + path.sep)) {
|
|
43
|
+
return NextResponse.json({ error: 'Path outside allowed directory' }, { status: 403 })
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
|
47
|
+
await fs.writeFile(filePath, content, 'utf-8')
|
|
48
|
+
return NextResponse.json({ ok: true })
|
|
49
|
+
} catch (err) {
|
|
50
|
+
return NextResponse.json({ error: String(err) }, { status: 500 })
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import { NextResponse } from 'next/server'
|
|
3
|
+
import { getSessions, listProjectJSONLFiles, readJSONLLines, resolveProjectPath } from '@/lib/claude-reader'
|
|
4
|
+
import { estimateCostFromUsage } from '@/lib/pricing'
|
|
5
|
+
import { projectDisplayName } from '@/lib/decode'
|
|
6
|
+
import type { SessionWithFacet } from '@/types/claude'
|
|
7
|
+
|
|
8
|
+
export const dynamic = 'force-dynamic'
|
|
9
|
+
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
|
+
type AnyLine = Record<string, any>
|
|
12
|
+
|
|
13
|
+
export async function GET(
|
|
14
|
+
_req: Request,
|
|
15
|
+
{ params }: { params: Promise<{ slug: string }> }
|
|
16
|
+
) {
|
|
17
|
+
const { slug } = await params
|
|
18
|
+
const projectPath = await resolveProjectPath(slug)
|
|
19
|
+
const allSessions = await getSessions()
|
|
20
|
+
let sessions = allSessions.filter(s => s.project_path === projectPath)
|
|
21
|
+
|
|
22
|
+
if (sessions.length === 0) {
|
|
23
|
+
const lastSegment = projectPath.split('/').filter(Boolean).pop() ?? ''
|
|
24
|
+
sessions = allSessions.filter(s =>
|
|
25
|
+
s.project_path?.endsWith('/' + lastSegment)
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Gather per-session branch data from JSONL
|
|
30
|
+
const files = await listProjectJSONLFiles(slug)
|
|
31
|
+
const branchTurns = new Map<string, number>()
|
|
32
|
+
const sessionMeta = new Map<string, { slug?: string; version?: string; has_compaction?: boolean }>()
|
|
33
|
+
|
|
34
|
+
await Promise.all(
|
|
35
|
+
files.map(async (f) => {
|
|
36
|
+
const sessionId = path.basename(f, '.jsonl')
|
|
37
|
+
const meta: { slug?: string; version?: string; has_compaction?: boolean } = {}
|
|
38
|
+
|
|
39
|
+
await readJSONLLines(f, (line: AnyLine) => {
|
|
40
|
+
if (!meta.slug && line.slug) meta.slug = line.slug
|
|
41
|
+
if (!meta.version && line.version) meta.version = line.version
|
|
42
|
+
if (line.type === 'system' && line.subtype === 'compact_boundary') meta.has_compaction = true
|
|
43
|
+
if (line.gitBranch && line.gitBranch !== 'HEAD') {
|
|
44
|
+
branchTurns.set(line.gitBranch, (branchTurns.get(line.gitBranch) ?? 0) + 1)
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
sessionMeta.set(sessionId, meta)
|
|
49
|
+
})
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
const enrichedSessions: SessionWithFacet[] = sessions.map(s => {
|
|
53
|
+
const enrich = sessionMeta.get(s.session_id) ?? {}
|
|
54
|
+
return {
|
|
55
|
+
...s,
|
|
56
|
+
estimated_cost: estimateCostFromUsage('claude-opus-4-6', {
|
|
57
|
+
input_tokens: s.input_tokens ?? 0,
|
|
58
|
+
output_tokens: s.output_tokens ?? 0,
|
|
59
|
+
cache_creation_input_tokens: s.cache_creation_input_tokens ?? 0,
|
|
60
|
+
cache_read_input_tokens: s.cache_read_input_tokens ?? 0,
|
|
61
|
+
}),
|
|
62
|
+
slug: enrich.slug,
|
|
63
|
+
version: enrich.version,
|
|
64
|
+
has_compaction: enrich.has_compaction,
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
// Aggregate tools
|
|
69
|
+
const toolCounts: Record<string, number> = {}
|
|
70
|
+
for (const s of sessions) {
|
|
71
|
+
for (const [t, c] of Object.entries(s.tool_counts ?? {})) {
|
|
72
|
+
toolCounts[t] = (toolCounts[t] ?? 0) + c
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Cost per session (for chart)
|
|
77
|
+
const costBySession = enrichedSessions.map(s => ({
|
|
78
|
+
session_id: s.session_id,
|
|
79
|
+
start_time: s.start_time,
|
|
80
|
+
cost: s.estimated_cost,
|
|
81
|
+
messages: (s.user_message_count ?? 0) + (s.assistant_message_count ?? 0),
|
|
82
|
+
}))
|
|
83
|
+
|
|
84
|
+
const branches = [...branchTurns.entries()]
|
|
85
|
+
.map(([branch, turns]) => ({ branch, turns }))
|
|
86
|
+
.sort((a, b) => b.turns - a.turns)
|
|
87
|
+
|
|
88
|
+
return NextResponse.json({
|
|
89
|
+
project_path: projectPath,
|
|
90
|
+
display_name: projectDisplayName(projectPath),
|
|
91
|
+
sessions: enrichedSessions.sort((a, b) => b.start_time.localeCompare(a.start_time)),
|
|
92
|
+
tool_counts: toolCounts,
|
|
93
|
+
cost_by_session: costBySession,
|
|
94
|
+
branches,
|
|
95
|
+
})
|
|
96
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { getSessions, listProjectSlugs, listProjectJSONLFiles, readJSONLLines, resolveProjectPath } from '@/lib/claude-reader'
|
|
3
|
+
import { estimateCostFromUsage } from '@/lib/pricing'
|
|
4
|
+
import { projectDisplayName } from '@/lib/decode'
|
|
5
|
+
import type { ProjectSummary } from '@/types/claude'
|
|
6
|
+
|
|
7
|
+
export const dynamic = 'force-dynamic'
|
|
8
|
+
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
|
+
type AnyLine = Record<string, any>
|
|
11
|
+
|
|
12
|
+
export async function GET() {
|
|
13
|
+
const [sessions, slugDirs] = await Promise.all([getSessions(), listProjectSlugs()])
|
|
14
|
+
|
|
15
|
+
// Build path→slug lookup from actual project directories
|
|
16
|
+
const pathToSlugMap = new Map<string, string>()
|
|
17
|
+
await Promise.all(
|
|
18
|
+
slugDirs.map(async (slug) => {
|
|
19
|
+
const resolved = await resolveProjectPath(slug)
|
|
20
|
+
pathToSlugMap.set(resolved, slug)
|
|
21
|
+
})
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
// Group sessions by project_path
|
|
25
|
+
const byPath = new Map<string, typeof sessions>()
|
|
26
|
+
for (const s of sessions) {
|
|
27
|
+
const pp = s.project_path ?? ''
|
|
28
|
+
if (!byPath.has(pp)) byPath.set(pp, [])
|
|
29
|
+
byPath.get(pp)!.push(s)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Gather branches per slug from JSONL
|
|
33
|
+
const slugBranches = new Map<string, Set<string>>()
|
|
34
|
+
await Promise.all(
|
|
35
|
+
slugDirs.map(async (slug) => {
|
|
36
|
+
const files = await listProjectJSONLFiles(slug)
|
|
37
|
+
const branches = new Set<string>()
|
|
38
|
+
await Promise.all(
|
|
39
|
+
files.map(async (f) => {
|
|
40
|
+
await readJSONLLines(f, (line: AnyLine) => {
|
|
41
|
+
if (line.gitBranch && line.gitBranch !== 'HEAD') {
|
|
42
|
+
branches.add(line.gitBranch)
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
)
|
|
47
|
+
slugBranches.set(slug, branches)
|
|
48
|
+
})
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
const projects: ProjectSummary[] = []
|
|
52
|
+
|
|
53
|
+
for (const [projectPath, sessionList] of byPath.entries()) {
|
|
54
|
+
const slug = pathToSlugMap.get(projectPath) ?? projectPath.replace(/\//g, '-')
|
|
55
|
+
|
|
56
|
+
const totalMessages = sessionList.reduce(
|
|
57
|
+
(s, m) => s + (m.user_message_count ?? 0) + (m.assistant_message_count ?? 0), 0
|
|
58
|
+
)
|
|
59
|
+
const totalDuration = sessionList.reduce((s, m) => s + (m.duration_minutes ?? 0), 0)
|
|
60
|
+
const totalLinesAdded = sessionList.reduce((s, m) => s + (m.lines_added ?? 0), 0)
|
|
61
|
+
const totalLinesRemoved = sessionList.reduce((s, m) => s + (m.lines_removed ?? 0), 0)
|
|
62
|
+
const totalFilesModified = sessionList.reduce((s, m) => s + (m.files_modified ?? 0), 0)
|
|
63
|
+
const gitCommits = sessionList.reduce((s, m) => s + (m.git_commits ?? 0), 0)
|
|
64
|
+
const gitPushes = sessionList.reduce((s, m) => s + (m.git_pushes ?? 0), 0)
|
|
65
|
+
const inputTokens = sessionList.reduce((s, m) => s + (m.input_tokens ?? 0), 0)
|
|
66
|
+
const outputTokens = sessionList.reduce((s, m) => s + (m.output_tokens ?? 0), 0)
|
|
67
|
+
|
|
68
|
+
const estimatedCost = sessionList.reduce((sum, s) => {
|
|
69
|
+
return sum + estimateCostFromUsage('claude-opus-4-6', {
|
|
70
|
+
input_tokens: s.input_tokens ?? 0,
|
|
71
|
+
output_tokens: s.output_tokens ?? 0,
|
|
72
|
+
cache_creation_input_tokens: s.cache_creation_input_tokens ?? 0,
|
|
73
|
+
cache_read_input_tokens: s.cache_read_input_tokens ?? 0,
|
|
74
|
+
})
|
|
75
|
+
}, 0)
|
|
76
|
+
|
|
77
|
+
const languages: Record<string, number> = {}
|
|
78
|
+
for (const s of sessionList) {
|
|
79
|
+
for (const [lang, count] of Object.entries(s.languages ?? {})) {
|
|
80
|
+
languages[lang] = (languages[lang] ?? 0) + count
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const toolCounts: Record<string, number> = {}
|
|
85
|
+
for (const s of sessionList) {
|
|
86
|
+
for (const [tool, count] of Object.entries(s.tool_counts ?? {})) {
|
|
87
|
+
toolCounts[tool] = (toolCounts[tool] ?? 0) + count
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const sortedDates = sessionList.map(s => s.start_time).sort()
|
|
92
|
+
|
|
93
|
+
projects.push({
|
|
94
|
+
slug,
|
|
95
|
+
project_path: projectPath,
|
|
96
|
+
display_name: projectDisplayName(projectPath),
|
|
97
|
+
session_count: sessionList.length,
|
|
98
|
+
total_messages: totalMessages,
|
|
99
|
+
total_duration_minutes: totalDuration,
|
|
100
|
+
total_lines_added: totalLinesAdded,
|
|
101
|
+
total_lines_removed: totalLinesRemoved,
|
|
102
|
+
total_files_modified: totalFilesModified,
|
|
103
|
+
git_commits: gitCommits,
|
|
104
|
+
git_pushes: gitPushes,
|
|
105
|
+
estimated_cost: estimatedCost,
|
|
106
|
+
input_tokens: inputTokens,
|
|
107
|
+
output_tokens: outputTokens,
|
|
108
|
+
languages,
|
|
109
|
+
tool_counts: toolCounts,
|
|
110
|
+
last_active: sortedDates[sortedDates.length - 1] ?? '',
|
|
111
|
+
first_active: sortedDates[0] ?? '',
|
|
112
|
+
uses_mcp: sessionList.some(s => s.uses_mcp),
|
|
113
|
+
uses_task_agent: sessionList.some(s => s.uses_task_agent),
|
|
114
|
+
branches: [...(slugBranches.get(slug) ?? new Set())].slice(0, 10),
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return NextResponse.json({
|
|
119
|
+
projects: projects.sort((a, b) => b.last_active.localeCompare(a.last_active)),
|
|
120
|
+
})
|
|
121
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { findSessionJSONL } from '@/lib/claude-reader'
|
|
3
|
+
import { parseSessionReplay } from '@/lib/replay-parser'
|
|
4
|
+
|
|
5
|
+
export const dynamic = 'force-dynamic'
|
|
6
|
+
|
|
7
|
+
export async function GET(
|
|
8
|
+
_req: Request,
|
|
9
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
10
|
+
) {
|
|
11
|
+
const { id } = await params
|
|
12
|
+
const jsonlPath = await findSessionJSONL(id)
|
|
13
|
+
|
|
14
|
+
if (!jsonlPath) {
|
|
15
|
+
return NextResponse.json({ error: 'Session JSONL not found' }, { status: 404 })
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const replay = await parseSessionReplay(jsonlPath, id)
|
|
19
|
+
return NextResponse.json(replay)
|
|
20
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { readSessionMeta, readFacet, getSessions } from '@/lib/claude-reader'
|
|
3
|
+
import { estimateCostFromUsage } from '@/lib/pricing'
|
|
4
|
+
|
|
5
|
+
export const dynamic = 'force-dynamic'
|
|
6
|
+
|
|
7
|
+
export async function GET(
|
|
8
|
+
_req: Request,
|
|
9
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
10
|
+
) {
|
|
11
|
+
const { id } = await params
|
|
12
|
+
const [meta, facet] = await Promise.all([readSessionMeta(id), readFacet(id)])
|
|
13
|
+
|
|
14
|
+
// readSessionMeta only finds session-meta/*.json files (legacy path).
|
|
15
|
+
// Fall back to JSONL-derived sessions for machines without that directory.
|
|
16
|
+
const resolved = meta ?? (await getSessions()).find(s => s.session_id === id) ?? null
|
|
17
|
+
|
|
18
|
+
if (!resolved) {
|
|
19
|
+
return NextResponse.json({ error: 'Session not found' }, { status: 404 })
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const estimated_cost = estimateCostFromUsage('claude-opus-4-6', {
|
|
23
|
+
input_tokens: resolved.input_tokens ?? 0,
|
|
24
|
+
output_tokens: resolved.output_tokens ?? 0,
|
|
25
|
+
cache_creation_input_tokens: resolved.cache_creation_input_tokens ?? 0,
|
|
26
|
+
cache_read_input_tokens: resolved.cache_read_input_tokens ?? 0,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
return NextResponse.json({ session: { ...resolved, facet, estimated_cost } })
|
|
30
|
+
}
|
|
31
|
+
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import { NextResponse } from 'next/server'
|
|
3
|
+
import {
|
|
4
|
+
getSessions,
|
|
5
|
+
readAllSessionMeta,
|
|
6
|
+
readAllFacets,
|
|
7
|
+
listProjectSlugs,
|
|
8
|
+
listProjectJSONLFiles,
|
|
9
|
+
readJSONLLines,
|
|
10
|
+
} from '@/lib/claude-reader'
|
|
11
|
+
import { estimateCostFromUsage } from '@/lib/pricing'
|
|
12
|
+
import type { SessionWithFacet } from '@/types/claude'
|
|
13
|
+
|
|
14
|
+
export const dynamic = 'force-dynamic'
|
|
15
|
+
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
|
+
type AnyLine = Record<string, any>
|
|
18
|
+
|
|
19
|
+
async function enrichSessions(sessions: { session_id: string }[]) {
|
|
20
|
+
// Build a map of sessionId -> { slug, version, gitBranch, has_compaction, has_thinking }
|
|
21
|
+
const enrichment: Record<string, {
|
|
22
|
+
slug?: string
|
|
23
|
+
version?: string
|
|
24
|
+
git_branch?: string
|
|
25
|
+
has_compaction?: boolean
|
|
26
|
+
has_thinking?: boolean
|
|
27
|
+
}> = {}
|
|
28
|
+
|
|
29
|
+
const slugs = await listProjectSlugs()
|
|
30
|
+
await Promise.all(
|
|
31
|
+
slugs.map(async (slug) => {
|
|
32
|
+
const files = await listProjectJSONLFiles(slug)
|
|
33
|
+
await Promise.all(
|
|
34
|
+
files.map(async (f) => {
|
|
35
|
+
const sessionId = path.basename(f, '.jsonl')
|
|
36
|
+
const data: {
|
|
37
|
+
slug?: string; version?: string; gitBranch?: string
|
|
38
|
+
has_compaction?: boolean; has_thinking?: boolean
|
|
39
|
+
} = {}
|
|
40
|
+
|
|
41
|
+
await readJSONLLines(f, (line: AnyLine) => {
|
|
42
|
+
if (!data.slug && line.slug) data.slug = line.slug
|
|
43
|
+
if (!data.version && line.version) data.version = line.version
|
|
44
|
+
if (!data.gitBranch && line.gitBranch && line.gitBranch !== 'HEAD') {
|
|
45
|
+
data.gitBranch = line.gitBranch
|
|
46
|
+
}
|
|
47
|
+
if (line.type === 'system' && line.subtype === 'compact_boundary') {
|
|
48
|
+
data.has_compaction = true
|
|
49
|
+
}
|
|
50
|
+
if (line.type === 'assistant' && Array.isArray(line.message?.content)) {
|
|
51
|
+
if (line.message.content.some((c: AnyLine) => c.type === 'thinking')) {
|
|
52
|
+
data.has_thinking = true
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
enrichment[sessionId] = {
|
|
58
|
+
slug: data.slug,
|
|
59
|
+
version: data.version,
|
|
60
|
+
git_branch: data.gitBranch,
|
|
61
|
+
has_compaction: data.has_compaction,
|
|
62
|
+
has_thinking: data.has_thinking,
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
)
|
|
66
|
+
})
|
|
67
|
+
)
|
|
68
|
+
return enrichment
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function GET() {
|
|
72
|
+
const [sessions, metaSessions, facets] = await Promise.all([
|
|
73
|
+
getSessions(),
|
|
74
|
+
readAllSessionMeta(),
|
|
75
|
+
readAllFacets(),
|
|
76
|
+
])
|
|
77
|
+
const metaMap = new Map(metaSessions.map((s) => [s.session_id, s]))
|
|
78
|
+
const merged = sessions.map((s) => {
|
|
79
|
+
const meta = metaMap.get(s.session_id)
|
|
80
|
+
if (meta) return { ...meta, ...s } as typeof s
|
|
81
|
+
return s
|
|
82
|
+
})
|
|
83
|
+
const enrichment = await enrichSessions(merged)
|
|
84
|
+
|
|
85
|
+
const facetMap = new Map(facets.map(f => [f.session_id, f]))
|
|
86
|
+
|
|
87
|
+
const result: SessionWithFacet[] = merged.map(s => {
|
|
88
|
+
const facet = facetMap.get(s.session_id)
|
|
89
|
+
const enrich = enrichment[s.session_id] ?? {}
|
|
90
|
+
|
|
91
|
+
// Estimate cost from session tokens (rough: treat all as opus)
|
|
92
|
+
const estimated_cost = estimateCostFromUsage('claude-opus-4-6', {
|
|
93
|
+
input_tokens: s.input_tokens ?? 0,
|
|
94
|
+
output_tokens: s.output_tokens ?? 0,
|
|
95
|
+
cache_creation_input_tokens: s.cache_creation_input_tokens ?? 0,
|
|
96
|
+
cache_read_input_tokens: s.cache_read_input_tokens ?? 0,
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
...s,
|
|
101
|
+
facet,
|
|
102
|
+
estimated_cost,
|
|
103
|
+
slug: enrich.slug,
|
|
104
|
+
version: enrich.version,
|
|
105
|
+
git_branch: enrich.git_branch,
|
|
106
|
+
has_compaction: enrich.has_compaction ?? false,
|
|
107
|
+
has_thinking: enrich.has_thinking ?? false,
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
return NextResponse.json({ sessions: result, total: result.length })
|
|
112
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { readSettings, getClaudeStorageBytes, readSkills, readInstalledPlugins } from '@/lib/claude-reader'
|
|
3
|
+
|
|
4
|
+
export const dynamic = 'force-dynamic'
|
|
5
|
+
|
|
6
|
+
export async function GET() {
|
|
7
|
+
const [settings, storageBytes, skills, plugins] = await Promise.all([
|
|
8
|
+
readSettings(),
|
|
9
|
+
getClaudeStorageBytes(),
|
|
10
|
+
readSkills(),
|
|
11
|
+
readInstalledPlugins(),
|
|
12
|
+
])
|
|
13
|
+
return NextResponse.json({ settings, storageBytes, skills, plugins })
|
|
14
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { readStatsCache, getSessions, getClaudeStorageBytes } from '@/lib/claude-reader'
|
|
3
|
+
import { estimateTotalCostFromModel, getPricing } from '@/lib/pricing'
|
|
4
|
+
import type { DailyActivity, SessionMeta } from '@/types/claude'
|
|
5
|
+
|
|
6
|
+
export const dynamic = 'force-dynamic'
|
|
7
|
+
|
|
8
|
+
/** Compute daily activity from session JSONL — fresher than stats-cache */
|
|
9
|
+
function computeDailyActivityFromSessions(sessions: SessionMeta[]): DailyActivity[] {
|
|
10
|
+
const byDate = new Map<string, { messages: number; sessions: number; tools: number }>()
|
|
11
|
+
for (const s of sessions) {
|
|
12
|
+
const date = s.start_time.slice(0, 10)
|
|
13
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) continue
|
|
14
|
+
const existing = byDate.get(date) ?? { messages: 0, sessions: 0, tools: 0 }
|
|
15
|
+
existing.messages += (s.user_message_count ?? 0) + (s.assistant_message_count ?? 0)
|
|
16
|
+
existing.sessions += 1
|
|
17
|
+
existing.tools += Object.values(s.tool_counts ?? {}).reduce((a, b) => a + b, 0)
|
|
18
|
+
byDate.set(date, existing)
|
|
19
|
+
}
|
|
20
|
+
return Array.from(byDate.entries())
|
|
21
|
+
.map(([date, { messages, sessions: count, tools }]) => ({
|
|
22
|
+
date,
|
|
23
|
+
messageCount: messages,
|
|
24
|
+
sessionCount: count,
|
|
25
|
+
toolCallCount: tools,
|
|
26
|
+
}))
|
|
27
|
+
.sort((a, b) => a.date.localeCompare(b.date))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Merge stats dailyActivity with session-derived data; session data overrides for same dates */
|
|
31
|
+
function mergeDailyActivity(
|
|
32
|
+
fromStats: DailyActivity[],
|
|
33
|
+
fromSessions: DailyActivity[]
|
|
34
|
+
): DailyActivity[] {
|
|
35
|
+
const map = new Map<string, DailyActivity>()
|
|
36
|
+
for (const d of fromStats) map.set(d.date, d)
|
|
37
|
+
for (const d of fromSessions) map.set(d.date, d)
|
|
38
|
+
return Array.from(map.values()).sort((a, b) => a.date.localeCompare(b.date))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function GET() {
|
|
42
|
+
const [stats, sessions, storageBytes] = await Promise.all([
|
|
43
|
+
readStatsCache(),
|
|
44
|
+
getSessions(),
|
|
45
|
+
getClaudeStorageBytes(),
|
|
46
|
+
])
|
|
47
|
+
|
|
48
|
+
const dailyFromSessions = computeDailyActivityFromSessions(sessions)
|
|
49
|
+
const dailyActivity = stats
|
|
50
|
+
? mergeDailyActivity(stats.dailyActivity ?? [], dailyFromSessions)
|
|
51
|
+
: dailyFromSessions
|
|
52
|
+
|
|
53
|
+
const modelUsage = stats?.modelUsage ?? {}
|
|
54
|
+
|
|
55
|
+
// Compute estimated total cost from modelUsage
|
|
56
|
+
let totalCost = 0
|
|
57
|
+
let totalCacheSavings = 0
|
|
58
|
+
for (const [model, usage] of Object.entries(modelUsage)) {
|
|
59
|
+
const cost = estimateTotalCostFromModel(model, usage)
|
|
60
|
+
totalCost += cost
|
|
61
|
+
const p = getPricing(model)
|
|
62
|
+
totalCacheSavings += (usage.cacheReadInputTokens ?? 0) * (p.input - p.cacheRead)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Compute total tokens
|
|
66
|
+
let totalInputTokens = 0
|
|
67
|
+
let totalOutputTokens = 0
|
|
68
|
+
let totalCacheReadTokens = 0
|
|
69
|
+
let totalCacheWriteTokens = 0
|
|
70
|
+
for (const usage of Object.values(modelUsage)) {
|
|
71
|
+
totalInputTokens += usage.inputTokens ?? 0
|
|
72
|
+
totalOutputTokens += usage.outputTokens ?? 0
|
|
73
|
+
totalCacheReadTokens += usage.cacheReadInputTokens ?? 0
|
|
74
|
+
totalCacheWriteTokens += usage.cacheCreationInputTokens ?? 0
|
|
75
|
+
}
|
|
76
|
+
const totalTokens = totalInputTokens + totalOutputTokens + totalCacheReadTokens + totalCacheWriteTokens
|
|
77
|
+
|
|
78
|
+
// Aggregate tool calls total
|
|
79
|
+
let totalToolCalls = 0
|
|
80
|
+
for (const s of sessions) {
|
|
81
|
+
for (const count of Object.values(s.tool_counts ?? {})) {
|
|
82
|
+
totalToolCalls += count
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Active days (days with at least 1 session)
|
|
87
|
+
const activeDays = dailyActivity.filter(d => d.sessionCount > 0).length
|
|
88
|
+
|
|
89
|
+
// Average session length
|
|
90
|
+
const avgSessionMinutes =
|
|
91
|
+
sessions.length > 0
|
|
92
|
+
? sessions.reduce((sum, s) => sum + (s.duration_minutes ?? 0), 0) / sessions.length
|
|
93
|
+
: 0
|
|
94
|
+
|
|
95
|
+
// Sessions this month & week
|
|
96
|
+
const now = new Date()
|
|
97
|
+
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1)
|
|
98
|
+
const weekStart = new Date(now)
|
|
99
|
+
weekStart.setDate(now.getDate() - 7)
|
|
100
|
+
|
|
101
|
+
const sessionsThisMonth = sessions.filter(
|
|
102
|
+
s => new Date(s.start_time) >= monthStart
|
|
103
|
+
).length
|
|
104
|
+
const sessionsThisWeek = sessions.filter(
|
|
105
|
+
s => new Date(s.start_time) >= weekStart
|
|
106
|
+
).length
|
|
107
|
+
|
|
108
|
+
const statsOut = stats
|
|
109
|
+
? { ...stats, dailyActivity }
|
|
110
|
+
: {
|
|
111
|
+
version: 0,
|
|
112
|
+
lastComputedDate: '',
|
|
113
|
+
dailyActivity,
|
|
114
|
+
tokensByDate: [],
|
|
115
|
+
modelUsage: {},
|
|
116
|
+
totalSessions: sessions.length,
|
|
117
|
+
totalMessages: sessions.reduce((s, m) => s + (m.user_message_count ?? 0) + (m.assistant_message_count ?? 0), 0),
|
|
118
|
+
longestSession: { sessionId: '', duration: 0, messageCount: 0, timestamp: '' },
|
|
119
|
+
firstSessionDate: sessions[sessions.length - 1]?.start_time ?? '',
|
|
120
|
+
hourCounts: {},
|
|
121
|
+
totalSpeculationTimeSavedMs: 0,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return NextResponse.json({
|
|
125
|
+
stats: statsOut,
|
|
126
|
+
computed: {
|
|
127
|
+
totalCost,
|
|
128
|
+
totalCacheSavings,
|
|
129
|
+
totalTokens,
|
|
130
|
+
totalInputTokens,
|
|
131
|
+
totalOutputTokens,
|
|
132
|
+
totalCacheReadTokens,
|
|
133
|
+
totalCacheWriteTokens,
|
|
134
|
+
totalToolCalls,
|
|
135
|
+
activeDays,
|
|
136
|
+
avgSessionMinutes,
|
|
137
|
+
sessionsThisMonth,
|
|
138
|
+
sessionsThisWeek,
|
|
139
|
+
storageBytes,
|
|
140
|
+
sessionCount: sessions.length,
|
|
141
|
+
},
|
|
142
|
+
})
|
|
143
|
+
}
|