@bahulam/code 0.1.5 → 0.1.7
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/LICENSE +1 -1
- package/NOTICE +6 -6
- package/README.md +10 -17
- package/package.json +9 -9
- package/src/agents/loader.mjs +1 -1
- package/src/agents/workflow_loader.mjs +2 -2
- package/src/auth/tarang-auth.mjs +3 -2
- package/src/config/model-catalog-default.json +4 -3
- package/src/config/model-catalog.mjs +2 -0
- package/src/context/prose-chunker.mjs +162 -5
- package/src/core/attachments.mjs +65 -3
- package/src/core/bundled-runtime.mjs +1 -1
- package/src/core/headless.mjs +14 -0
- package/src/core/jsonl-writer.mjs +3 -3
- package/src/core/lint-resolver.mjs +472 -0
- package/src/core/local-agent.mjs +15 -4
- package/src/core/local-store.mjs +4 -0
- package/src/core/output-filter.mjs +21 -25
- package/src/core/pricing.mjs +10 -3
- package/src/core/stream-client.mjs +112 -16
- package/src/core/tool-executor.mjs +123 -31
- package/src/daemon/http-dashboard.mjs +400 -0
- package/src/daemon/session-core.mjs +1 -0
- package/src/local-service/agent-relay.mjs +868 -0
- package/src/local-service/approval-bridge.mjs +222 -0
- package/src/local-service/browser.mjs +24 -0
- package/src/local-service/command.mjs +155 -0
- package/src/local-service/file-access.mjs +393 -0
- package/src/local-service/machine.mjs +86 -0
- package/src/local-service/paths.mjs +29 -0
- package/src/local-service/preview-converters.mjs +260 -0
- package/src/local-service/server.mjs +2644 -0
- package/src/local-service/session-store.mjs +221 -0
- package/src/onboarding/preflight.mjs +1 -1
- package/src/terminal/analytics.mjs +2 -6
- package/src/terminal/ansi.mjs +11 -3
- package/src/terminal/main.mjs +37 -11
- package/src/terminal/model-catalog-display.mjs +111 -0
- package/src/terminal/repl-format.mjs +4 -1
- package/src/terminal/repl-model-form.mjs +81 -14
- package/src/terminal/repl-render.mjs +35 -5
- package/src/terminal/repl-state.mjs +12 -0
- package/src/terminal/repl.mjs +255 -55
- package/src/terminal/tool-display.mjs +32 -0
- package/src/terminal/watch-state.mjs +118 -0
- package/src/tools/analyze-image.mjs +158 -0
- package/src/tools/bash.mjs +11 -9
- package/src/tools/generate-image.mjs +411 -0
- package/src/tools/lint.mjs +24 -43
- package/src/tools/project-overview.mjs +5 -2
- package/src/tools/registry.mjs +4 -0
- package/src/ui/icons.mjs +2 -0
- package/src/ui/palette.mjs +1 -1
- package/src/ui/slash-commands.mjs +4 -6
- package/src/ui/tool-card.mjs +7 -3
- package/pulse/app/activity/page.tsx +0 -190
- package/pulse/app/api/activity/route.ts +0 -138
- package/pulse/app/api/benchmark/route.ts +0 -113
- package/pulse/app/api/benchmarks/route.ts +0 -195
- package/pulse/app/api/costs/route.ts +0 -88
- package/pulse/app/api/export/route.ts +0 -77
- package/pulse/app/api/history/route.ts +0 -11
- package/pulse/app/api/import/route.ts +0 -31
- package/pulse/app/api/memory/route.ts +0 -50
- package/pulse/app/api/plans/route.ts +0 -9
- package/pulse/app/api/projects/[slug]/route.ts +0 -96
- package/pulse/app/api/projects/route.ts +0 -121
- package/pulse/app/api/sessions/[id]/replay/route.ts +0 -20
- package/pulse/app/api/sessions/[id]/route.ts +0 -31
- package/pulse/app/api/sessions/route.ts +0 -112
- package/pulse/app/api/settings/route.ts +0 -14
- package/pulse/app/api/stats/route.ts +0 -143
- package/pulse/app/api/todos/route.ts +0 -9
- package/pulse/app/api/tools/route.ts +0 -160
- package/pulse/app/benchmarks/page.tsx +0 -224
- package/pulse/app/costs/page.tsx +0 -179
- package/pulse/app/export/page.tsx +0 -465
- package/pulse/app/favicon.ico +0 -0
- package/pulse/app/globals.css +0 -263
- package/pulse/app/help/page.tsx +0 -143
- package/pulse/app/history/page.tsx +0 -157
- package/pulse/app/layout.tsx +0 -46
- package/pulse/app/memory/page.tsx +0 -365
- package/pulse/app/overview-client.tsx +0 -393
- package/pulse/app/page.tsx +0 -14
- package/pulse/app/plans/page.tsx +0 -308
- package/pulse/app/projects/[slug]/page.tsx +0 -390
- package/pulse/app/projects/page.tsx +0 -110
- package/pulse/app/sessions/[id]/page.tsx +0 -243
- package/pulse/app/sessions/page.tsx +0 -39
- package/pulse/app/settings/page.tsx +0 -188
- package/pulse/app/todos/page.tsx +0 -211
- package/pulse/app/tools/page.tsx +0 -249
- package/pulse/cli.js +0 -164
- package/pulse/components/activity/day-of-week-chart.tsx +0 -35
- package/pulse/components/activity/streak-card.tsx +0 -36
- package/pulse/components/costs/cache-efficiency-panel.tsx +0 -76
- package/pulse/components/costs/cost-by-project-chart.tsx +0 -48
- package/pulse/components/costs/cost-over-time-chart.tsx +0 -95
- package/pulse/components/costs/model-token-table.tsx +0 -60
- package/pulse/components/global-search.tsx +0 -193
- package/pulse/components/keyboard-nav-provider.tsx +0 -23
- package/pulse/components/layout/bottom-nav.tsx +0 -53
- package/pulse/components/layout/client-layout.tsx +0 -31
- package/pulse/components/layout/sidebar-context.tsx +0 -50
- package/pulse/components/layout/sidebar.tsx +0 -183
- package/pulse/components/layout/top-bar.tsx +0 -121
- package/pulse/components/overview/activity-heatmap.tsx +0 -107
- package/pulse/components/overview/conversation-table.tsx +0 -148
- package/pulse/components/overview/model-breakdown-donut.tsx +0 -95
- package/pulse/components/overview/peak-hours-chart.tsx +0 -87
- package/pulse/components/overview/project-activity-donut.tsx +0 -96
- package/pulse/components/overview/stat-card.tsx +0 -102
- package/pulse/components/overview/usage-over-time-chart.tsx +0 -166
- package/pulse/components/projects/project-card.tsx +0 -175
- package/pulse/components/sessions/replay/assistant-markdown.tsx +0 -94
- package/pulse/components/sessions/replay/compaction-card.tsx +0 -25
- package/pulse/components/sessions/replay/session-sidebar.tsx +0 -231
- package/pulse/components/sessions/replay/token-accumulation-chart.tsx +0 -98
- package/pulse/components/sessions/replay/tool-call-badge.tsx +0 -127
- package/pulse/components/sessions/replay/turn-cards.tsx +0 -220
- package/pulse/components/sessions/replay/user-tool-result.tsx +0 -158
- package/pulse/components/sessions/session-badges.tsx +0 -49
- package/pulse/components/sessions/session-table.tsx +0 -299
- package/pulse/components/theme-provider.tsx +0 -44
- package/pulse/components/tools/feature-adoption-table.tsx +0 -58
- package/pulse/components/tools/mcp-server-panel.tsx +0 -45
- package/pulse/components/tools/tool-ranking-chart.tsx +0 -57
- package/pulse/components/tools/version-history-table.tsx +0 -32
- package/pulse/components/ui/alert.tsx +0 -66
- package/pulse/components/ui/badge.tsx +0 -48
- package/pulse/components/ui/breadcrumb.tsx +0 -109
- package/pulse/components/ui/button.tsx +0 -64
- package/pulse/components/ui/calendar.tsx +0 -220
- package/pulse/components/ui/card.tsx +0 -92
- package/pulse/components/ui/command.tsx +0 -158
- package/pulse/components/ui/dialog.tsx +0 -158
- package/pulse/components/ui/input.tsx +0 -21
- package/pulse/components/ui/popover.tsx +0 -89
- package/pulse/components/ui/progress.tsx +0 -31
- package/pulse/components/ui/select.tsx +0 -190
- package/pulse/components/ui/separator.tsx +0 -28
- package/pulse/components/ui/sheet.tsx +0 -143
- package/pulse/components/ui/skeleton.tsx +0 -13
- package/pulse/components/ui/table.tsx +0 -116
- package/pulse/components/ui/tabs.tsx +0 -91
- package/pulse/components/ui/tooltip.tsx +0 -57
- package/pulse/components/use-global-keyboard-nav.ts +0 -79
- package/pulse/components.json +0 -23
- package/pulse/eslint.config.mjs +0 -18
- package/pulse/lib/bahulam-paths.ts +0 -23
- package/pulse/lib/claude-reader.ts +0 -592
- package/pulse/lib/decode.ts +0 -129
- package/pulse/lib/pricing.ts +0 -102
- package/pulse/lib/replay-parser.ts +0 -165
- package/pulse/lib/tool-categories.ts +0 -140
- package/pulse/lib/utils.ts +0 -6
- package/pulse/next-env.d.ts +0 -6
- package/pulse/next.config.ts +0 -16
- package/pulse/package.json +0 -45
- package/pulse/postcss.config.mjs +0 -7
- 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 +0 -1
- package/pulse/public/globe.svg +0 -1
- package/pulse/public/next.svg +0 -1
- 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 +0 -1
- package/pulse/public/window.svg +0 -1
- package/pulse/tsconfig.json +0 -34
- package/pulse/types/claude.ts +0 -294
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server'
|
|
2
|
-
import { readStatsCache, getSessions } from '@/lib/claude-reader'
|
|
3
|
-
import { estimateTotalCostFromModel, cacheEfficiency, getPricing, PRICING } from '@/lib/pricing'
|
|
4
|
-
import { projectDisplayName } from '@/lib/decode'
|
|
5
|
-
import type { CostAnalytics, ModelCostBreakdown, DailyCost, ProjectCost } from '@/types/claude'
|
|
6
|
-
|
|
7
|
-
export const dynamic = 'force-dynamic'
|
|
8
|
-
|
|
9
|
-
export async function GET() {
|
|
10
|
-
const [stats, sessions] = await Promise.all([readStatsCache(), getSessions()])
|
|
11
|
-
|
|
12
|
-
if (!stats) {
|
|
13
|
-
return NextResponse.json({ error: 'stats-cache.json not found' }, { status: 404 })
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// ── Per-model breakdown ────────────────────────────────────────────────────
|
|
17
|
-
let totalCost = 0
|
|
18
|
-
let totalSavings = 0
|
|
19
|
-
const models: ModelCostBreakdown[] = Object.entries(stats.modelUsage ?? {}).map(([model, usage]) => {
|
|
20
|
-
const cost = estimateTotalCostFromModel(model, usage)
|
|
21
|
-
const eff = cacheEfficiency(model, usage)
|
|
22
|
-
totalCost += cost
|
|
23
|
-
totalSavings += eff.savedUSD
|
|
24
|
-
return {
|
|
25
|
-
model,
|
|
26
|
-
input_tokens: usage.inputTokens ?? 0,
|
|
27
|
-
output_tokens: usage.outputTokens ?? 0,
|
|
28
|
-
cache_write_tokens: usage.cacheCreationInputTokens ?? 0,
|
|
29
|
-
cache_read_tokens: usage.cacheReadInputTokens ?? 0,
|
|
30
|
-
estimated_cost: cost,
|
|
31
|
-
cache_savings: eff.savedUSD ?? 0,
|
|
32
|
-
cache_hit_rate: eff.hitRate ?? 0,
|
|
33
|
-
}
|
|
34
|
-
}).sort((a, b) => b.estimated_cost - a.estimated_cost)
|
|
35
|
-
|
|
36
|
-
// ── Daily cost by model ────────────────────────────────────────────────────
|
|
37
|
-
// stats-cache.json uses "dailyModelTokens" in newer CC versions; "tokensByDate" in older ones
|
|
38
|
-
const daily: DailyCost[] = (stats.dailyModelTokens ?? stats.tokensByDate ?? []).map(d => {
|
|
39
|
-
const costs: Record<string, number> = {}
|
|
40
|
-
let dayTotal = 0
|
|
41
|
-
for (const [model, tokens] of Object.entries(d.tokensByModel ?? {})) {
|
|
42
|
-
const p = getPricing(model)
|
|
43
|
-
// tokensByDate only has total tokens, approximate as input+output split 50/50
|
|
44
|
-
const cost = tokens * p.input * 0.5 + tokens * p.output * 0.5
|
|
45
|
-
costs[model] = cost
|
|
46
|
-
dayTotal += cost
|
|
47
|
-
}
|
|
48
|
-
return { date: d.date, costs, total: dayTotal }
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
// ── Cost by project ────────────────────────────────────────────────────────
|
|
52
|
-
const projectMap = new Map<string, { cost: number; input: number; output: number }>()
|
|
53
|
-
for (const s of sessions) {
|
|
54
|
-
const pp = s.project_path ?? ''
|
|
55
|
-
const slug = pp
|
|
56
|
-
const existing = projectMap.get(slug) ?? { cost: 0, input: 0, output: 0 }
|
|
57
|
-
const cost = estimateTotalCostFromModel('claude-opus-4-6', {
|
|
58
|
-
inputTokens: s.input_tokens ?? 0,
|
|
59
|
-
outputTokens: s.output_tokens ?? 0,
|
|
60
|
-
cacheCreationInputTokens: s.cache_creation_input_tokens ?? 0,
|
|
61
|
-
cacheReadInputTokens: s.cache_read_input_tokens ?? 0,
|
|
62
|
-
costUSD: 0,
|
|
63
|
-
webSearchRequests: 0,
|
|
64
|
-
})
|
|
65
|
-
projectMap.set(slug, {
|
|
66
|
-
cost: existing.cost + cost,
|
|
67
|
-
input: existing.input + (s.input_tokens ?? 0),
|
|
68
|
-
output: existing.output + (s.output_tokens ?? 0),
|
|
69
|
-
})
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const by_project: ProjectCost[] = [...projectMap.entries()]
|
|
73
|
-
.map(([slug, data]) => {
|
|
74
|
-
const projectPath = slug
|
|
75
|
-
return {
|
|
76
|
-
slug,
|
|
77
|
-
display_name: projectDisplayName(projectPath),
|
|
78
|
-
estimated_cost: data.cost,
|
|
79
|
-
input_tokens: data.input,
|
|
80
|
-
output_tokens: data.output,
|
|
81
|
-
}
|
|
82
|
-
})
|
|
83
|
-
.sort((a, b) => b.estimated_cost - a.estimated_cost)
|
|
84
|
-
.slice(0, 20)
|
|
85
|
-
|
|
86
|
-
const result: CostAnalytics = { total_cost: totalCost, total_savings: totalSavings, models, daily, by_project }
|
|
87
|
-
return NextResponse.json(result)
|
|
88
|
-
}
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server'
|
|
2
|
-
import { readStatsCache, getSessions, readAllFacets, readHistory } from '@/lib/claude-reader'
|
|
3
|
-
import type { ExportPayload, Facet, SessionMeta } from '@/types/claude'
|
|
4
|
-
|
|
5
|
-
export const dynamic = 'force-dynamic'
|
|
6
|
-
|
|
7
|
-
function filterSessionsByDateRange(
|
|
8
|
-
sessions: SessionMeta[],
|
|
9
|
-
dateRange?: { from?: string; to?: string }
|
|
10
|
-
) {
|
|
11
|
-
const fromMs = dateRange?.from ? new Date(dateRange.from).getTime() : null
|
|
12
|
-
const toMs = dateRange?.to ? new Date(dateRange.to + 'T23:59:59.999Z').getTime() : null
|
|
13
|
-
return sessions.filter(s => {
|
|
14
|
-
if (!s.start_time) return true
|
|
15
|
-
const t = new Date(s.start_time).getTime()
|
|
16
|
-
if (fromMs !== null && t < fromMs) return false
|
|
17
|
-
if (toMs !== null && t > toMs) return false
|
|
18
|
-
return true
|
|
19
|
-
})
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function facetsForSessions(facets: Facet[], sessions: SessionMeta[]) {
|
|
23
|
-
const sessionIds = new Set(sessions.map(s => s.session_id))
|
|
24
|
-
return facets.filter(f => sessionIds.has(f.session_id))
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** Preview counts for the export UI (optional date filter via query params). */
|
|
28
|
-
export async function GET(req: Request) {
|
|
29
|
-
const url = new URL(req.url)
|
|
30
|
-
const from = url.searchParams.get('from') || undefined
|
|
31
|
-
const to = url.searchParams.get('to') || undefined
|
|
32
|
-
const dateRange = from || to ? { from, to } : undefined
|
|
33
|
-
|
|
34
|
-
const [stats, sessions, facets, history] = await Promise.all([
|
|
35
|
-
readStatsCache(),
|
|
36
|
-
getSessions(),
|
|
37
|
-
readAllFacets(),
|
|
38
|
-
readHistory(10_000),
|
|
39
|
-
])
|
|
40
|
-
|
|
41
|
-
const filteredSessions = filterSessionsByDateRange(sessions, dateRange)
|
|
42
|
-
const filteredFacets = facetsForSessions(facets, filteredSessions)
|
|
43
|
-
|
|
44
|
-
return NextResponse.json({
|
|
45
|
-
sessionCount: filteredSessions.length,
|
|
46
|
-
facetCount: filteredFacets.length,
|
|
47
|
-
historyEntries: history.length,
|
|
48
|
-
hasStatsCache: stats !== null,
|
|
49
|
-
totalSessionsIndexed: sessions.length,
|
|
50
|
-
})
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export async function POST(req: Request) {
|
|
54
|
-
const body = await req.json().catch(() => ({}))
|
|
55
|
-
const { dateRange } = body as { dateRange?: { from?: string; to?: string } }
|
|
56
|
-
|
|
57
|
-
const [stats, sessions, facets, history] = await Promise.all([
|
|
58
|
-
readStatsCache(),
|
|
59
|
-
getSessions(),
|
|
60
|
-
readAllFacets(),
|
|
61
|
-
readHistory(10_000),
|
|
62
|
-
])
|
|
63
|
-
|
|
64
|
-
const filteredSessions = filterSessionsByDateRange(sessions, dateRange)
|
|
65
|
-
const filteredFacets = facetsForSessions(facets, filteredSessions)
|
|
66
|
-
|
|
67
|
-
const payload: ExportPayload = {
|
|
68
|
-
exportedAt: new Date().toISOString(),
|
|
69
|
-
version: '1.0.0',
|
|
70
|
-
stats,
|
|
71
|
-
sessions: filteredSessions,
|
|
72
|
-
facets: filteredFacets,
|
|
73
|
-
history,
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return NextResponse.json(payload)
|
|
77
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server'
|
|
2
|
-
import { readHistory } from '@/lib/claude-reader'
|
|
3
|
-
|
|
4
|
-
export const dynamic = 'force-dynamic'
|
|
5
|
-
|
|
6
|
-
export async function GET(req: Request) {
|
|
7
|
-
const { searchParams } = new URL(req.url)
|
|
8
|
-
const limit = Math.min(parseInt(searchParams.get('limit') ?? '200', 10), 10_000)
|
|
9
|
-
const history = await readHistory(limit)
|
|
10
|
-
return NextResponse.json({ history })
|
|
11
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server'
|
|
2
|
-
import { getSessions } from '@/lib/claude-reader'
|
|
3
|
-
import type { ExportPayload, ImportDiff } from '@/types/claude'
|
|
4
|
-
|
|
5
|
-
export const dynamic = 'force-dynamic'
|
|
6
|
-
|
|
7
|
-
export async function POST(req: Request) {
|
|
8
|
-
const payload = await req.json().catch(() => null) as ExportPayload | null
|
|
9
|
-
|
|
10
|
-
if (!payload || !payload.sessions) {
|
|
11
|
-
return NextResponse.json({ error: 'Invalid import payload' }, { status: 400 })
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const existing = await getSessions()
|
|
15
|
-
const existingIds = new Set(existing.map(s => s.session_id))
|
|
16
|
-
|
|
17
|
-
const sessions_to_add = payload.sessions.filter(s => !existingIds.has(s.session_id))
|
|
18
|
-
|
|
19
|
-
const diff: ImportDiff = {
|
|
20
|
-
total_in_export: payload.sessions.length,
|
|
21
|
-
already_present: payload.sessions.length - sessions_to_add.length,
|
|
22
|
-
new_sessions: sessions_to_add.length,
|
|
23
|
-
sessions_to_add,
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Note: actual file writing is intentionally not implemented here
|
|
27
|
-
// to prevent accidental corruption of ~/.bahulam/ live data.
|
|
28
|
-
// The import feature shows a diff preview only.
|
|
29
|
-
|
|
30
|
-
return NextResponse.json(diff)
|
|
31
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server'
|
|
2
|
-
import fs from 'fs/promises'
|
|
3
|
-
import path from 'path'
|
|
4
|
-
import { readMemories } from '@/lib/claude-reader'
|
|
5
|
-
import { bahulamPath } from '@/lib/bahulam-paths'
|
|
6
|
-
|
|
7
|
-
export const dynamic = 'force-dynamic'
|
|
8
|
-
|
|
9
|
-
export async function GET() {
|
|
10
|
-
const memories = await readMemories()
|
|
11
|
-
return NextResponse.json({ memories })
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export async function PATCH(req: Request) {
|
|
15
|
-
try {
|
|
16
|
-
const { projectSlug, file, content } = await req.json() as {
|
|
17
|
-
projectSlug?: string
|
|
18
|
-
file?: string
|
|
19
|
-
content?: string
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
if (!projectSlug || !file || typeof content !== 'string') {
|
|
23
|
-
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 })
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Only allow .md files
|
|
27
|
-
if (!file.endsWith('.md')) {
|
|
28
|
-
return NextResponse.json({ error: 'Only .md files allowed' }, { status: 400 })
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Prevent path traversal — slug and file must be plain names (no slashes or dots)
|
|
32
|
-
if (/[/\\]/.test(projectSlug) || /[/\\]/.test(file)) {
|
|
33
|
-
return NextResponse.json({ error: 'Invalid path' }, { status: 400 })
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const filePath = bahulamPath('projects', projectSlug, 'memory', file)
|
|
37
|
-
|
|
38
|
-
// Ensure the resolved path stays within ~/.bahulam/projects/ (or the configured legacy dir).
|
|
39
|
-
const allowedRoot = bahulamPath('projects')
|
|
40
|
-
if (!filePath.startsWith(allowedRoot + path.sep)) {
|
|
41
|
-
return NextResponse.json({ error: 'Path outside allowed directory' }, { status: 403 })
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
|
45
|
-
await fs.writeFile(filePath, content, 'utf-8')
|
|
46
|
-
return NextResponse.json({ ok: true })
|
|
47
|
-
} catch (err) {
|
|
48
|
-
return NextResponse.json({ error: String(err) }, { status: 500 })
|
|
49
|
-
}
|
|
50
|
-
}
|
|
@@ -1,96 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,121 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
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
|
-
|
|
@@ -1,112 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
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
|
-
}
|