@axplusb/kepler 0.0.1 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/README.md +82 -0
  2. package/package.json +36 -4
  3. package/pulse/app/activity/page.tsx +190 -0
  4. package/pulse/app/api/activity/route.ts +138 -0
  5. package/pulse/app/api/costs/route.ts +88 -0
  6. package/pulse/app/api/export/route.ts +77 -0
  7. package/pulse/app/api/history/route.ts +11 -0
  8. package/pulse/app/api/import/route.ts +31 -0
  9. package/pulse/app/api/memory/route.ts +52 -0
  10. package/pulse/app/api/plans/route.ts +9 -0
  11. package/pulse/app/api/projects/[slug]/route.ts +96 -0
  12. package/pulse/app/api/projects/route.ts +121 -0
  13. package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
  14. package/pulse/app/api/sessions/[id]/route.ts +31 -0
  15. package/pulse/app/api/sessions/route.ts +112 -0
  16. package/pulse/app/api/settings/route.ts +14 -0
  17. package/pulse/app/api/stats/route.ts +143 -0
  18. package/pulse/app/api/todos/route.ts +9 -0
  19. package/pulse/app/api/tools/route.ts +160 -0
  20. package/pulse/app/costs/page.tsx +179 -0
  21. package/pulse/app/export/page.tsx +465 -0
  22. package/pulse/app/favicon.ico +0 -0
  23. package/pulse/app/globals.css +263 -0
  24. package/pulse/app/help/page.tsx +142 -0
  25. package/pulse/app/history/page.tsx +157 -0
  26. package/pulse/app/layout.tsx +46 -0
  27. package/pulse/app/memory/page.tsx +365 -0
  28. package/pulse/app/overview-client.tsx +393 -0
  29. package/pulse/app/page.tsx +14 -0
  30. package/pulse/app/plans/page.tsx +308 -0
  31. package/pulse/app/projects/[slug]/page.tsx +390 -0
  32. package/pulse/app/projects/page.tsx +110 -0
  33. package/pulse/app/sessions/[id]/page.tsx +243 -0
  34. package/pulse/app/sessions/page.tsx +39 -0
  35. package/pulse/app/settings/page.tsx +188 -0
  36. package/pulse/app/todos/page.tsx +211 -0
  37. package/pulse/app/tools/page.tsx +249 -0
  38. package/pulse/cli.js +159 -0
  39. package/pulse/components/activity/day-of-week-chart.tsx +35 -0
  40. package/pulse/components/activity/streak-card.tsx +36 -0
  41. package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
  42. package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
  43. package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
  44. package/pulse/components/costs/model-token-table.tsx +60 -0
  45. package/pulse/components/global-search.tsx +193 -0
  46. package/pulse/components/keyboard-nav-provider.tsx +23 -0
  47. package/pulse/components/layout/bottom-nav.tsx +52 -0
  48. package/pulse/components/layout/client-layout.tsx +31 -0
  49. package/pulse/components/layout/sidebar-context.tsx +50 -0
  50. package/pulse/components/layout/sidebar.tsx +182 -0
  51. package/pulse/components/layout/top-bar.tsx +121 -0
  52. package/pulse/components/overview/activity-heatmap.tsx +107 -0
  53. package/pulse/components/overview/conversation-table.tsx +148 -0
  54. package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
  55. package/pulse/components/overview/peak-hours-chart.tsx +87 -0
  56. package/pulse/components/overview/project-activity-donut.tsx +96 -0
  57. package/pulse/components/overview/stat-card.tsx +102 -0
  58. package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
  59. package/pulse/components/projects/project-card.tsx +175 -0
  60. package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
  61. package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
  62. package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
  63. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
  64. package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
  65. package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
  66. package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
  67. package/pulse/components/sessions/session-badges.tsx +49 -0
  68. package/pulse/components/sessions/session-table.tsx +299 -0
  69. package/pulse/components/theme-provider.tsx +44 -0
  70. package/pulse/components/tools/feature-adoption-table.tsx +58 -0
  71. package/pulse/components/tools/mcp-server-panel.tsx +45 -0
  72. package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
  73. package/pulse/components/tools/version-history-table.tsx +32 -0
  74. package/pulse/components/ui/alert.tsx +66 -0
  75. package/pulse/components/ui/badge.tsx +48 -0
  76. package/pulse/components/ui/breadcrumb.tsx +109 -0
  77. package/pulse/components/ui/button.tsx +64 -0
  78. package/pulse/components/ui/calendar.tsx +220 -0
  79. package/pulse/components/ui/card.tsx +92 -0
  80. package/pulse/components/ui/command.tsx +158 -0
  81. package/pulse/components/ui/dialog.tsx +158 -0
  82. package/pulse/components/ui/input.tsx +21 -0
  83. package/pulse/components/ui/popover.tsx +89 -0
  84. package/pulse/components/ui/progress.tsx +31 -0
  85. package/pulse/components/ui/select.tsx +190 -0
  86. package/pulse/components/ui/separator.tsx +28 -0
  87. package/pulse/components/ui/sheet.tsx +143 -0
  88. package/pulse/components/ui/skeleton.tsx +13 -0
  89. package/pulse/components/ui/table.tsx +116 -0
  90. package/pulse/components/ui/tabs.tsx +91 -0
  91. package/pulse/components/ui/tooltip.tsx +57 -0
  92. package/pulse/components/use-global-keyboard-nav.ts +79 -0
  93. package/pulse/components.json +23 -0
  94. package/pulse/eslint.config.mjs +18 -0
  95. package/pulse/lib/claude-reader.ts +594 -0
  96. package/pulse/lib/decode.ts +129 -0
  97. package/pulse/lib/pricing.ts +102 -0
  98. package/pulse/lib/replay-parser.ts +165 -0
  99. package/pulse/lib/tool-categories.ts +127 -0
  100. package/pulse/lib/utils.ts +6 -0
  101. package/pulse/next-env.d.ts +6 -0
  102. package/pulse/next.config.ts +16 -0
  103. package/pulse/package.json +45 -0
  104. package/pulse/postcss.config.mjs +7 -0
  105. package/pulse/public/activity.png +0 -0
  106. package/pulse/public/cc-lens.png +0 -0
  107. package/pulse/public/command-k.png +0 -0
  108. package/pulse/public/costs.png +0 -0
  109. package/pulse/public/dashboard-dark.png +0 -0
  110. package/pulse/public/dashboard-white.png +0 -0
  111. package/pulse/public/export.png +0 -0
  112. package/pulse/public/file.svg +1 -0
  113. package/pulse/public/globe.svg +1 -0
  114. package/pulse/public/next.svg +1 -0
  115. package/pulse/public/projects.png +0 -0
  116. package/pulse/public/session-chat.png +0 -0
  117. package/pulse/public/todos.png +0 -0
  118. package/pulse/public/tools.png +0 -0
  119. package/pulse/public/vercel.svg +1 -0
  120. package/pulse/public/window.svg +1 -0
  121. package/pulse/tsconfig.json +34 -0
  122. package/pulse/types/claude.ts +294 -0
  123. package/src/agents/loader.mjs +89 -0
  124. package/src/agents/parser.mjs +98 -0
  125. package/src/agents/teams.mjs +123 -0
  126. package/src/auth/oauth.mjs +220 -0
  127. package/src/auth/tarang-auth.mjs +277 -0
  128. package/src/config/cli-args.mjs +173 -0
  129. package/src/config/env.mjs +263 -0
  130. package/src/config/settings.mjs +132 -0
  131. package/src/context/ast-parser.mjs +298 -0
  132. package/src/context/bm25.mjs +85 -0
  133. package/src/context/retriever.mjs +270 -0
  134. package/src/context/skeleton.mjs +134 -0
  135. package/src/core/agent-loop.mjs +480 -0
  136. package/src/core/approval.mjs +273 -0
  137. package/src/core/backend-url.mjs +57 -0
  138. package/src/core/cache.mjs +105 -0
  139. package/src/core/callback-client.mjs +149 -0
  140. package/src/core/checkpoints.mjs +142 -0
  141. package/src/core/context-manager.mjs +198 -0
  142. package/src/core/headless.mjs +168 -0
  143. package/src/core/hooks-manager.mjs +87 -0
  144. package/src/core/jsonl-writer.mjs +351 -0
  145. package/src/core/local-agent.mjs +429 -0
  146. package/src/core/local-store.mjs +325 -0
  147. package/src/core/mode-selector.mjs +51 -0
  148. package/src/core/output-filter.mjs +177 -0
  149. package/src/core/paths.mjs +101 -0
  150. package/src/core/pricing.mjs +314 -0
  151. package/src/core/providers.mjs +219 -0
  152. package/src/core/rate-limiter.mjs +119 -0
  153. package/src/core/safety.mjs +200 -0
  154. package/src/core/scheduler.mjs +173 -0
  155. package/src/core/session-manager.mjs +317 -0
  156. package/src/core/session.mjs +143 -0
  157. package/src/core/settings-sync.mjs +85 -0
  158. package/src/core/stagnation.mjs +57 -0
  159. package/src/core/stream-client.mjs +367 -0
  160. package/src/core/streaming.mjs +182 -0
  161. package/src/core/system-prompt.mjs +135 -0
  162. package/src/core/tool-executor.mjs +725 -0
  163. package/src/hooks/engine.mjs +162 -0
  164. package/src/index.mjs +370 -0
  165. package/src/mcp/client.mjs +253 -0
  166. package/src/mcp/transport-shttp.mjs +130 -0
  167. package/src/mcp/transport-sse.mjs +131 -0
  168. package/src/mcp/transport-ws.mjs +134 -0
  169. package/src/permissions/checker.mjs +57 -0
  170. package/src/permissions/command-classifier.mjs +573 -0
  171. package/src/permissions/injection-check.mjs +60 -0
  172. package/src/permissions/path-check.mjs +102 -0
  173. package/src/permissions/prompt.mjs +73 -0
  174. package/src/permissions/sandbox.mjs +112 -0
  175. package/src/plugins/loader.mjs +138 -0
  176. package/src/skills/loader.mjs +147 -0
  177. package/src/skills/runner.mjs +55 -0
  178. package/src/telemetry/index.mjs +96 -0
  179. package/src/terminal/agents.mjs +177 -0
  180. package/src/terminal/analytics.mjs +292 -0
  181. package/src/terminal/ansi.mjs +421 -0
  182. package/src/terminal/main.mjs +150 -0
  183. package/src/terminal/repl.mjs +1484 -0
  184. package/src/terminal/tool-display.mjs +58 -0
  185. package/src/tools/agent.mjs +137 -0
  186. package/src/tools/ask-user.mjs +61 -0
  187. package/src/tools/bash.mjs +148 -0
  188. package/src/tools/cron-create.mjs +120 -0
  189. package/src/tools/cron-delete.mjs +49 -0
  190. package/src/tools/cron-list.mjs +37 -0
  191. package/src/tools/edit.mjs +82 -0
  192. package/src/tools/enter-worktree.mjs +69 -0
  193. package/src/tools/exit-worktree.mjs +57 -0
  194. package/src/tools/glob.mjs +117 -0
  195. package/src/tools/grep.mjs +129 -0
  196. package/src/tools/lint.mjs +71 -0
  197. package/src/tools/ls.mjs +58 -0
  198. package/src/tools/lsp.mjs +115 -0
  199. package/src/tools/multi-edit.mjs +94 -0
  200. package/src/tools/notebook-edit.mjs +96 -0
  201. package/src/tools/read-mcp-resource.mjs +57 -0
  202. package/src/tools/read.mjs +138 -0
  203. package/src/tools/registry.mjs +132 -0
  204. package/src/tools/remote-trigger.mjs +84 -0
  205. package/src/tools/send-message.mjs +64 -0
  206. package/src/tools/skill.mjs +52 -0
  207. package/src/tools/test-runner.mjs +49 -0
  208. package/src/tools/todo-write.mjs +68 -0
  209. package/src/tools/tool-search.mjs +77 -0
  210. package/src/tools/web-fetch.mjs +65 -0
  211. package/src/tools/web-search.mjs +89 -0
  212. package/src/tools/write.mjs +55 -0
  213. package/src/ui/banner.mjs +237 -0
  214. package/src/ui/commands.mjs +499 -0
  215. package/src/ui/formatter.mjs +379 -0
  216. package/src/ui/markdown.mjs +278 -0
  217. package/src/ui/slash-commands.mjs +258 -0
  218. package/index.js +0 -1
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Telemetry Stub — basic telemetry interface (no actual reporting).
3
+ *
4
+ * Logs events to debug output when CLAUDE_CODE_DEBUG is set.
5
+ * Designed to be a drop-in interface for the full telemetry system.
6
+ */
7
+
8
+ const events = [];
9
+ let enabled = true;
10
+
11
+ /**
12
+ * Track a telemetry event.
13
+ * @param {string} event - event name
14
+ * @param {object} [properties] - event properties
15
+ */
16
+ export function track(event, properties = {}) {
17
+ if (!enabled) return;
18
+ if (process.env.CLAUDE_CODE_DISABLE_TELEMETRY === '1') return;
19
+
20
+ const entry = {
21
+ event,
22
+ properties,
23
+ timestamp: Date.now(),
24
+ };
25
+
26
+ events.push(entry);
27
+
28
+ // Keep max 1000 events in memory
29
+ if (events.length > 1000) {
30
+ events.splice(0, events.length - 1000);
31
+ }
32
+
33
+ if (process.env.CLAUDE_CODE_DEBUG) {
34
+ console.error(`[telemetry] ${event}`, JSON.stringify(properties).slice(0, 200));
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Track a timing event.
40
+ * @param {string} event - event name
41
+ * @param {number} durationMs - duration in milliseconds
42
+ * @param {object} [properties] - additional properties
43
+ */
44
+ export function trackTiming(event, durationMs, properties = {}) {
45
+ track(event, { ...properties, durationMs });
46
+ }
47
+
48
+ /**
49
+ * Track an error.
50
+ * @param {string} event - error context
51
+ * @param {Error} error - the error
52
+ */
53
+ export function trackError(event, error) {
54
+ track(`error.${event}`, {
55
+ message: error.message,
56
+ stack: error.stack?.split('\n').slice(0, 3).join('\n'),
57
+ });
58
+ }
59
+
60
+ /**
61
+ * Get collected events (for debugging).
62
+ * @returns {Array}
63
+ */
64
+ export function getEvents() {
65
+ return [...events];
66
+ }
67
+
68
+ /**
69
+ * Clear collected events.
70
+ */
71
+ export function clear() {
72
+ events.length = 0;
73
+ }
74
+
75
+ /**
76
+ * Enable or disable telemetry.
77
+ * @param {boolean} value
78
+ */
79
+ export function setEnabled(value) {
80
+ enabled = value;
81
+ }
82
+
83
+ /**
84
+ * Get telemetry stats.
85
+ */
86
+ export function getStats() {
87
+ const counts = {};
88
+ for (const e of events) {
89
+ counts[e.event] = (counts[e.event] || 0) + 1;
90
+ }
91
+ return {
92
+ totalEvents: events.length,
93
+ enabled,
94
+ eventCounts: counts,
95
+ };
96
+ }
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Built-in Agents — specialized agent modes invoked via slash commands.
3
+ *
4
+ * Each agent wraps the same backend SSE flow but with a specialized
5
+ * system prompt prefix that focuses the AI on a specific task type.
6
+ *
7
+ * Agents:
8
+ * - explore: Code explorer — traces execution paths, maps architecture
9
+ * - review: Code reviewer — finds bugs, security issues, quality problems
10
+ * - architect: Feature architect — designs implementations, file plans
11
+ */
12
+
13
+ import { c, spinner, inPlace, hr } from './ansi.mjs';
14
+ import { TarangStreamClient } from '../core/stream-client.mjs';
15
+ import { resolveBackendUrl } from '../core/backend-url.mjs';
16
+
17
+ // ── Agent Definitions ────────────────────────────────────────
18
+
19
+ export const BUILTIN_AGENTS = [
20
+ {
21
+ command: 'explore',
22
+ name: 'Code Explorer',
23
+ description: 'Deeply analyze codebase features and architecture',
24
+ detail: 'Traces execution paths, maps layers, documents dependencies',
25
+ icon: '🔭',
26
+ systemPrompt: `You are a Code Explorer agent. Your job is to deeply analyze the codebase to answer the user's question.
27
+
28
+ Your approach:
29
+ 1. Start by understanding what the user wants to know
30
+ 2. Search for relevant files using search_code and list_files
31
+ 3. Read the key files to trace the execution path
32
+ 4. Map the architecture layers (entry points → business logic → data layer)
33
+ 5. Document dependencies between components
34
+ 6. Provide a clear, structured answer with file references
35
+
36
+ Rules:
37
+ - ONLY use read-only tools: read_file, search_code, list_files, search_files, get_file_info
38
+ - NEVER modify any files
39
+ - NEVER run shell commands that modify state
40
+ - Include file paths and line numbers in your references
41
+ - Structure your response with clear sections: Overview, Key Files, Execution Flow, Dependencies
42
+ - Be thorough but concise — focus on what matters for the user's question`,
43
+ readOnly: true,
44
+ },
45
+ {
46
+ command: 'review',
47
+ name: 'Code Reviewer',
48
+ description: 'Review code for bugs, security issues, and quality',
49
+ detail: 'Scans for OWASP top 10, logic errors, code smells',
50
+ icon: '🔍',
51
+ systemPrompt: `You are a Code Review agent. Your job is to review code for issues.
52
+
53
+ Your approach:
54
+ 1. Read the files or directories the user specifies
55
+ 2. Check for:
56
+ - Security vulnerabilities (OWASP top 10: injection, XSS, auth bypass, etc.)
57
+ - Logic errors and edge cases
58
+ - Error handling gaps
59
+ - Performance issues (N+1 queries, memory leaks, blocking operations)
60
+ - Code quality (naming, complexity, duplication)
61
+ 3. Rate each finding by severity: CRITICAL / HIGH / MEDIUM / LOW
62
+ 4. Provide specific fix suggestions with code
63
+
64
+ Rules:
65
+ - ONLY use read-only tools: read_file, search_code, list_files, search_files
66
+ - NEVER modify any files
67
+ - Focus on HIGH and CRITICAL issues first
68
+ - Include file:line references
69
+ - Structure: Summary → Critical Issues → Other Findings → Recommendations
70
+ - Be specific — "line 42 has SQL injection via string interpolation" not "check for SQL injection"`,
71
+ readOnly: true,
72
+ },
73
+ {
74
+ command: 'architect',
75
+ name: 'Feature Architect',
76
+ description: 'Design feature implementations with file plans',
77
+ detail: 'Analyzes patterns, designs components, maps data flows',
78
+ icon: '📐',
79
+ systemPrompt: `You are a Feature Architect agent. Your job is to design how a feature should be implemented.
80
+
81
+ Your approach:
82
+ 1. Understand the feature requirements from the user
83
+ 2. Analyze existing codebase patterns and conventions:
84
+ - File organization and naming
85
+ - Import patterns and module structure
86
+ - Error handling patterns
87
+ - Testing patterns
88
+ 3. Design the implementation:
89
+ - List all files to create/modify
90
+ - Component/module design with interfaces
91
+ - Data flow (request → processing → response)
92
+ - Database schema changes if needed
93
+ 4. Provide implementation order (what to build first)
94
+
95
+ Rules:
96
+ - ONLY use read-only tools: read_file, search_code, list_files, search_files
97
+ - NEVER modify any files — you DESIGN, you don't implement
98
+ - Follow existing project conventions
99
+ - Structure: Requirements → Architecture → File Plan → Implementation Order → Risks
100
+ - Include code sketches for key interfaces
101
+ - Call out edge cases and potential pitfalls`,
102
+ readOnly: true,
103
+ },
104
+ ];
105
+
106
+ // ── Agent Runner ─────────────────────────────────────────────
107
+
108
+ /**
109
+ * Run a built-in agent with the given instruction.
110
+ * @param {string} agentName - e.g. 'explore', 'review', 'architect'
111
+ * @param {string} instruction - User's instruction
112
+ * @param {Object} ctx - { auth, toolExecutor, approval }
113
+ * @param {Object} session - Session state
114
+ * @param {Function} renderEvent - Event renderer function
115
+ */
116
+ export async function runAgent(agentName, instruction, ctx, session, renderEvent) {
117
+ const agent = BUILTIN_AGENTS.find(a => a.command === agentName);
118
+ if (!agent) {
119
+ process.stderr.write(` ${c.red('Unknown agent: ' + agentName)}\n`);
120
+ return;
121
+ }
122
+
123
+ const creds = ctx.auth.loadCredentials();
124
+ if (!creds.token) {
125
+ process.stderr.write(` ${c.red('Not logged in. Run /login first.')}\n`);
126
+ return;
127
+ }
128
+
129
+ // Header
130
+ process.stderr.write(`\n ${agent.icon} ${c.bold(c.brand(agent.name))}\n`);
131
+ process.stderr.write(` ${c.gray('─'.repeat(40))}\n`);
132
+ process.stderr.write(` ${c.gray(instruction)}\n\n`);
133
+
134
+ // Prepend agent system prompt to instruction
135
+ const fullInstruction = `${agent.systemPrompt}\n\n---\n\nUser request: ${instruction}`;
136
+
137
+ // For read-only agents, use a restricted approval manager
138
+ const { ApprovalManager } = await import('../core/approval.mjs');
139
+ const agentApproval = agent.readOnly
140
+ ? new ApprovalManager({ planMode: true }) // planMode blocks all writes
141
+ : ctx.approval;
142
+
143
+ const client = new TarangStreamClient({
144
+ baseUrl: creds.backendUrl,
145
+ token: creds.token,
146
+ toolExecutor: ctx.toolExecutor,
147
+ approvalManager: agentApproval,
148
+ });
149
+
150
+ session.turns++;
151
+ session.toolCalls = 0;
152
+ let assistantContent = '';
153
+
154
+ try {
155
+ for await (const event of client.execute(fullInstruction, { cwd: process.cwd() })) {
156
+ renderEvent(event);
157
+
158
+ if (event.type === 'content' || event.type === 'content_partial') {
159
+ const text = event.data?.text || '';
160
+ if (text) assistantContent = text;
161
+ }
162
+ }
163
+ } catch (err) {
164
+ inPlace('');
165
+ process.stderr.write(` ${c.red('Agent error: ' + err.message)}\n`);
166
+ }
167
+
168
+ // Save to conversation history
169
+ if (assistantContent) {
170
+ session.history.push(
171
+ { role: 'user', content: `[${agent.name}] ${instruction}` },
172
+ { role: 'assistant', content: assistantContent }
173
+ );
174
+ }
175
+
176
+ process.stderr.write('\n');
177
+ }
@@ -0,0 +1,292 @@
1
+ import * as http from 'node:http';
2
+ import * as path from 'node:path';
3
+ import { spawn } from 'node:child_process';
4
+ import { createRequire } from 'node:module';
5
+
6
+ import {
7
+ getRecentSessions,
8
+ getSessionStats,
9
+ getToolBreakdown,
10
+ getModelBreakdown,
11
+ getHistory,
12
+ getSessionDetail,
13
+ getStorePaths,
14
+ } from '../core/local-store.mjs';
15
+
16
+ const require = createRequire(import.meta.url);
17
+ const { version } = require('../../package.json');
18
+
19
+ function parseNumber(value, fallback) {
20
+ const num = Number.parseInt(value, 10);
21
+ return Number.isFinite(num) && num >= 0 ? num : fallback;
22
+ }
23
+
24
+ function readOption(args, name, fallback = null) {
25
+ const idx = args.indexOf(name);
26
+ if (idx === -1 || idx === args.length - 1) return fallback;
27
+ return args[idx + 1];
28
+ }
29
+
30
+ function hasFlag(args, name) {
31
+ return args.includes(name);
32
+ }
33
+
34
+ function basename(projectPath) {
35
+ return projectPath ? path.basename(projectPath) : 'unknown';
36
+ }
37
+
38
+ function formatNumber(num) {
39
+ return new Intl.NumberFormat('en-US').format(num || 0);
40
+ }
41
+
42
+ function formatWhen(value) {
43
+ if (!value) return 'n/a';
44
+ const date = new Date(value);
45
+ return Number.isNaN(date.getTime()) ? 'n/a' : date.toLocaleString();
46
+ }
47
+
48
+ function formatDuration(startTime, endTime) {
49
+ if (!startTime || !endTime) return 'n/a';
50
+ const ms = new Date(endTime).getTime() - new Date(startTime).getTime();
51
+ if (!Number.isFinite(ms) || ms < 0) return 'n/a';
52
+ if (ms < 1000) return `${ms}ms`;
53
+ const seconds = Math.round(ms / 100) / 10;
54
+ if (seconds < 60) return `${seconds}s`;
55
+ const minutes = Math.floor(seconds / 60);
56
+ const rem = Math.round((seconds % 60) * 10) / 10;
57
+ return `${minutes}m ${rem}s`;
58
+ }
59
+
60
+ function truncate(text, max = 80) {
61
+ if (!text) return '';
62
+ return text.length > max ? `${text.slice(0, max - 3)}...` : text;
63
+ }
64
+
65
+ function relativeDaysLabel(days) {
66
+ return days === 0 ? 'all time' : `last ${days} days`;
67
+ }
68
+
69
+ function extractPrompt(entry) {
70
+ return truncate(entry?.display || '', 120);
71
+ }
72
+
73
+ export function formatSessionsReport(sessions, limit) {
74
+ if (!sessions.length) {
75
+ return 'No local sessions found in ~/.kepler/projects.\n';
76
+ }
77
+
78
+ const lines = [];
79
+ lines.push(`KEPLER SESSIONS ${sessions.length} shown`);
80
+ lines.push(`Recent local transcripts from ~/.kepler/projects (limit ${limit})`);
81
+ lines.push('');
82
+
83
+ for (const session of sessions) {
84
+ const model = session.models[0] || 'backend default';
85
+ const totalTokens = (session.inputTokens || 0) + (session.outputTokens || 0);
86
+ lines.push(`${formatWhen(session.endTime || session.startTime)} ${basename(session.project)}`);
87
+ lines.push(` session ${session.sessionId}`);
88
+ lines.push(` prompt ${truncate(session.firstPrompt || '(no prompt captured)', 96)}`);
89
+ lines.push(` usage ${formatNumber(totalTokens)} tok ${formatNumber(session.toolCalls.reduce((sum, tool) => sum + tool.count, 0))} tools ${session.userMessages} user / ${session.assistantMessages} assistant`);
90
+ lines.push(` model ${model}${session.gitBranch ? ` git:${session.gitBranch}` : ''}`);
91
+ lines.push('');
92
+ }
93
+
94
+ return lines.join('\n');
95
+ }
96
+
97
+ export function formatStatsReport(stats, tools, models, days, paths) {
98
+ const lines = [];
99
+ lines.push(`KEPLER STATS ${relativeDaysLabel(days)}`);
100
+ lines.push(`Store: ${paths.keplerDir}`);
101
+ lines.push('');
102
+ lines.push(`Sessions ${formatNumber(stats.totalSessions)}`);
103
+ lines.push(`Messages ${formatNumber(stats.totalUserMessages + stats.totalAssistantMessages)} (${formatNumber(stats.totalUserMessages)} user, ${formatNumber(stats.totalAssistantMessages)} assistant)`);
104
+ lines.push(`Tokens ${formatNumber(stats.totalInputTokens + stats.totalOutputTokens)} (${formatNumber(stats.totalInputTokens)} in, ${formatNumber(stats.totalOutputTokens)} out)`);
105
+ lines.push(`Cache Read ${formatNumber(stats.totalCacheReadTokens)}`);
106
+ lines.push(`Tool Calls ${formatNumber(stats.totalToolCalls)}`);
107
+ lines.push('');
108
+
109
+ lines.push('Top Tools');
110
+ if (tools.length === 0) {
111
+ lines.push(' none');
112
+ } else {
113
+ for (const tool of tools.slice(0, 8)) {
114
+ lines.push(` ${tool.name.padEnd(18)} ${formatNumber(tool.count)}`);
115
+ }
116
+ }
117
+ lines.push('');
118
+
119
+ lines.push('Models');
120
+ if (models.length === 0) {
121
+ lines.push(' none');
122
+ } else {
123
+ for (const model of models.slice(0, 8)) {
124
+ lines.push(` ${truncate(model.model, 42).padEnd(42)} ${formatNumber(model.sessions)} sessions`);
125
+ }
126
+ }
127
+ lines.push('');
128
+
129
+ return lines.join('\n');
130
+ }
131
+
132
+ export function formatHistoryReport(entries, limit) {
133
+ if (!entries.length) {
134
+ return 'No local prompt history found in ~/.kepler/history.jsonl.\n';
135
+ }
136
+
137
+ const lines = [];
138
+ lines.push(`KEPLER HISTORY ${entries.length} shown`);
139
+ lines.push(`Recent prompts from ~/.kepler/history.jsonl (limit ${limit})`);
140
+ lines.push('');
141
+
142
+ for (const entry of entries) {
143
+ lines.push(`${formatWhen(entry.timestamp)} ${basename(entry.project)}`);
144
+ lines.push(` ${extractPrompt(entry)}`);
145
+ lines.push('');
146
+ }
147
+
148
+ return lines.join('\n');
149
+ }
150
+
151
+ function json(res, status, payload) {
152
+ res.writeHead(status, {
153
+ 'Content-Type': 'application/json; charset=utf-8',
154
+ 'Cache-Control': 'no-store',
155
+ });
156
+ res.end(JSON.stringify(payload));
157
+ }
158
+
159
+ function escapeHtml(value) {
160
+ return String(value)
161
+ .replace(/&/g, '&amp;')
162
+ .replace(/</g, '&lt;')
163
+ .replace(/>/g, '&gt;')
164
+ .replace(/"/g, '&quot;');
165
+ }
166
+
167
+ function openBrowser(url) {
168
+ try {
169
+ let child;
170
+ if (process.platform === 'darwin') {
171
+ child = spawn('open', [url], { stdio: 'ignore', detached: true });
172
+ } else if (process.platform === 'win32') {
173
+ child = spawn('cmd', ['/c', 'start', '', url], { stdio: 'ignore', detached: true });
174
+ } else {
175
+ child = spawn('xdg-open', [url], { stdio: 'ignore', detached: true });
176
+ }
177
+ child.unref();
178
+ return true;
179
+ } catch {
180
+ return false;
181
+ }
182
+ }
183
+
184
+ export async function runSessionsCommand(args = []) {
185
+ const limit = parseNumber(readOption(args, '--limit', '20'), 20);
186
+ const sessions = getRecentSessions(limit);
187
+ process.stdout.write(formatSessionsReport(sessions, limit));
188
+ }
189
+
190
+ export async function runStatsCommand(args = []) {
191
+ const days = parseNumber(readOption(args, '--days', '30'), 30);
192
+ const stats = getSessionStats(days);
193
+ const tools = getToolBreakdown(days);
194
+ const models = getModelBreakdown(days);
195
+ const paths = getStorePaths();
196
+ process.stdout.write(formatStatsReport(stats, tools, models, days, paths));
197
+ }
198
+
199
+ export async function runHistoryCommand(args = []) {
200
+ const limit = parseNumber(readOption(args, '--limit', '50'), 50);
201
+ const history = getHistory(limit);
202
+ process.stdout.write(formatHistoryReport(history, limit));
203
+ }
204
+
205
+ // Dashboard removed — now using Kepler Pulse (pulse/cli.js)
206
+
207
+ export async function _runDashboardCommand_REMOVED() { /* see pulse/cli.js */ }
208
+
209
+ export async function _OLD_runDashboardCommand(args = []) {
210
+ const requestedPort = parseNumber(readOption(args, '--port', '4318'), 4318);
211
+ const host = readOption(args, '--host', '127.0.0.1') || '127.0.0.1';
212
+ const shouldOpen = !hasFlag(args, '--no-open');
213
+
214
+ const server = http.createServer(async (req, res) => {
215
+ try {
216
+ const url = new URL(req.url || '/', `http://${req.headers.host || `${host}:${requestedPort}`}`);
217
+
218
+ if (url.pathname === '/') {
219
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
220
+ res.end(dashboardHtml());
221
+ return;
222
+ }
223
+
224
+ if (url.pathname === '/api/overview') {
225
+ const days = parseNumber(url.searchParams.get('days') || '30', 30);
226
+ const limit = parseNumber(url.searchParams.get('limit') || '18', 18);
227
+ const historyLimit = parseNumber(url.searchParams.get('history_limit') || '40', 40);
228
+ const [stats, tools, models, sessions] = await Promise.all([
229
+ getSessionStats(days),
230
+ getToolBreakdown(days),
231
+ getModelBreakdown(days),
232
+ getRecentSessions(limit),
233
+ ]);
234
+ json(res, 200, {
235
+ stats,
236
+ tools,
237
+ models,
238
+ sessions,
239
+ history: getHistory(historyLimit),
240
+ paths: getStorePaths(),
241
+ });
242
+ return;
243
+ }
244
+
245
+ if (url.pathname === '/api/session') {
246
+ const sessionId = url.searchParams.get('session_id') || '';
247
+ if (!sessionId) {
248
+ json(res, 400, { error: 'session_id is required' });
249
+ return;
250
+ }
251
+ const detail = await getSessionDetail(sessionId);
252
+ if (!detail) {
253
+ json(res, 404, { error: 'Session not found' });
254
+ return;
255
+ }
256
+ json(res, 200, detail);
257
+ return;
258
+ }
259
+
260
+ json(res, 404, { error: 'Not found' });
261
+ } catch (err) {
262
+ json(res, 500, { error: err.message || 'Internal error' });
263
+ }
264
+ });
265
+
266
+ await new Promise((resolve, reject) => {
267
+ server.once('error', reject);
268
+ server.listen(requestedPort, host, resolve);
269
+ });
270
+
271
+ const address = server.address();
272
+ const actualPort = typeof address === 'object' && address ? address.port : requestedPort;
273
+ const url = `http://${host}:${actualPort}`;
274
+
275
+ process.stderr.write(`\x1b[36mKepler dashboard\x1b[0m ${url}\n`);
276
+ process.stderr.write(`\x1b[2mReading local analytics from ${getStorePaths().keplerDir}\x1b[0m\n`);
277
+ process.stderr.write(`\x1b[2mPress Ctrl+C to stop the dashboard server.\x1b[0m\n`);
278
+
279
+ if (shouldOpen) {
280
+ const opened = openBrowser(url);
281
+ if (!opened) {
282
+ process.stderr.write(`\x1b[33mUnable to open a browser automatically. Open ${url} manually.\x1b[0m\n`);
283
+ }
284
+ }
285
+
286
+ const shutdown = () => {
287
+ server.close(() => process.exit(0));
288
+ };
289
+
290
+ process.on('SIGINT', shutdown);
291
+ process.on('SIGTERM', shutdown);
292
+ }