@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,325 @@
1
+ /**
2
+ * Local Store Reader — scans ~/.kepler/ JSONL files for historical stats.
3
+ *
4
+ * Provides read helpers for CLI commands (/stats, /history, /tokens, /tools, /sessions).
5
+ * All data comes from local JSONL files — no cloud dependency.
6
+ */
7
+
8
+ import * as fs from 'node:fs';
9
+ import * as path from 'node:path';
10
+ import * as os from 'node:os';
11
+ import * as readline from 'node:readline';
12
+
13
+ const KEPLER_DIR = process.env.KEPLER_HOME || path.join(os.homedir(), '.kepler');
14
+ const PROJECTS_DIR = path.join(KEPLER_DIR, 'projects');
15
+
16
+ function normalizeBlock(block) {
17
+ if (!block || typeof block !== 'object') {
18
+ return { type: 'unknown', value: String(block ?? '') };
19
+ }
20
+ if (block.type === 'text') {
21
+ return { type: 'text', text: block.text || '' };
22
+ }
23
+ if (block.type === 'tool_use') {
24
+ return {
25
+ type: 'tool_use',
26
+ id: block.id || null,
27
+ name: block.name || 'unknown',
28
+ input: block.input || {},
29
+ };
30
+ }
31
+ if (block.type === 'tool_result') {
32
+ return {
33
+ type: 'tool_result',
34
+ tool_use_id: block.tool_use_id || null,
35
+ content: typeof block.content === 'string' ? block.content : JSON.stringify(block.content || ''),
36
+ is_error: !!block.is_error,
37
+ };
38
+ }
39
+ return { ...block };
40
+ }
41
+
42
+ function normalizeMessageContent(content) {
43
+ if (typeof content === 'string') return content;
44
+ if (Array.isArray(content)) return content.map(normalizeBlock);
45
+ if (content == null) return '';
46
+ try {
47
+ return JSON.stringify(content);
48
+ } catch {
49
+ return String(content);
50
+ }
51
+ }
52
+
53
+ /**
54
+ * List all session JSONL files across all projects.
55
+ * Returns [{slug, sessionId, filePath, mtime}] sorted by mtime desc.
56
+ */
57
+ function listSessionFiles() {
58
+ const results = [];
59
+ try {
60
+ const slugs = fs.readdirSync(PROJECTS_DIR);
61
+ for (const slug of slugs) {
62
+ const slugDir = path.join(PROJECTS_DIR, slug);
63
+ if (!fs.statSync(slugDir).isDirectory()) continue;
64
+ const files = fs.readdirSync(slugDir).filter(f => f.endsWith('.jsonl'));
65
+ for (const file of files) {
66
+ const filePath = path.join(slugDir, file);
67
+ const stat = fs.statSync(filePath);
68
+ results.push({
69
+ slug,
70
+ sessionId: file.replace('.jsonl', ''),
71
+ filePath,
72
+ mtime: stat.mtimeMs,
73
+ });
74
+ }
75
+ }
76
+ } catch { /* projects dir may not exist yet */ }
77
+ results.sort((a, b) => b.mtime - a.mtime);
78
+ return results;
79
+ }
80
+
81
+ function findSessionFile(sessionId) {
82
+ return listSessionFiles().find((entry) => entry.sessionId === sessionId) || null;
83
+ }
84
+
85
+ /**
86
+ * Parse a session JSONL file and extract metadata.
87
+ * Reads line-by-line (streaming) to handle large files.
88
+ */
89
+ async function parseSessionMeta(filePath) {
90
+ const meta = {
91
+ sessionId: null,
92
+ project: null,
93
+ firstPrompt: null,
94
+ userMessages: 0,
95
+ assistantMessages: 0,
96
+ inputTokens: 0,
97
+ outputTokens: 0,
98
+ cacheReadTokens: 0,
99
+ cacheCreationTokens: 0,
100
+ toolCalls: [], // [{name, count}]
101
+ models: [], // [model strings]
102
+ startTime: null,
103
+ endTime: null,
104
+ gitBranch: null,
105
+ };
106
+
107
+ const toolCounts = {};
108
+ const modelSet = new Set();
109
+
110
+ try {
111
+ const fileStream = fs.createReadStream(filePath, { encoding: 'utf-8' });
112
+ const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
113
+
114
+ for await (const line of rl) {
115
+ if (!line.trim()) continue;
116
+ let obj;
117
+ try { obj = JSON.parse(line); } catch { continue; }
118
+
119
+ if (obj.sessionId && !meta.sessionId) meta.sessionId = obj.sessionId;
120
+ if (obj.cwd && !meta.project) meta.project = obj.cwd;
121
+ if (obj.gitBranch && !meta.gitBranch) meta.gitBranch = obj.gitBranch;
122
+
123
+ const ts = obj.timestamp;
124
+ if (ts) {
125
+ if (!meta.startTime || ts < meta.startTime) meta.startTime = ts;
126
+ if (!meta.endTime || ts > meta.endTime) meta.endTime = ts;
127
+ }
128
+
129
+ if (obj.type === 'user') {
130
+ meta.userMessages++;
131
+ // Capture first user prompt (string content only)
132
+ if (!meta.firstPrompt) {
133
+ const content = obj.message?.content;
134
+ if (typeof content === 'string' && content.length > 0) {
135
+ meta.firstPrompt = content.slice(0, 100);
136
+ }
137
+ }
138
+ }
139
+
140
+ if (obj.type === 'assistant') {
141
+ meta.assistantMessages++;
142
+ const usage = obj.message?.usage;
143
+ if (usage) {
144
+ meta.inputTokens += usage.input_tokens || 0;
145
+ meta.outputTokens += usage.output_tokens || 0;
146
+ meta.cacheReadTokens += usage.cache_read_input_tokens || 0;
147
+ meta.cacheCreationTokens += usage.cache_creation_input_tokens || 0;
148
+ }
149
+ const model = obj.message?.model;
150
+ if (model) modelSet.add(model);
151
+
152
+ // Count tool_use blocks
153
+ const content = obj.message?.content;
154
+ if (Array.isArray(content)) {
155
+ for (const block of content) {
156
+ if (block.type === 'tool_use' && block.name) {
157
+ toolCounts[block.name] = (toolCounts[block.name] || 0) + 1;
158
+ }
159
+ }
160
+ }
161
+ }
162
+ }
163
+ } catch { /* file read error — return partial meta */ }
164
+
165
+ meta.toolCalls = Object.entries(toolCounts)
166
+ .map(([name, count]) => ({ name, count }))
167
+ .sort((a, b) => b.count - a.count);
168
+ meta.models = [...modelSet];
169
+
170
+ return meta;
171
+ }
172
+
173
+ /**
174
+ * Get recent sessions with metadata.
175
+ * @param {number} n — max sessions to return
176
+ */
177
+ export async function getRecentSessions(n = 10) {
178
+ const files = listSessionFiles().slice(0, n);
179
+ const sessions = [];
180
+ for (const f of files) {
181
+ const meta = await parseSessionMeta(f.filePath);
182
+ sessions.push({
183
+ ...meta,
184
+ slug: f.slug,
185
+ mtime: f.mtime,
186
+ });
187
+ }
188
+ return sessions;
189
+ }
190
+
191
+ /**
192
+ * Return normalized entries for a single session transcript.
193
+ * @param {string} sessionId
194
+ */
195
+ export async function getSessionDetail(sessionId) {
196
+ const file = findSessionFile(sessionId);
197
+ if (!file) return null;
198
+
199
+ const entries = [];
200
+ const fileStream = fs.createReadStream(file.filePath, { encoding: 'utf-8' });
201
+ const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
202
+
203
+ for await (const line of rl) {
204
+ if (!line.trim()) continue;
205
+ let obj;
206
+ try {
207
+ obj = JSON.parse(line);
208
+ } catch {
209
+ continue;
210
+ }
211
+
212
+ const message = obj.message || {};
213
+ entries.push({
214
+ type: obj.type || null,
215
+ timestamp: obj.timestamp || null,
216
+ cwd: obj.cwd || null,
217
+ role: message.role || null,
218
+ model: message.model || null,
219
+ usage: message.usage || null,
220
+ content: normalizeMessageContent(message.content),
221
+ uuid: obj.uuid || null,
222
+ parentUuid: obj.parentUuid || null,
223
+ });
224
+ }
225
+
226
+ const meta = await parseSessionMeta(file.filePath);
227
+ return {
228
+ sessionId: file.sessionId,
229
+ slug: file.slug,
230
+ filePath: file.filePath,
231
+ mtime: file.mtime,
232
+ meta,
233
+ entries,
234
+ };
235
+ }
236
+
237
+ /**
238
+ * Aggregate stats across sessions within a date range.
239
+ * @param {number} days — look back this many days (0 = all time)
240
+ */
241
+ export async function getSessionStats(days = 30) {
242
+ const files = listSessionFiles();
243
+ const cutoff = days > 0 ? Date.now() - (days * 86400000) : 0;
244
+ const filtered = files.filter(f => f.mtime >= cutoff);
245
+
246
+ const stats = {
247
+ totalSessions: filtered.length,
248
+ totalUserMessages: 0,
249
+ totalAssistantMessages: 0,
250
+ totalInputTokens: 0,
251
+ totalOutputTokens: 0,
252
+ totalCacheReadTokens: 0,
253
+ totalToolCalls: 0,
254
+ toolBreakdown: {},
255
+ modelBreakdown: {},
256
+ };
257
+
258
+ for (const f of filtered) {
259
+ const meta = await parseSessionMeta(f.filePath);
260
+ stats.totalUserMessages += meta.userMessages;
261
+ stats.totalAssistantMessages += meta.assistantMessages;
262
+ stats.totalInputTokens += meta.inputTokens;
263
+ stats.totalOutputTokens += meta.outputTokens;
264
+ stats.totalCacheReadTokens += meta.cacheReadTokens;
265
+
266
+ for (const tc of meta.toolCalls) {
267
+ stats.toolBreakdown[tc.name] = (stats.toolBreakdown[tc.name] || 0) + tc.count;
268
+ stats.totalToolCalls += tc.count;
269
+ }
270
+ for (const model of meta.models) {
271
+ stats.modelBreakdown[model] = (stats.modelBreakdown[model] || 0) + 1;
272
+ }
273
+ }
274
+
275
+ return stats;
276
+ }
277
+
278
+ /**
279
+ * Get tool breakdown ranked by usage.
280
+ * @param {number} days — look back period
281
+ */
282
+ export async function getToolBreakdown(days = 30) {
283
+ const stats = await getSessionStats(days);
284
+ return Object.entries(stats.toolBreakdown)
285
+ .map(([name, count]) => ({ name, count }))
286
+ .sort((a, b) => b.count - a.count);
287
+ }
288
+
289
+ /**
290
+ * Get model breakdown with session counts.
291
+ * @param {number} days — look back period
292
+ */
293
+ export async function getModelBreakdown(days = 30) {
294
+ const stats = await getSessionStats(days);
295
+ return Object.entries(stats.modelBreakdown)
296
+ .map(([model, sessions]) => ({ model, sessions }))
297
+ .sort((a, b) => b.sessions - a.sessions);
298
+ }
299
+
300
+ /**
301
+ * Read history.jsonl entries.
302
+ * @param {number} n — max entries to return (most recent first)
303
+ */
304
+ export function getHistory(n = 50) {
305
+ const historyPath = path.join(KEPLER_DIR, 'history.jsonl');
306
+ try {
307
+ const content = fs.readFileSync(historyPath, 'utf-8');
308
+ const lines = content.trim().split('\n').filter(Boolean);
309
+ const entries = [];
310
+ for (const line of lines) {
311
+ try { entries.push(JSON.parse(line)); } catch { /* skip bad lines */ }
312
+ }
313
+ return entries.slice(-n).reverse();
314
+ } catch {
315
+ return [];
316
+ }
317
+ }
318
+
319
+ export function getStorePaths() {
320
+ return {
321
+ keplerDir: KEPLER_DIR,
322
+ projectsDir: PROJECTS_DIR,
323
+ historyPath: path.join(KEPLER_DIR, 'history.jsonl'),
324
+ };
325
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Mode Selector
3
+ *
4
+ * remote (default): All requests go to Tarang backend.
5
+ * Backend handles orchestration, model selection, tool routing.
6
+ * User's provider and models configured via web Settings page.
7
+ *
8
+ * local: For local LLMs (Ollama, LM Studio, etc.)
9
+ * Direct API call, no backend. Only when user explicitly opts in.
10
+ */
11
+
12
+ let _probeCache = { available: null, timestamp: 0 };
13
+ const PROBE_CACHE_TTL = 60_000; // 60s
14
+
15
+ export async function selectMode(instruction, options, config) {
16
+ // Explicit --local flag: user wants local LLM
17
+ if (options.local) return 'local';
18
+
19
+ // Everything else goes to the backend
20
+ return 'remote';
21
+ }
22
+
23
+ export async function probeBackend(url) {
24
+ if (!url) return false;
25
+ const now = Date.now();
26
+ if (_probeCache.available !== null && (now - _probeCache.timestamp) < PROBE_CACHE_TTL) {
27
+ return _probeCache.available;
28
+ }
29
+ try {
30
+ const resp = await fetch(`${url}/health`, { signal: AbortSignal.timeout(2000) });
31
+ _probeCache = { available: resp.ok, timestamp: now };
32
+ return resp.ok;
33
+ } catch {
34
+ _probeCache = { available: false, timestamp: now };
35
+ return false;
36
+ }
37
+ }
38
+
39
+ export function classifyTask(instruction) {
40
+ if (!instruction) return 'simple';
41
+ const isSimple = SIMPLE_PATTERNS.some(p => p.test(instruction));
42
+ const isComplex = COMPLEX_PATTERNS.some(p => p.test(instruction));
43
+ if (isComplex && !isSimple) return 'complex';
44
+ if (isSimple && !isComplex) return 'simple';
45
+ return 'medium'; // ambiguous → default to remote when available
46
+ }
47
+
48
+ /** Reset probe cache (for testing). */
49
+ export function resetProbeCache() {
50
+ _probeCache = { available: null, timestamp: 0 };
51
+ }
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Output Filter — Smart shell output filtering + auto-lint.
3
+ * Ported from tarang-cli (Python) ws/executor.py with enhanced patterns.
4
+ */
5
+
6
+ import * as path from 'node:path';
7
+ import * as fs from 'node:fs';
8
+ import { execSync } from 'node:child_process';
9
+
10
+ // ── Command Classification ──────────────────────────────────
11
+
12
+ const COMMAND_PROFILES = {
13
+ install: {
14
+ patterns: [/pip install/i, /npm install/i, /yarn add/i, /pnpm add/i, /cargo add/i, /go get/i, /brew install/i, /apt install/i],
15
+ successLimit: 500,
16
+ failureLimit: 2000,
17
+ noisePatterns: [
18
+ /^Collecting \S+/,
19
+ /^Downloading \S+/,
20
+ /^Installing collected/,
21
+ /^Successfully installed/,
22
+ /^━+/, // Progress bars
23
+ /^\s*\d+%\s*\|/, // Percentage bars
24
+ /^Using cached/,
25
+ /^Requirement already satisfied/,
26
+ /^added \d+ packages?/,
27
+ /^up to date/,
28
+ /^npm WARN/,
29
+ /^npm notice/,
30
+ /^\s*$/, // Empty lines
31
+ ],
32
+ keepPatterns: [/error/i, /failed/i, /WARN(?:ING)?/i, /not found/i, /permission denied/i],
33
+ },
34
+ test: {
35
+ patterns: [/pytest/i, /npm test/i, /cargo test/i, /go test/i, /jest/i, /vitest/i, /mocha/i],
36
+ successLimit: 2000,
37
+ failureLimit: 8000,
38
+ noisePatterns: [
39
+ /^\.+$/, // Lines of dots (pytest progress)
40
+ /^PASSED/,
41
+ /^\s*✓/, // Checkmarks
42
+ /^\s*$/,
43
+ ],
44
+ keepPatterns: [/FAILED/i, /FAIL/i, /Error/i, /AssertionError/i, /Expected/i, /Actual/i, /✗/, /✘/],
45
+ },
46
+ build: {
47
+ patterns: [/npm run build/i, /cargo build/i, /go build/i, /tsc/i, /webpack/i, /vite build/i, /make\b/i, /next build/i],
48
+ successLimit: 1000,
49
+ failureLimit: 6000,
50
+ noisePatterns: [
51
+ /^Compiling \S+/,
52
+ /^Finished \S+ target/,
53
+ /^\s*$/,
54
+ ],
55
+ keepPatterns: [/error/i, /warning/i],
56
+ },
57
+ run: {
58
+ patterns: [/python\s/i, /node\s/i, /go run/i, /cargo run/i, /npm start/i, /npm run dev/i],
59
+ successLimit: 4000,
60
+ failureLimit: 8000,
61
+ noisePatterns: [],
62
+ keepPatterns: [],
63
+ },
64
+ default: {
65
+ patterns: [],
66
+ successLimit: 3000,
67
+ failureLimit: 6000,
68
+ noisePatterns: [/^\s*$/],
69
+ keepPatterns: [],
70
+ },
71
+ };
72
+
73
+ /** Detect shell command type for smart filtering. */
74
+ export function detectCommandType(command) {
75
+ if (!command) return 'default';
76
+ for (const [type, profile] of Object.entries(COMMAND_PROFILES)) {
77
+ if (type === 'default') continue;
78
+ if (profile.patterns.some(p => p.test(command))) return type;
79
+ }
80
+ return 'default';
81
+ }
82
+
83
+ // ── Output Filtering ────────────────────────────────────────
84
+
85
+ /**
86
+ * Filter shell output based on command type.
87
+ * Reduces noise from install/build while preserving errors and useful output.
88
+ *
89
+ * @param {string} output - Raw shell output
90
+ * @param {string} command - The command that was run
91
+ * @param {boolean} success - Whether the command succeeded
92
+ * @returns {{ output: string, commandType: string, truncated: boolean, originalLines: number, filteredLines: number }}
93
+ */
94
+ export function filterOutput(output, command, success = true) {
95
+ if (!output) return { output: '', commandType: 'default', truncated: false, originalLines: 0, filteredLines: 0 };
96
+
97
+ const type = detectCommandType(command);
98
+ const profile = COMMAND_PROFILES[type];
99
+ const limit = success ? profile.successLimit : profile.failureLimit;
100
+ const lines = output.split('\n');
101
+ const originalLines = lines.length;
102
+
103
+ let filteredLines = [];
104
+
105
+ for (const line of lines) {
106
+ // Always keep lines matching keep patterns (errors, failures)
107
+ const shouldKeep = profile.keepPatterns.length > 0 &&
108
+ profile.keepPatterns.some(p => p.test(line));
109
+
110
+ // Filter out noise patterns
111
+ const isNoise = !shouldKeep && profile.noisePatterns.length > 0 &&
112
+ profile.noisePatterns.some(p => p.test(line));
113
+
114
+ if (shouldKeep || !isNoise) {
115
+ filteredLines.push(line);
116
+ }
117
+ }
118
+
119
+ let filteredOutput = filteredLines.join('\n');
120
+ let truncated = false;
121
+
122
+ // Truncate to limit
123
+ if (filteredOutput.length > limit) {
124
+ filteredOutput = filteredOutput.slice(0, limit);
125
+ const lastNewline = filteredOutput.lastIndexOf('\n');
126
+ if (lastNewline > 0) {
127
+ filteredOutput = filteredOutput.slice(0, lastNewline);
128
+ }
129
+ filteredOutput += '\n... (truncated)';
130
+ truncated = true;
131
+ }
132
+
133
+ return {
134
+ output: filteredOutput,
135
+ commandType: type,
136
+ truncated,
137
+ originalLines,
138
+ filteredLines: filteredLines.length,
139
+ };
140
+ }
141
+
142
+ // ── Auto-Lint ───────────────────────────────────────────────
143
+
144
+ /** Auto-lint a file after write/edit. Returns lint output or null. */
145
+ export function autoLint(filePath) {
146
+ if (!filePath || !fs.existsSync(filePath)) return null;
147
+ const ext = path.extname(filePath);
148
+ let cmd;
149
+
150
+ try {
151
+ switch (ext) {
152
+ case '.py':
153
+ cmd = `python3 -m py_compile "${filePath}" 2>&1`;
154
+ break;
155
+ case '.js': case '.mjs': case '.cjs':
156
+ cmd = `node --check "${filePath}" 2>&1`;
157
+ break;
158
+ case '.ts': case '.tsx':
159
+ if (fs.existsSync('node_modules/.bin/tsc'))
160
+ cmd = `npx tsc --noEmit --pretty "${filePath}" 2>&1`;
161
+ break;
162
+ case '.go':
163
+ cmd = `go vet "${filePath}" 2>&1`;
164
+ break;
165
+ case '.rs':
166
+ cmd = `rustfmt --check "${filePath}" 2>&1`;
167
+ break;
168
+ }
169
+
170
+ if (!cmd) return null;
171
+ const output = execSync(cmd, { stdio: 'pipe', timeout: 15_000, encoding: 'utf-8' });
172
+ return output.trim() || null;
173
+ } catch (err) {
174
+ const output = (err.stderr || err.stdout || '').toString().trim();
175
+ return output || null;
176
+ }
177
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Kepler Paths — centralized path resolution for all Kepler data.
3
+ *
4
+ * Everything lives under ~/.kepler/:
5
+ * ~/.kepler/
6
+ * config.json — auth credentials + settings
7
+ * history.jsonl — prompt history
8
+ * hooks.json — global hooks
9
+ * conversations/ — conversation JSONL files
10
+ * projects/
11
+ * {hash}/ — per-project data (hash of project path)
12
+ * index/ — BM25 search index
13
+ * checkpoints/ — file undo checkpoints
14
+ * state.json — current session state
15
+ * sessions/ — session metadata archive
16
+ * hooks.json — project-specific hooks
17
+ * projects.json — slug → project path mapping
18
+ */
19
+
20
+ import * as fs from 'node:fs';
21
+ import * as path from 'node:path';
22
+ import * as os from 'node:os';
23
+ import * as crypto from 'node:crypto';
24
+
25
+ const KEPLER_HOME = process.env.KEPLER_HOME || process.env.ORCA_HOME || path.join(os.homedir(), '.kepler');
26
+
27
+ /**
28
+ * Hash a project path to a short directory name.
29
+ * Uses first 16 chars of SHA-256 (same as Claude Code).
30
+ */
31
+ export function projectHash(projectDir) {
32
+ // Resolve symlinks (macOS: /tmp → /private/tmp) so the hash is stable
33
+ let resolved = projectDir;
34
+ try {
35
+ resolved = fs.realpathSync(projectDir);
36
+ } catch {
37
+ // realpathSync fails if path doesn't exist yet — use as-is
38
+ }
39
+ return crypto.createHash('sha256')
40
+ .update(resolved)
41
+ .digest('hex')
42
+ .slice(0, 16);
43
+ }
44
+
45
+ /** Root ~/.kepler/ directory. */
46
+ export function keplerHome() {
47
+ return KEPLER_HOME;
48
+ }
49
+
50
+ /** @deprecated Use keplerHome() */
51
+ export const orcaHome = keplerHome;
52
+
53
+ /** ~/.kepler/projects/{hash}/ for a given project path. */
54
+ export function projectDir(projectPath) {
55
+ return path.join(KEPLER_HOME, 'projects', projectHash(projectPath));
56
+ }
57
+
58
+ /** ~/.kepler/projects/{hash}/index/ — BM25 search index. */
59
+ export function indexDir(projectPath) {
60
+ return path.join(projectDir(projectPath), 'index');
61
+ }
62
+
63
+ /** ~/.kepler/projects/{hash}/checkpoints/ — file undo. */
64
+ export function checkpointsDir(projectPath) {
65
+ return path.join(projectDir(projectPath), 'checkpoints');
66
+ }
67
+
68
+ /** ~/.kepler/projects/{hash}/state.json — current session. */
69
+ export function statePath(projectPath) {
70
+ return path.join(projectDir(projectPath), 'state.json');
71
+ }
72
+
73
+ /** ~/.kepler/projects/{hash}/sessions/ — session archive. */
74
+ export function sessionsDir(projectPath) {
75
+ return path.join(projectDir(projectPath), 'sessions');
76
+ }
77
+
78
+ /** ~/.kepler/projects/{hash}/hooks.json — project hooks. */
79
+ export function projectHooksPath(projectPath) {
80
+ return path.join(projectDir(projectPath), 'hooks.json');
81
+ }
82
+
83
+ /** ~/.kepler/conversations/ — central conversation storage. */
84
+ export function conversationsDir() {
85
+ return path.join(KEPLER_HOME, 'conversations');
86
+ }
87
+
88
+ /** ~/.kepler/conversations/{sessionId}.jsonl */
89
+ export function conversationPath(sessionId) {
90
+ return path.join(conversationsDir(), `${sessionId}.jsonl`);
91
+ }
92
+
93
+ /** ~/.kepler/hooks.json — global hooks. */
94
+ export function globalHooksPath() {
95
+ return path.join(KEPLER_HOME, 'hooks.json');
96
+ }
97
+
98
+ /** ~/.kepler/history.jsonl — prompt history. */
99
+ export function historyPath() {
100
+ return path.join(KEPLER_HOME, 'history.jsonl');
101
+ }