@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,142 @@
1
+ /**
2
+ * File Checkpointing — save and restore file state before edits.
3
+ *
4
+ * Before any file edit, a checkpoint is created containing the
5
+ * original file content. The /undo command restores the last checkpoint.
6
+ * Checkpoints are stored in ~/.kepler/projects/{hash}/checkpoints/
7
+ */
8
+
9
+ import fs from 'fs';
10
+ import path from 'path';
11
+ import crypto from 'crypto';
12
+ import { checkpointsDir } from './paths.mjs';
13
+
14
+ export class CheckpointManager {
15
+ /**
16
+ * @param {string} [baseDir] - project root directory
17
+ */
18
+ constructor(baseDir = process.cwd()) {
19
+ this.baseDir = baseDir;
20
+ this.checkpointDir = checkpointsDir(baseDir);
21
+ this.history = []; // Stack of checkpoint IDs
22
+ this.maxCheckpoints = 50;
23
+ }
24
+
25
+ /**
26
+ * Create a checkpoint for a file before editing.
27
+ * @param {string} filePath - absolute path to the file
28
+ * @returns {string|null} checkpoint ID, or null if file doesn't exist
29
+ */
30
+ save(filePath) {
31
+ const absPath = path.resolve(filePath);
32
+
33
+ try {
34
+ const content = fs.readFileSync(absPath, 'utf-8');
35
+ const id = `ckpt_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`;
36
+
37
+ fs.mkdirSync(this.checkpointDir, { recursive: true });
38
+
39
+ const checkpoint = {
40
+ id,
41
+ filePath: absPath,
42
+ relativePath: path.relative(this.baseDir, absPath),
43
+ content,
44
+ timestamp: new Date().toISOString(),
45
+ size: content.length,
46
+ };
47
+
48
+ const ckptFile = path.join(this.checkpointDir, `${id}.json`);
49
+ fs.writeFileSync(ckptFile, JSON.stringify(checkpoint));
50
+
51
+ this.history.push(id);
52
+
53
+ // Trim old checkpoints
54
+ while (this.history.length > this.maxCheckpoints) {
55
+ const old = this.history.shift();
56
+ try {
57
+ fs.unlinkSync(path.join(this.checkpointDir, `${old}.json`));
58
+ } catch {
59
+ // Already deleted
60
+ }
61
+ }
62
+
63
+ return id;
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Restore the most recent checkpoint (undo last edit).
71
+ * @returns {{ filePath: string, restored: boolean, id: string }|null}
72
+ */
73
+ undo() {
74
+ if (this.history.length === 0) return null;
75
+
76
+ const id = this.history.pop();
77
+ const ckptFile = path.join(this.checkpointDir, `${id}.json`);
78
+
79
+ try {
80
+ const raw = fs.readFileSync(ckptFile, 'utf-8');
81
+ const checkpoint = JSON.parse(raw);
82
+
83
+ fs.writeFileSync(checkpoint.filePath, checkpoint.content);
84
+ fs.unlinkSync(ckptFile);
85
+
86
+ return {
87
+ id: checkpoint.id,
88
+ filePath: checkpoint.filePath,
89
+ restored: true,
90
+ };
91
+ } catch (err) {
92
+ return { id, filePath: null, restored: false, error: err.message };
93
+ }
94
+ }
95
+
96
+ /**
97
+ * List recent checkpoints.
98
+ * @param {number} [limit=10]
99
+ * @returns {Array}
100
+ */
101
+ list(limit = 10) {
102
+ const result = [];
103
+ const ids = this.history.slice(-limit).reverse();
104
+
105
+ for (const id of ids) {
106
+ try {
107
+ const raw = fs.readFileSync(
108
+ path.join(this.checkpointDir, `${id}.json`),
109
+ 'utf-8'
110
+ );
111
+ const ckpt = JSON.parse(raw);
112
+ result.push({
113
+ id: ckpt.id,
114
+ file: ckpt.relativePath,
115
+ timestamp: ckpt.timestamp,
116
+ size: ckpt.size,
117
+ });
118
+ } catch {
119
+ result.push({ id, file: '?', timestamp: '?', size: 0 });
120
+ }
121
+ }
122
+
123
+ return result;
124
+ }
125
+
126
+ /**
127
+ * Clear all checkpoints.
128
+ */
129
+ clear() {
130
+ this.history.length = 0;
131
+ try {
132
+ const entries = fs.readdirSync(this.checkpointDir);
133
+ for (const entry of entries) {
134
+ if (entry.startsWith('ckpt_') && entry.endsWith('.json')) {
135
+ fs.unlinkSync(path.join(this.checkpointDir, entry));
136
+ }
137
+ }
138
+ } catch {
139
+ // Directory doesn't exist
140
+ }
141
+ }
142
+ }
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Context Manager — tracks token usage and compacts conversation history.
3
+ *
4
+ * Features:
5
+ * - Proper token estimation (4 chars ~ 1 token for English)
6
+ * - Micro-compaction (remove stale tool results older than 5 turns)
7
+ * - Keep system prompt and recent 3 turns intact during compaction
8
+ * - Track pre/post compaction token counts
9
+ */
10
+
11
+ const DEFAULT_MAX_TOKENS = 180000; // ~200k model limit with buffer
12
+ const COMPACT_THRESHOLD = 0.80;
13
+ const CHARS_PER_TOKEN = 4; // rough estimate for English text
14
+ const STALE_TOOL_RESULT_TURNS = 5; // tool results older than this are micro-compacted
15
+
16
+ export class ContextManager {
17
+ /**
18
+ * @param {number} maxTokens - Maximum tokens for context window
19
+ */
20
+ constructor(maxTokens = DEFAULT_MAX_TOKENS) {
21
+ this.maxTokens = maxTokens;
22
+ this.threshold = COMPACT_THRESHOLD;
23
+ this.compactionCount = 0;
24
+ this.lastPreCompactTokens = 0;
25
+ this.lastPostCompactTokens = 0;
26
+ }
27
+
28
+ /**
29
+ * Estimate token count for a message array.
30
+ * Uses character-based heuristic (no external tokenizer dependency).
31
+ * @param {Array} messages - conversation messages
32
+ * @returns {number} estimated token count
33
+ */
34
+ getTokenCount(messages) {
35
+ let chars = 0;
36
+ for (const msg of messages) {
37
+ // Role overhead (~4 tokens)
38
+ chars += 16;
39
+
40
+ if (typeof msg.content === 'string') {
41
+ chars += msg.content.length;
42
+ } else if (Array.isArray(msg.content)) {
43
+ for (const block of msg.content) {
44
+ if (block.type === 'text') chars += (block.text || '').length;
45
+ else if (block.type === 'tool_result') chars += (block.content || '').length;
46
+ else if (block.type === 'tool_use') chars += JSON.stringify(block.input || {}).length + 20;
47
+ else if (block.type === 'thinking') chars += (block.thinking || '').length;
48
+ else chars += JSON.stringify(block).length;
49
+ }
50
+ }
51
+ }
52
+ return Math.ceil(chars / CHARS_PER_TOKEN);
53
+ }
54
+
55
+ /**
56
+ * Check if compaction is needed.
57
+ * @param {Array} messages - current conversation messages
58
+ * @returns {boolean}
59
+ */
60
+ shouldCompact(messages) {
61
+ const tokenCount = this.getTokenCount(messages);
62
+ return tokenCount >= this.maxTokens * this.threshold;
63
+ }
64
+
65
+ /**
66
+ * Micro-compact: remove verbose tool results from messages older than N turns.
67
+ * Keeps the tool call reference but truncates result content.
68
+ * @param {Array} messages
69
+ * @param {number} recentTurns - number of recent user/assistant pairs to preserve
70
+ * @returns {Array}
71
+ */
72
+ microCompact(messages, recentTurns = STALE_TOOL_RESULT_TURNS) {
73
+ // Count turns (each user message is roughly one turn)
74
+ let turnCount = 0;
75
+ for (let i = messages.length - 1; i >= 0; i--) {
76
+ if (messages[i].role === 'user') turnCount++;
77
+ }
78
+
79
+ if (turnCount <= recentTurns) return messages;
80
+
81
+ // Mark the boundary: keep last recentTurns user messages intact
82
+ let usersSeen = 0;
83
+ let boundary = messages.length;
84
+ for (let i = messages.length - 1; i >= 0; i--) {
85
+ if (messages[i].role === 'user') {
86
+ usersSeen++;
87
+ if (usersSeen >= recentTurns) {
88
+ boundary = i;
89
+ break;
90
+ }
91
+ }
92
+ }
93
+
94
+ // Truncate tool results before the boundary
95
+ const result = messages.map((msg, idx) => {
96
+ if (idx >= boundary) return msg;
97
+ if (!Array.isArray(msg.content)) return msg;
98
+
99
+ const newContent = msg.content.map(block => {
100
+ if (block.type === 'tool_result' && typeof block.content === 'string' && block.content.length > 200) {
101
+ return {
102
+ ...block,
103
+ content: block.content.slice(0, 100) + '...[truncated]',
104
+ };
105
+ }
106
+ return block;
107
+ });
108
+
109
+ return { ...msg, content: newContent };
110
+ });
111
+
112
+ return result;
113
+ }
114
+
115
+ /**
116
+ * Compact messages by summarizing older history.
117
+ * Keeps the most recent N messages intact and replaces older ones
118
+ * with a summary message.
119
+ *
120
+ * @param {Array} messages - current conversation messages
121
+ * @param {number} keepRecent - number of recent messages to preserve (default 6 = ~3 turns)
122
+ * @returns {Array} compacted message array
123
+ */
124
+ compact(messages, keepRecent = 6) {
125
+ if (messages.length <= keepRecent) return messages;
126
+
127
+ this.lastPreCompactTokens = this.getTokenCount(messages);
128
+ this.compactionCount++;
129
+
130
+ // First try micro-compaction
131
+ let working = this.microCompact(messages);
132
+ if (!this.shouldCompact(working)) {
133
+ this.lastPostCompactTokens = this.getTokenCount(working);
134
+ return working;
135
+ }
136
+
137
+ // Full compaction
138
+ const oldMessages = messages.slice(0, -keepRecent);
139
+ const recentMessages = messages.slice(-keepRecent);
140
+
141
+ // Build a summary of old messages
142
+ const summaryParts = [];
143
+ for (const msg of oldMessages) {
144
+ const role = msg.role;
145
+ let text = '';
146
+ if (typeof msg.content === 'string') {
147
+ text = msg.content.slice(0, 200);
148
+ } else if (Array.isArray(msg.content)) {
149
+ text = msg.content
150
+ .map(b => {
151
+ if (b.type === 'text') return b.text?.slice(0, 100);
152
+ if (b.type === 'tool_use') return `[tool:${b.name}]`;
153
+ if (b.type === 'tool_result') return `[result:${String(b.content).slice(0, 80)}]`;
154
+ return `[${b.type}]`;
155
+ })
156
+ .filter(Boolean)
157
+ .join(' ');
158
+ }
159
+ if (text) summaryParts.push(`${role}: ${text}`);
160
+ }
161
+
162
+ const summary = {
163
+ role: 'user',
164
+ content: `[Context compacted — summary of ${oldMessages.length} earlier messages]\n` +
165
+ summaryParts.join('\n').slice(0, 2000),
166
+ };
167
+
168
+ const compacted = [summary, ...recentMessages];
169
+ this.lastPostCompactTokens = this.getTokenCount(compacted);
170
+ return compacted;
171
+ }
172
+
173
+ /**
174
+ * Add a message and auto-compact if needed.
175
+ * @param {Array} messages - mutable message array
176
+ * @param {object} msg - new message to add
177
+ * @returns {Array} possibly compacted array with new message
178
+ */
179
+ addMessage(messages, msg) {
180
+ messages.push(msg);
181
+ if (this.shouldCompact(messages)) {
182
+ return this.compact(messages);
183
+ }
184
+ return messages;
185
+ }
186
+
187
+ /**
188
+ * Get compaction statistics.
189
+ * @returns {object}
190
+ */
191
+ getStats() {
192
+ return {
193
+ compactionCount: this.compactionCount,
194
+ lastPreCompactTokens: this.lastPreCompactTokens,
195
+ lastPostCompactTokens: this.lastPostCompactTokens,
196
+ };
197
+ }
198
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Headless Runner — non-interactive mode for benchmarks and automation.
3
+ *
4
+ * No REPL, no spinners, no approval prompts. Auto-approves all tools.
5
+ * Outputs structured JSONL to stdout for machine consumption.
6
+ * stderr gets minimal progress (optional with --verbose).
7
+ *
8
+ * Usage:
9
+ * kepler --headless "Fix the bug in auth.py"
10
+ * kepler --headless --timeout 300 --max-cost 2.00 "Refactor the login flow"
11
+ * kepler --headless --model deepseek/deepseek-chat-v3-0324 "Add tests"
12
+ */
13
+
14
+ import { TarangStreamClient } from './stream-client.mjs';
15
+ import { createToolExecutor } from './tool-executor.mjs';
16
+ import { TarangAuth } from '../auth/tarang-auth.mjs';
17
+ import { ApprovalManager } from './approval.mjs';
18
+ import { ContextRetriever } from '../context/retriever.mjs';
19
+ import { buildProjectSkeleton } from '../context/skeleton.mjs';
20
+
21
+ /**
22
+ * Run a single instruction in headless mode.
23
+ * @param {object} opts
24
+ * @param {string} opts.instruction - the prompt to send
25
+ * @param {string} [opts.model] - model override
26
+ * @param {number} [opts.timeout] - max seconds (default: 300)
27
+ * @param {number} [opts.maxCost] - abort if cost exceeds this USD amount
28
+ * @param {boolean} [opts.verbose] - show progress on stderr
29
+ */
30
+ export async function runHeadless({ instruction, model, timeout = 300, maxCost, verbose = false }) {
31
+ const startTime = Date.now();
32
+
33
+ const log = (msg) => {
34
+ if (verbose) process.stderr.write(`[headless] ${msg}\n`);
35
+ };
36
+
37
+ const emit = (obj) => {
38
+ process.stdout.write(JSON.stringify(obj) + '\n');
39
+ };
40
+
41
+ // ── Auth ──
42
+ const auth = new TarangAuth();
43
+ const creds = auth.loadCredentials();
44
+ if (!creds.token) {
45
+ emit({ type: 'error', error: 'Not logged in. Run: kepler login' });
46
+ process.exit(1);
47
+ }
48
+
49
+ // ── Index project (with timeout — large repos can take minutes) ──
50
+ log('Indexing project...');
51
+ const retriever = new ContextRetriever(process.cwd());
52
+ try {
53
+ const indexPromise = retriever.buildIndex();
54
+ const indexTimeout = new Promise((_, reject) =>
55
+ setTimeout(() => reject(new Error('Index timeout')), 15000)
56
+ );
57
+ await Promise.race([indexPromise, indexTimeout]);
58
+ log('Index ready');
59
+ } catch (e) {
60
+ log(`Index skipped: ${e.message || 'failed'}`);
61
+ }
62
+
63
+ const skeleton = buildProjectSkeleton(process.cwd());
64
+ const toolExecutor = createToolExecutor({ retriever });
65
+
66
+ // Auto-approve everything — no prompts
67
+ const approval = new ApprovalManager({ autoApprove: true });
68
+
69
+ // ── Stream client ──
70
+ const client = new TarangStreamClient({
71
+ baseUrl: creds.backendUrl,
72
+ token: creds.token,
73
+ toolExecutor,
74
+ approvalManager: approval,
75
+ });
76
+
77
+ // ── Timeout ──
78
+ const timeoutMs = timeout * 1000;
79
+ const timeoutTimer = setTimeout(() => {
80
+ emit({ type: 'timeout', duration_s: timeout });
81
+ log(`Timeout after ${timeout}s`);
82
+ process.exit(2);
83
+ }, timeoutMs);
84
+
85
+ // ── Execute ──
86
+ emit({ type: 'start', timestamp: Date.now(), instruction, model: model || 'default', cwd: process.cwd() });
87
+
88
+ const execContext = { cwd: process.cwd(), freeswim: true };
89
+ if (skeleton) execContext.project_skeleton = skeleton;
90
+ if (model) execContext.model_override = model;
91
+
92
+ let toolCount = 0;
93
+ let finalContent = '';
94
+ let totalCost = 0;
95
+
96
+ try {
97
+ for await (const event of client.execute(instruction, execContext)) {
98
+ const { type, data } = event;
99
+
100
+ if (type === 'tool_call' || type === 'tool_request') {
101
+ toolCount++;
102
+ const toolName = data?.tool || 'unknown';
103
+ const args = data?.args || {};
104
+ emit({ type: 'tool_call', tool: toolName, args, approved: true });
105
+ log(`Tool: ${toolName}`);
106
+ }
107
+
108
+ if (type === 'tool_result' || type === 'tool_done') {
109
+ const success = data?.success !== false;
110
+ const duration = data?.duration_s || 0;
111
+ emit({ type: 'tool_result', tool: data?.tool || '', success, duration_ms: Math.round(duration * 1000) });
112
+ }
113
+
114
+ if (type === 'content') {
115
+ finalContent = data?.text || '';
116
+ }
117
+
118
+ if (type === 'content_partial') {
119
+ const text = data?.text || '';
120
+ if (text) finalContent = text;
121
+ }
122
+
123
+ if (type === 'complete') {
124
+ totalCost = data?.cost || data?.total_cost || 0;
125
+
126
+ // Extract cost from usage breakdown if available
127
+ if (data?.usage?.total_cost) {
128
+ totalCost = data.usage.total_cost;
129
+ }
130
+ }
131
+
132
+ if (type === 'error') {
133
+ emit({ type: 'error', error: data?.message || 'Unknown error' });
134
+ }
135
+
136
+ // ── Cost guard ──
137
+ if (maxCost && totalCost > maxCost) {
138
+ emit({ type: 'cost_exceeded', cost_usd: totalCost, max_cost: maxCost });
139
+ log(`Cost exceeded: $${totalCost.toFixed(3)} > $${maxCost}`);
140
+ break;
141
+ }
142
+ }
143
+ } catch (err) {
144
+ emit({ type: 'error', error: err.message });
145
+ }
146
+
147
+ clearTimeout(timeoutTimer);
148
+
149
+ const durationS = (Date.now() - startTime) / 1000;
150
+
151
+ emit({
152
+ type: 'complete',
153
+ tools: toolCount,
154
+ duration_s: Math.round(durationS * 10) / 10,
155
+ cost_usd: totalCost,
156
+ model: model || 'default',
157
+ content_length: finalContent.length,
158
+ });
159
+
160
+ log(`Done: ${toolCount} tools, ${durationS.toFixed(1)}s, $${totalCost.toFixed(3)}`);
161
+
162
+ // Write final content to stderr so it's human-readable (stdout is JSONL)
163
+ if (verbose && finalContent) {
164
+ process.stderr.write(`\n--- Response ---\n${finalContent.slice(0, 2000)}\n`);
165
+ }
166
+
167
+ process.exit(0);
168
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Hooks Manager — T22: PreToolUse/PostToolUse hooks from config.
3
+ */
4
+
5
+ import * as fs from 'node:fs';
6
+ import * as path from 'node:path';
7
+ import { execSync } from 'node:child_process';
8
+ import { projectHooksPath, globalHooksPath } from './paths.mjs';
9
+
10
+ export class HooksManager {
11
+ constructor(projectDir = process.cwd()) {
12
+ this.projectHooksPath = projectHooksPath(projectDir);
13
+ this.globalHooksPath = globalHooksPath();
14
+ this.hooks = this._loadHooks();
15
+ this.firedHooks = [];
16
+ }
17
+
18
+ _loadHooks() {
19
+ const hooks = { PreToolUse: [], PostToolUse: [] };
20
+
21
+ // Global hooks (lower priority)
22
+ try {
23
+ if (fs.existsSync(this.globalHooksPath)) {
24
+ const global = JSON.parse(fs.readFileSync(this.globalHooksPath, 'utf-8'));
25
+ if (global.PreToolUse) hooks.PreToolUse.push(...global.PreToolUse);
26
+ if (global.PostToolUse) hooks.PostToolUse.push(...global.PostToolUse);
27
+ }
28
+ } catch { /* skip corrupt */ }
29
+
30
+ // Project hooks (higher priority, appended after global)
31
+ try {
32
+ if (fs.existsSync(this.projectHooksPath)) {
33
+ const project = JSON.parse(fs.readFileSync(this.projectHooksPath, 'utf-8'));
34
+ if (project.PreToolUse) hooks.PreToolUse.push(...project.PreToolUse);
35
+ if (project.PostToolUse) hooks.PostToolUse.push(...project.PostToolUse);
36
+ }
37
+ } catch { /* skip corrupt */ }
38
+
39
+ return hooks;
40
+ }
41
+
42
+ /** Run PreToolUse hooks. Returns { allowed, message }. */
43
+ runPreToolUse(toolName, toolInput) {
44
+ return this._runHooks('PreToolUse', toolName, toolInput);
45
+ }
46
+
47
+ /** Run PostToolUse hooks. Returns { success }. */
48
+ runPostToolUse(toolName, toolInput, result) {
49
+ return this._runHooks('PostToolUse', toolName, toolInput, result);
50
+ }
51
+
52
+ _runHooks(event, toolName, toolInput, result = null) {
53
+ const hooks = (this.hooks[event] || []).filter(h => !h.tool || h.tool === toolName);
54
+ for (const hook of hooks) {
55
+ try {
56
+ const env = {
57
+ ...process.env,
58
+ HOOK_EVENT: event,
59
+ TOOL_NAME: toolName,
60
+ TOOL_INPUT: JSON.stringify(toolInput),
61
+ FILE_PATH: toolInput?.path || toolInput?.file_path || '',
62
+ };
63
+ if (result) env.TOOL_RESULT = JSON.stringify(result);
64
+
65
+ execSync(hook.command, { env, stdio: 'pipe', timeout: 10_000 });
66
+ this.firedHooks.push({ event, tool: toolName, command: hook.command, success: true });
67
+ } catch (err) {
68
+ this.firedHooks.push({ event, tool: toolName, command: hook.command, success: false });
69
+ if (event === 'PreToolUse') {
70
+ return { allowed: false, message: `Hook blocked ${toolName}: ${err.message}` };
71
+ }
72
+ }
73
+ }
74
+ return { allowed: true, success: true };
75
+ }
76
+
77
+ /** List configured hooks. */
78
+ listHooks() { return this.hooks; }
79
+
80
+ /** List hooks that fired this session. */
81
+ getFiredHooks() { return this.firedHooks; }
82
+
83
+ /** Check if any hooks are configured. */
84
+ hasHooks() {
85
+ return (this.hooks.PreToolUse.length + this.hooks.PostToolUse.length) > 0;
86
+ }
87
+ }