@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
package/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# @devtarang/orca
|
|
2
|
+
|
|
3
|
+
Orca — AI coding agent that plans, builds, and ships software from your terminal. Multi-agent orchestration with explore, review, and architect sub-agents.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @devtarang/orca
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
orca login # Sign in via browser (GitHub/Google)
|
|
15
|
+
orca # Start interactive REPL
|
|
16
|
+
orca "add user authentication" # Run a single instruction
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Commands
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
orca Start interactive REPL
|
|
23
|
+
orca "instruction" Run a single instruction and exit
|
|
24
|
+
orca login Sign in via browser
|
|
25
|
+
orca dashboard Open Orca Pulse analytics dashboard
|
|
26
|
+
orca sessions List recent local sessions
|
|
27
|
+
orca stats Aggregate session stats (tokens, cost, tools)
|
|
28
|
+
orca history Recent prompt history
|
|
29
|
+
orca version Show version
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## REPL Commands
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
/help Show available commands
|
|
36
|
+
/stats Session metrics (tokens, cost, tools)
|
|
37
|
+
/cost Detailed cost breakdown by model
|
|
38
|
+
/history Conversation history
|
|
39
|
+
/clear Clear conversation history
|
|
40
|
+
/explore <query> Spawn read-only codebase explorer
|
|
41
|
+
/review <query> Spawn code review agent
|
|
42
|
+
/architect <query> Spawn architecture planning agent
|
|
43
|
+
/safety Show safety guardrail status
|
|
44
|
+
/revoke Revoke auto-approvals
|
|
45
|
+
/exit Exit the REPL
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Keyboard
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
Esc Cancel current execution
|
|
52
|
+
Space Pause / resume execution
|
|
53
|
+
Ctrl+C Exit
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Configuration
|
|
57
|
+
|
|
58
|
+
Settings are managed via the web dashboard at [devtarang.ai/dashboard/settings](https://devtarang.ai/dashboard/settings) and synced to the CLI automatically.
|
|
59
|
+
|
|
60
|
+
- **API Key**: Add your OpenRouter/Anthropic/OpenAI key in Settings
|
|
61
|
+
- **Model**: Choose your preferred model (40+ supported)
|
|
62
|
+
- **Gateway**: Select provider (OpenRouter, Anthropic, OpenAI, Bedrock, Google AI, etc.)
|
|
63
|
+
|
|
64
|
+
Config directory: `~/.orca/`
|
|
65
|
+
|
|
66
|
+
## Environment Variables
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
TARANG_ENV Set backend (local, development, production)
|
|
70
|
+
ANTHROPIC_API_KEY Direct Anthropic API key
|
|
71
|
+
OPENROUTER_API_KEY OpenRouter API key
|
|
72
|
+
ORCA_CONFIG_DIR Override config directory (default: ~/.orca)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Links
|
|
76
|
+
|
|
77
|
+
- [Tarang Platform](https://devtarang.ai)
|
|
78
|
+
- [Documentation](https://devtarang.ai/docs)
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,7 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axplusb/kepler",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
6
|
-
"
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Kepler — AI coding agent CLI. 30.7% on SWE-bench Lite. Powered by axplusb.tech",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"kepler": "src/terminal/main.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src/",
|
|
11
|
+
"pulse/"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"start": "node src/terminal/main.mjs",
|
|
15
|
+
"test": "node test/test-sse-client.mjs && node test/test-tool-executor.mjs && node test/test-callback.mjs && node test/test-formatter.mjs && node test/test-terminal-rendering.mjs && node test/test-slash-commands.mjs && node test/test-approval.mjs && node test/test-session-manager.mjs && node test/test-safety.mjs && node test/test-jsonl-writer.mjs && node test/test-analytics.mjs && node test/test-stagnation.mjs"
|
|
16
|
+
},
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=18.0.0"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"kepler",
|
|
22
|
+
"codekepler",
|
|
23
|
+
"ai",
|
|
24
|
+
"coding-agent",
|
|
25
|
+
"swe-bench",
|
|
26
|
+
"cli",
|
|
27
|
+
"code-generation",
|
|
28
|
+
"deepseek",
|
|
29
|
+
"axplusb"
|
|
30
|
+
],
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"author": "axplusb.tech <hello@axplusb.tech>",
|
|
33
|
+
"homepage": "https://codekepler.ai",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/axplusb/kepler.git"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {}
|
|
7
39
|
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import useSWR from 'swr'
|
|
4
|
+
import { TopBar } from '@/components/layout/top-bar'
|
|
5
|
+
import { ActivityHeatmap } from '@/components/overview/activity-heatmap'
|
|
6
|
+
import { PeakHoursChart } from '@/components/overview/peak-hours-chart'
|
|
7
|
+
import { DayOfWeekChart } from '@/components/activity/day-of-week-chart'
|
|
8
|
+
import { UsageOverTimeChart } from '@/components/overview/usage-over-time-chart'
|
|
9
|
+
import type { DailyActivity } from '@/types/claude'
|
|
10
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
11
|
+
import { Skeleton } from '@/components/ui/skeleton'
|
|
12
|
+
import { Alert, AlertDescription } from '@/components/ui/alert'
|
|
13
|
+
import { AlertTriangle, Flame, CalendarDays, BarChart3, Clock, Zap, TrendingUp, Star } from 'lucide-react'
|
|
14
|
+
|
|
15
|
+
const fetcher = (url: string) =>
|
|
16
|
+
fetch(url).then(r => { if (!r.ok) throw new Error(`API error ${r.status}`); return r.json() })
|
|
17
|
+
|
|
18
|
+
interface ActivityData {
|
|
19
|
+
daily_activity: DailyActivity[]
|
|
20
|
+
hour_counts: Array<{ hour: number; count: number }>
|
|
21
|
+
dow_counts: Array<{ day: string; count: number }>
|
|
22
|
+
streaks: { current: number; longest: number }
|
|
23
|
+
most_active_day: string
|
|
24
|
+
most_active_day_msgs: number
|
|
25
|
+
total_active_days: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function StatTile({
|
|
29
|
+
label,
|
|
30
|
+
value,
|
|
31
|
+
sub,
|
|
32
|
+
icon: Icon,
|
|
33
|
+
color,
|
|
34
|
+
}: {
|
|
35
|
+
label: string
|
|
36
|
+
value: string | number
|
|
37
|
+
sub: string
|
|
38
|
+
icon: React.ElementType
|
|
39
|
+
color: string
|
|
40
|
+
}) {
|
|
41
|
+
return (
|
|
42
|
+
<Card className="gap-0">
|
|
43
|
+
<CardHeader className="pb-2">
|
|
44
|
+
<CardDescription className="flex items-center gap-2 text-sm font-medium">
|
|
45
|
+
<Icon className="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
46
|
+
{label}
|
|
47
|
+
</CardDescription>
|
|
48
|
+
<CardTitle className="text-3xl font-bold tabular-nums leading-none" style={{ color }}>
|
|
49
|
+
{value}
|
|
50
|
+
</CardTitle>
|
|
51
|
+
</CardHeader>
|
|
52
|
+
<CardContent className="pt-0">
|
|
53
|
+
<p className="text-xs text-muted-foreground">{sub}</p>
|
|
54
|
+
</CardContent>
|
|
55
|
+
</Card>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export default function ActivityPage() {
|
|
60
|
+
const { data, error, isLoading } = useSWR<ActivityData>('/api/activity', fetcher, { refreshInterval: 5_000 })
|
|
61
|
+
|
|
62
|
+
const hourCounts = data
|
|
63
|
+
? Object.fromEntries(data.hour_counts.map(h => [String(h.hour), h.count]))
|
|
64
|
+
: {}
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div className="flex flex-col min-h-screen">
|
|
68
|
+
<TopBar title="Activity" subtitle="Patterns, streaks, and peak hours" />
|
|
69
|
+
<div className="w-full space-y-4 p-4 md:p-6">
|
|
70
|
+
|
|
71
|
+
{error && (
|
|
72
|
+
<Alert variant="destructive">
|
|
73
|
+
<AlertTriangle className="h-4 w-4" />
|
|
74
|
+
<AlertDescription>Error loading data: {String(error)}</AlertDescription>
|
|
75
|
+
</Alert>
|
|
76
|
+
)}
|
|
77
|
+
|
|
78
|
+
{isLoading && (
|
|
79
|
+
<>
|
|
80
|
+
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
81
|
+
{Array.from({ length: 4 }).map((_, i) => <Skeleton key={i} className="h-28 rounded-xl" />)}
|
|
82
|
+
</div>
|
|
83
|
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 md:items-stretch">
|
|
84
|
+
<Skeleton className="h-72 min-h-72 rounded-xl md:min-h-80" />
|
|
85
|
+
<Skeleton className="h-56 min-h-56 rounded-xl md:min-h-80" />
|
|
86
|
+
</div>
|
|
87
|
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 md:items-stretch">
|
|
88
|
+
<Skeleton className="h-64 rounded-xl md:min-h-72" />
|
|
89
|
+
<Skeleton className="h-56 rounded-xl md:min-h-72" />
|
|
90
|
+
</div>
|
|
91
|
+
</>
|
|
92
|
+
)}
|
|
93
|
+
|
|
94
|
+
{data && (
|
|
95
|
+
<>
|
|
96
|
+
{/* Stat tiles */}
|
|
97
|
+
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
98
|
+
<StatTile
|
|
99
|
+
label="Current Streak"
|
|
100
|
+
value={data.streaks.current}
|
|
101
|
+
sub="consecutive days"
|
|
102
|
+
icon={Flame}
|
|
103
|
+
color="#f97316"
|
|
104
|
+
/>
|
|
105
|
+
<StatTile
|
|
106
|
+
label="Longest Streak"
|
|
107
|
+
value={data.streaks.longest}
|
|
108
|
+
sub="personal best"
|
|
109
|
+
icon={Zap}
|
|
110
|
+
color="var(--viz-sky)"
|
|
111
|
+
/>
|
|
112
|
+
<StatTile
|
|
113
|
+
label="Active Days"
|
|
114
|
+
value={data.total_active_days}
|
|
115
|
+
sub="total days with activity"
|
|
116
|
+
icon={TrendingUp}
|
|
117
|
+
color="#a78bfa"
|
|
118
|
+
/>
|
|
119
|
+
<StatTile
|
|
120
|
+
label="Most Active Day"
|
|
121
|
+
value={data.most_active_day ? data.most_active_day.slice(5) : '—'}
|
|
122
|
+
sub={data.most_active_day_msgs ? `${data.most_active_day_msgs.toLocaleString()} messages` : 'no data'}
|
|
123
|
+
icon={Star}
|
|
124
|
+
color="#34d399"
|
|
125
|
+
/>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
{/* Row 1: Activity calendar | Peak hours */}
|
|
129
|
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 md:items-stretch">
|
|
130
|
+
<Card className="flex h-full min-h-0 min-w-0 flex-col gap-2 py-4">
|
|
131
|
+
<CardHeader className="space-y-1 pb-0">
|
|
132
|
+
<CardTitle className="flex items-center gap-2 text-base font-semibold">
|
|
133
|
+
<CalendarDays className="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
134
|
+
Activity Calendar
|
|
135
|
+
</CardTitle>
|
|
136
|
+
<CardDescription>GitHub-style contribution heatmap</CardDescription>
|
|
137
|
+
</CardHeader>
|
|
138
|
+
<CardContent className="flex flex-1 flex-col pt-3 pb-0">
|
|
139
|
+
<ActivityHeatmap data={data.daily_activity} />
|
|
140
|
+
</CardContent>
|
|
141
|
+
</Card>
|
|
142
|
+
|
|
143
|
+
<Card className="flex h-full min-h-0 min-w-0 flex-col gap-2 py-4">
|
|
144
|
+
<CardHeader className="space-y-1 pb-0">
|
|
145
|
+
<CardTitle className="flex items-center gap-2 text-base font-semibold">
|
|
146
|
+
<Clock className="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
147
|
+
Peak Hours
|
|
148
|
+
</CardTitle>
|
|
149
|
+
<CardDescription>Activity by hour of day</CardDescription>
|
|
150
|
+
</CardHeader>
|
|
151
|
+
<CardContent className="flex flex-1 flex-col justify-between pt-3 pb-0">
|
|
152
|
+
<PeakHoursChart hourCounts={hourCounts} />
|
|
153
|
+
</CardContent>
|
|
154
|
+
</Card>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
{/* Row 2: Usage over time | Day of week */}
|
|
158
|
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 md:items-stretch">
|
|
159
|
+
<Card className="flex h-full min-h-0 min-w-0 flex-col gap-2 py-4">
|
|
160
|
+
<CardHeader className="space-y-1 pb-0">
|
|
161
|
+
<CardTitle className="flex items-center gap-2 text-base font-semibold">
|
|
162
|
+
<BarChart3 className="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
163
|
+
Usage Over Time
|
|
164
|
+
</CardTitle>
|
|
165
|
+
<CardDescription>Messages and sessions over the last 90 days</CardDescription>
|
|
166
|
+
</CardHeader>
|
|
167
|
+
<CardContent className="flex flex-1 flex-col pt-3 pb-0">
|
|
168
|
+
<UsageOverTimeChart data={data.daily_activity} days={90} />
|
|
169
|
+
</CardContent>
|
|
170
|
+
</Card>
|
|
171
|
+
|
|
172
|
+
<Card className="flex h-full min-h-0 min-w-0 flex-col gap-2 py-4">
|
|
173
|
+
<CardHeader className="space-y-1 pb-0">
|
|
174
|
+
<CardTitle className="flex items-center gap-2 text-base font-semibold">
|
|
175
|
+
<CalendarDays className="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
176
|
+
Day of Week
|
|
177
|
+
</CardTitle>
|
|
178
|
+
<CardDescription>Which days you use Orca most</CardDescription>
|
|
179
|
+
</CardHeader>
|
|
180
|
+
<CardContent className="flex flex-1 flex-col justify-between pt-3 pb-0">
|
|
181
|
+
<DayOfWeekChart data={data.dow_counts} />
|
|
182
|
+
</CardContent>
|
|
183
|
+
</Card>
|
|
184
|
+
</div>
|
|
185
|
+
</>
|
|
186
|
+
)}
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
)
|
|
190
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { getSessions, readStatsCache } from '@/lib/claude-reader'
|
|
3
|
+
import type { DailyActivity, SessionMeta } from '@/types/claude'
|
|
4
|
+
|
|
5
|
+
export const dynamic = 'force-dynamic'
|
|
6
|
+
|
|
7
|
+
function computeStreaks(dates: Set<string>): { current: number; longest: number } {
|
|
8
|
+
const sorted = [...dates].sort()
|
|
9
|
+
if (sorted.length === 0) return { current: 0, longest: 0 }
|
|
10
|
+
|
|
11
|
+
let longest = 1
|
|
12
|
+
let streak = 1
|
|
13
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
14
|
+
const prev = new Date(sorted[i - 1])
|
|
15
|
+
const curr = new Date(sorted[i])
|
|
16
|
+
const diff = (curr.getTime() - prev.getTime()) / 86_400_000
|
|
17
|
+
if (diff === 1) {
|
|
18
|
+
streak++
|
|
19
|
+
if (streak > longest) longest = streak
|
|
20
|
+
} else {
|
|
21
|
+
streak = 1
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Current streak (from today backwards)
|
|
26
|
+
const today = new Date().toISOString().slice(0, 10)
|
|
27
|
+
let current = 0
|
|
28
|
+
const d = new Date(today)
|
|
29
|
+
while (dates.has(d.toISOString().slice(0, 10))) {
|
|
30
|
+
current++
|
|
31
|
+
d.setDate(d.getDate() - 1)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { current, longest }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function computeDailyActivityFromSessions(sessions: SessionMeta[]): DailyActivity[] {
|
|
38
|
+
const byDate = new Map<string, { messages: number; sessions: number; tools: number }>()
|
|
39
|
+
|
|
40
|
+
for (const s of sessions) {
|
|
41
|
+
if (!s.start_time) continue
|
|
42
|
+
const date = s.start_time.slice(0, 10)
|
|
43
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) continue
|
|
44
|
+
|
|
45
|
+
const existing = byDate.get(date) ?? { messages: 0, sessions: 0, tools: 0 }
|
|
46
|
+
existing.messages += (s.user_message_count ?? 0) + (s.assistant_message_count ?? 0)
|
|
47
|
+
existing.sessions += 1
|
|
48
|
+
existing.tools += Object.values(s.tool_counts ?? {}).reduce((sum, count) => sum + count, 0)
|
|
49
|
+
byDate.set(date, existing)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return Array.from(byDate.entries())
|
|
53
|
+
.map(([date, { messages, sessions: sessionCount, tools }]) => ({
|
|
54
|
+
date,
|
|
55
|
+
messageCount: messages,
|
|
56
|
+
sessionCount,
|
|
57
|
+
toolCallCount: tools,
|
|
58
|
+
}))
|
|
59
|
+
.sort((a, b) => a.date.localeCompare(b.date))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function mergeDailyActivity(fromStats: DailyActivity[], fromSessions: DailyActivity[]): DailyActivity[] {
|
|
63
|
+
const byDate = new Map<string, DailyActivity>()
|
|
64
|
+
for (const d of fromStats) byDate.set(d.date, d)
|
|
65
|
+
for (const d of fromSessions) byDate.set(d.date, d)
|
|
66
|
+
return Array.from(byDate.values()).sort((a, b) => a.date.localeCompare(b.date))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function computeHourCounts(sessions: SessionMeta[]): Array<{ hour: number; count: number }> {
|
|
70
|
+
const hourCounts = Array.from({ length: 24 }, () => 0)
|
|
71
|
+
|
|
72
|
+
for (const s of sessions) {
|
|
73
|
+
if (s.user_message_timestamps?.length) {
|
|
74
|
+
for (const ts of s.user_message_timestamps) {
|
|
75
|
+
const d = new Date(ts)
|
|
76
|
+
if (!isNaN(d.getTime())) hourCounts[d.getHours()]++
|
|
77
|
+
}
|
|
78
|
+
continue
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (s.message_hours?.length) {
|
|
82
|
+
for (const hour of s.message_hours) {
|
|
83
|
+
if (Number.isInteger(hour) && hour >= 0 && hour < 24) hourCounts[hour]++
|
|
84
|
+
}
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const d = new Date(s.start_time)
|
|
89
|
+
if (!isNaN(d.getTime())) hourCounts[d.getHours()]++
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return hourCounts.map((count, hour) => ({ hour, count }))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function GET() {
|
|
96
|
+
const [stats, sessions] = await Promise.all([readStatsCache(), getSessions()])
|
|
97
|
+
const dailyFromSessions = computeDailyActivityFromSessions(sessions)
|
|
98
|
+
const dailyActivity = stats
|
|
99
|
+
? mergeDailyActivity(stats.dailyActivity ?? [], dailyFromSessions)
|
|
100
|
+
: dailyFromSessions
|
|
101
|
+
|
|
102
|
+
// Day-of-week counts from session timestamps
|
|
103
|
+
const dowCounts: number[] = [0, 0, 0, 0, 0, 0, 0] // Sun=0..Sat=6
|
|
104
|
+
const activeDates = new Set<string>()
|
|
105
|
+
|
|
106
|
+
for (const s of sessions) {
|
|
107
|
+
if (!s.start_time) continue
|
|
108
|
+
const d = new Date(s.start_time)
|
|
109
|
+
if (isNaN(d.getTime())) continue
|
|
110
|
+
dowCounts[d.getDay()]++
|
|
111
|
+
activeDates.add(s.start_time.slice(0, 10))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const streaks = computeStreaks(activeDates)
|
|
115
|
+
|
|
116
|
+
// Most active day
|
|
117
|
+
let mostActiveDay = ''
|
|
118
|
+
let mostActiveMsgs = 0
|
|
119
|
+
for (const da of dailyActivity) {
|
|
120
|
+
if (da.messageCount > mostActiveMsgs) {
|
|
121
|
+
mostActiveMsgs = da.messageCount
|
|
122
|
+
mostActiveDay = da.date
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return NextResponse.json({
|
|
127
|
+
daily_activity: dailyActivity,
|
|
128
|
+
hour_counts: computeHourCounts(sessions),
|
|
129
|
+
dow_counts: dowCounts.map((count, i) => ({
|
|
130
|
+
day: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][i],
|
|
131
|
+
count,
|
|
132
|
+
})),
|
|
133
|
+
streaks,
|
|
134
|
+
most_active_day: mostActiveDay,
|
|
135
|
+
most_active_day_msgs: mostActiveMsgs,
|
|
136
|
+
total_active_days: activeDates.size,
|
|
137
|
+
})
|
|
138
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
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 ~/.orca/ live data.
|
|
28
|
+
// The import feature shows a diff preview only.
|
|
29
|
+
|
|
30
|
+
return NextResponse.json(diff)
|
|
31
|
+
}
|