@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.
- package/README.md +82 -0
- package/package.json +36 -4
- package/pulse/app/activity/page.tsx +190 -0
- package/pulse/app/api/activity/route.ts +138 -0
- package/pulse/app/api/costs/route.ts +88 -0
- package/pulse/app/api/export/route.ts +77 -0
- package/pulse/app/api/history/route.ts +11 -0
- package/pulse/app/api/import/route.ts +31 -0
- package/pulse/app/api/memory/route.ts +52 -0
- package/pulse/app/api/plans/route.ts +9 -0
- package/pulse/app/api/projects/[slug]/route.ts +96 -0
- package/pulse/app/api/projects/route.ts +121 -0
- package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
- package/pulse/app/api/sessions/[id]/route.ts +31 -0
- package/pulse/app/api/sessions/route.ts +112 -0
- package/pulse/app/api/settings/route.ts +14 -0
- package/pulse/app/api/stats/route.ts +143 -0
- package/pulse/app/api/todos/route.ts +9 -0
- package/pulse/app/api/tools/route.ts +160 -0
- package/pulse/app/costs/page.tsx +179 -0
- package/pulse/app/export/page.tsx +465 -0
- package/pulse/app/favicon.ico +0 -0
- package/pulse/app/globals.css +263 -0
- package/pulse/app/help/page.tsx +142 -0
- package/pulse/app/history/page.tsx +157 -0
- package/pulse/app/layout.tsx +46 -0
- package/pulse/app/memory/page.tsx +365 -0
- package/pulse/app/overview-client.tsx +393 -0
- package/pulse/app/page.tsx +14 -0
- package/pulse/app/plans/page.tsx +308 -0
- package/pulse/app/projects/[slug]/page.tsx +390 -0
- package/pulse/app/projects/page.tsx +110 -0
- package/pulse/app/sessions/[id]/page.tsx +243 -0
- package/pulse/app/sessions/page.tsx +39 -0
- package/pulse/app/settings/page.tsx +188 -0
- package/pulse/app/todos/page.tsx +211 -0
- package/pulse/app/tools/page.tsx +249 -0
- package/pulse/cli.js +159 -0
- package/pulse/components/activity/day-of-week-chart.tsx +35 -0
- package/pulse/components/activity/streak-card.tsx +36 -0
- package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
- package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
- package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
- package/pulse/components/costs/model-token-table.tsx +60 -0
- package/pulse/components/global-search.tsx +193 -0
- package/pulse/components/keyboard-nav-provider.tsx +23 -0
- package/pulse/components/layout/bottom-nav.tsx +52 -0
- package/pulse/components/layout/client-layout.tsx +31 -0
- package/pulse/components/layout/sidebar-context.tsx +50 -0
- package/pulse/components/layout/sidebar.tsx +182 -0
- package/pulse/components/layout/top-bar.tsx +121 -0
- package/pulse/components/overview/activity-heatmap.tsx +107 -0
- package/pulse/components/overview/conversation-table.tsx +148 -0
- package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
- package/pulse/components/overview/peak-hours-chart.tsx +87 -0
- package/pulse/components/overview/project-activity-donut.tsx +96 -0
- package/pulse/components/overview/stat-card.tsx +102 -0
- package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
- package/pulse/components/projects/project-card.tsx +175 -0
- package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
- package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
- package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
- package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
- package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
- package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
- package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
- package/pulse/components/sessions/session-badges.tsx +49 -0
- package/pulse/components/sessions/session-table.tsx +299 -0
- package/pulse/components/theme-provider.tsx +44 -0
- package/pulse/components/tools/feature-adoption-table.tsx +58 -0
- package/pulse/components/tools/mcp-server-panel.tsx +45 -0
- package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
- package/pulse/components/tools/version-history-table.tsx +32 -0
- package/pulse/components/ui/alert.tsx +66 -0
- package/pulse/components/ui/badge.tsx +48 -0
- package/pulse/components/ui/breadcrumb.tsx +109 -0
- package/pulse/components/ui/button.tsx +64 -0
- package/pulse/components/ui/calendar.tsx +220 -0
- package/pulse/components/ui/card.tsx +92 -0
- package/pulse/components/ui/command.tsx +158 -0
- package/pulse/components/ui/dialog.tsx +158 -0
- package/pulse/components/ui/input.tsx +21 -0
- package/pulse/components/ui/popover.tsx +89 -0
- package/pulse/components/ui/progress.tsx +31 -0
- package/pulse/components/ui/select.tsx +190 -0
- package/pulse/components/ui/separator.tsx +28 -0
- package/pulse/components/ui/sheet.tsx +143 -0
- package/pulse/components/ui/skeleton.tsx +13 -0
- package/pulse/components/ui/table.tsx +116 -0
- package/pulse/components/ui/tabs.tsx +91 -0
- package/pulse/components/ui/tooltip.tsx +57 -0
- package/pulse/components/use-global-keyboard-nav.ts +79 -0
- package/pulse/components.json +23 -0
- package/pulse/eslint.config.mjs +18 -0
- package/pulse/lib/claude-reader.ts +594 -0
- package/pulse/lib/decode.ts +129 -0
- package/pulse/lib/pricing.ts +102 -0
- package/pulse/lib/replay-parser.ts +165 -0
- package/pulse/lib/tool-categories.ts +127 -0
- package/pulse/lib/utils.ts +6 -0
- package/pulse/next-env.d.ts +6 -0
- package/pulse/next.config.ts +16 -0
- package/pulse/package.json +45 -0
- package/pulse/postcss.config.mjs +7 -0
- package/pulse/public/activity.png +0 -0
- package/pulse/public/cc-lens.png +0 -0
- package/pulse/public/command-k.png +0 -0
- package/pulse/public/costs.png +0 -0
- package/pulse/public/dashboard-dark.png +0 -0
- package/pulse/public/dashboard-white.png +0 -0
- package/pulse/public/export.png +0 -0
- package/pulse/public/file.svg +1 -0
- package/pulse/public/globe.svg +1 -0
- package/pulse/public/next.svg +1 -0
- package/pulse/public/projects.png +0 -0
- package/pulse/public/session-chat.png +0 -0
- package/pulse/public/todos.png +0 -0
- package/pulse/public/tools.png +0 -0
- package/pulse/public/vercel.svg +1 -0
- package/pulse/public/window.svg +1 -0
- package/pulse/tsconfig.json +34 -0
- package/pulse/types/claude.ts +294 -0
- package/src/agents/loader.mjs +89 -0
- package/src/agents/parser.mjs +98 -0
- package/src/agents/teams.mjs +123 -0
- package/src/auth/oauth.mjs +220 -0
- package/src/auth/tarang-auth.mjs +277 -0
- package/src/config/cli-args.mjs +173 -0
- package/src/config/env.mjs +263 -0
- package/src/config/settings.mjs +132 -0
- package/src/context/ast-parser.mjs +298 -0
- package/src/context/bm25.mjs +85 -0
- package/src/context/retriever.mjs +270 -0
- package/src/context/skeleton.mjs +134 -0
- package/src/core/agent-loop.mjs +480 -0
- package/src/core/approval.mjs +273 -0
- package/src/core/backend-url.mjs +57 -0
- package/src/core/cache.mjs +105 -0
- package/src/core/callback-client.mjs +149 -0
- package/src/core/checkpoints.mjs +142 -0
- package/src/core/context-manager.mjs +198 -0
- package/src/core/headless.mjs +168 -0
- package/src/core/hooks-manager.mjs +87 -0
- package/src/core/jsonl-writer.mjs +351 -0
- package/src/core/local-agent.mjs +429 -0
- package/src/core/local-store.mjs +325 -0
- package/src/core/mode-selector.mjs +51 -0
- package/src/core/output-filter.mjs +177 -0
- package/src/core/paths.mjs +101 -0
- package/src/core/pricing.mjs +314 -0
- package/src/core/providers.mjs +219 -0
- package/src/core/rate-limiter.mjs +119 -0
- package/src/core/safety.mjs +200 -0
- package/src/core/scheduler.mjs +173 -0
- package/src/core/session-manager.mjs +317 -0
- package/src/core/session.mjs +143 -0
- package/src/core/settings-sync.mjs +85 -0
- package/src/core/stagnation.mjs +57 -0
- package/src/core/stream-client.mjs +367 -0
- package/src/core/streaming.mjs +182 -0
- package/src/core/system-prompt.mjs +135 -0
- package/src/core/tool-executor.mjs +725 -0
- package/src/hooks/engine.mjs +162 -0
- package/src/index.mjs +370 -0
- package/src/mcp/client.mjs +253 -0
- package/src/mcp/transport-shttp.mjs +130 -0
- package/src/mcp/transport-sse.mjs +131 -0
- package/src/mcp/transport-ws.mjs +134 -0
- package/src/permissions/checker.mjs +57 -0
- package/src/permissions/command-classifier.mjs +573 -0
- package/src/permissions/injection-check.mjs +60 -0
- package/src/permissions/path-check.mjs +102 -0
- package/src/permissions/prompt.mjs +73 -0
- package/src/permissions/sandbox.mjs +112 -0
- package/src/plugins/loader.mjs +138 -0
- package/src/skills/loader.mjs +147 -0
- package/src/skills/runner.mjs +55 -0
- package/src/telemetry/index.mjs +96 -0
- package/src/terminal/agents.mjs +177 -0
- package/src/terminal/analytics.mjs +292 -0
- package/src/terminal/ansi.mjs +421 -0
- package/src/terminal/main.mjs +150 -0
- package/src/terminal/repl.mjs +1484 -0
- package/src/terminal/tool-display.mjs +58 -0
- package/src/tools/agent.mjs +137 -0
- package/src/tools/ask-user.mjs +61 -0
- package/src/tools/bash.mjs +148 -0
- package/src/tools/cron-create.mjs +120 -0
- package/src/tools/cron-delete.mjs +49 -0
- package/src/tools/cron-list.mjs +37 -0
- package/src/tools/edit.mjs +82 -0
- package/src/tools/enter-worktree.mjs +69 -0
- package/src/tools/exit-worktree.mjs +57 -0
- package/src/tools/glob.mjs +117 -0
- package/src/tools/grep.mjs +129 -0
- package/src/tools/lint.mjs +71 -0
- package/src/tools/ls.mjs +58 -0
- package/src/tools/lsp.mjs +115 -0
- package/src/tools/multi-edit.mjs +94 -0
- package/src/tools/notebook-edit.mjs +96 -0
- package/src/tools/read-mcp-resource.mjs +57 -0
- package/src/tools/read.mjs +138 -0
- package/src/tools/registry.mjs +132 -0
- package/src/tools/remote-trigger.mjs +84 -0
- package/src/tools/send-message.mjs +64 -0
- package/src/tools/skill.mjs +52 -0
- package/src/tools/test-runner.mjs +49 -0
- package/src/tools/todo-write.mjs +68 -0
- package/src/tools/tool-search.mjs +77 -0
- package/src/tools/web-fetch.mjs +65 -0
- package/src/tools/web-search.mjs +89 -0
- package/src/tools/write.mjs +55 -0
- package/src/ui/banner.mjs +237 -0
- package/src/ui/commands.mjs +499 -0
- package/src/ui/formatter.mjs +379 -0
- package/src/ui/markdown.mjs +278 -0
- package/src/ui/slash-commands.mjs +258 -0
- package/index.js +0 -1
|
@@ -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
|
+
}
|