@axplusb/kepler 2.0.7 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/KEPLER-README.md +157 -142
- package/README.md +37 -65
- package/package.json +2 -2
- package/src/config/cli-args.mjs +14 -0
- package/src/config/hook-runner.mjs +100 -0
- package/src/config/memory-loader.mjs +32 -0
- package/src/config/settings-loader.mjs +45 -0
- package/src/core/agent-loop.mjs +8 -2
- package/src/core/approval-log.mjs +104 -0
- package/src/core/approval.mjs +164 -24
- package/src/core/cache-control.mjs +92 -0
- package/src/core/context-envelope.mjs +54 -0
- package/src/core/headless.mjs +99 -10
- package/src/core/jsonl-writer.mjs +50 -0
- package/src/core/local-agent.mjs +121 -14
- package/src/core/local-store.mjs +486 -5
- package/src/core/policy-resolver.mjs +156 -0
- package/src/core/project-context-loader.mjs +139 -0
- package/src/core/rate-limit-display.mjs +97 -0
- package/src/core/resume-mode.mjs +154 -0
- package/src/core/risk-tier.mjs +88 -2
- package/src/core/safety.mjs +3 -0
- package/src/core/session-manager.mjs +53 -10
- package/src/core/stream-client.mjs +69 -10
- package/src/core/system-prompt.mjs +6 -1
- package/src/core/tasks.mjs +196 -0
- package/src/core/tool-executor.mjs +72 -6
- package/src/core/trust.mjs +158 -0
- package/src/core/work-scope.mjs +217 -0
- package/src/onboarding/preflight.mjs +27 -11
- package/src/permissions/command-classifier.mjs +78 -0
- package/src/terminal/init.mjs +145 -0
- package/src/terminal/main.mjs +9 -0
- package/src/terminal/repl.mjs +1822 -140
- package/src/tools/bash.mjs +57 -9
- package/src/tools/project-overview.mjs +83 -10
- package/src/ui/approval.mjs +154 -35
- package/src/ui/commands.mjs +5 -4
- package/src/ui/formatter.mjs +39 -5
- package/src/ui/mission-report.mjs +55 -22
- package/src/ui/slash-commands.mjs +57 -5
- package/src/ui/tool-card.mjs +51 -1
|
@@ -5,18 +5,17 @@
|
|
|
5
5
|
* structured summary:
|
|
6
6
|
*
|
|
7
7
|
* ───────────────────────────────────────────────────
|
|
8
|
-
*
|
|
8
|
+
* ✓ done
|
|
9
9
|
* ───────────────────────────────────────────────────
|
|
10
10
|
* 📂 Files auth.py, tests/test_auth.py
|
|
11
|
-
* 🛠️ Tools
|
|
11
|
+
* 🛠️ Tools read(4) edit(2) shell(1) test(1) · ⏱ Time 2m 18s
|
|
12
12
|
* 🛰️ Sub-agents explore(1) plan(1) · saved ≈ $0.08
|
|
13
|
-
* 💰 Cost $0.14 ⏱ Time 2m 18s
|
|
14
13
|
* ✅ Health 24/24 tests pass
|
|
15
14
|
* ───────────────────────────────────────────────────
|
|
16
15
|
*
|
|
17
16
|
* Next: /commit /pr /undo /report
|
|
18
17
|
*
|
|
19
|
-
* Failure variant
|
|
18
|
+
* Failure variant uses "held" and lists blockers.
|
|
20
19
|
*
|
|
21
20
|
* `renderMissionReport(state)` returns the ANSI block; `toMarkdown(state)`
|
|
22
21
|
* returns the plain-markdown version saved by `/report`.
|
|
@@ -24,6 +23,7 @@
|
|
|
24
23
|
|
|
25
24
|
import path from 'node:path';
|
|
26
25
|
import fs from 'node:fs';
|
|
26
|
+
import { execFileSync } from 'node:child_process';
|
|
27
27
|
import { paint, width as visibleWidth } from './palette.mjs';
|
|
28
28
|
import { icons } from './icons.mjs';
|
|
29
29
|
import { toolFamily } from './icons.mjs';
|
|
@@ -39,6 +39,7 @@ const WIDTH = 56;
|
|
|
39
39
|
* task — string (the user's prompt for this session)
|
|
40
40
|
* success — boolean (overall outcome)
|
|
41
41
|
* filesChanged — string[]
|
|
42
|
+
* filesRead — string[]
|
|
42
43
|
* toolCounts — { [tool]: count } or array of {tool}
|
|
43
44
|
* subAgents — array of { type, costUsd?, tokens? } or { explore:1, plan:1 }
|
|
44
45
|
* costUsd — number
|
|
@@ -46,46 +47,41 @@ const WIDTH = 56;
|
|
|
46
47
|
* testsPass — { passed: number, total: number } | null
|
|
47
48
|
* blockers — string[] (for failure variant)
|
|
48
49
|
* nextActions — string[] (slash-command hints)
|
|
50
|
+
* cwd — string (used to derive git repo + author metadata)
|
|
49
51
|
*/
|
|
50
52
|
export function renderMissionReport(state) {
|
|
51
53
|
const success = state.success !== false;
|
|
52
54
|
const lines = [];
|
|
53
55
|
const rule = paint.text.dim('─'.repeat(WIDTH));
|
|
54
56
|
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
const titleAccent = success ? paint.state.success : paint.state.danger;
|
|
57
|
+
const statusIcon = success ? paint.state.success('✓') : paint.state.danger('✗');
|
|
58
|
+
const statusText = success ? paint.state.success('done') : paint.state.danger('held');
|
|
58
59
|
const headerTask = state.task ? paint.text.dim(' · ') + paint.text.primary(truncate(state.task, 60)) : '';
|
|
59
60
|
|
|
60
61
|
lines.push('');
|
|
61
62
|
lines.push(rule);
|
|
62
|
-
lines.push(`${
|
|
63
|
+
lines.push(`${statusIcon} ${statusText}${headerTask}`);
|
|
63
64
|
lines.push(rule);
|
|
64
65
|
|
|
65
66
|
if (Array.isArray(state.filesChanged) && state.filesChanged.length) {
|
|
66
67
|
lines.push(row('📂', 'Files', formatFiles(state.filesChanged)));
|
|
67
68
|
}
|
|
69
|
+
if (Array.isArray(state.filesRead) && state.filesRead.length) {
|
|
70
|
+
lines.push(row('📖', 'Read', formatFiles(state.filesRead)));
|
|
71
|
+
}
|
|
68
72
|
|
|
69
73
|
const toolSummary = formatToolCounts(state.toolCounts);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
74
|
+
const time = state.durationS != null ? paint.brand.data(formatDuration(state.durationS)) : '';
|
|
75
|
+
const metricSegments = [];
|
|
76
|
+
if (toolSummary) metricSegments.push(`${icons.write} ${paint.text.dim('Tools')} ${toolSummary}`);
|
|
77
|
+
if (time) metricSegments.push(`${paint.text.dim('⏱ Time')} ${time}`);
|
|
78
|
+
if (metricSegments.length) lines.push(' ' + metricSegments.join(paint.text.dim(' · ')));
|
|
73
79
|
|
|
74
80
|
if (state.subAgents) {
|
|
75
81
|
const subSummary = formatSubAgents(state.subAgents);
|
|
76
82
|
if (subSummary) lines.push(row(icons.subAgent, 'Sub-agents', subSummary));
|
|
77
83
|
}
|
|
78
84
|
|
|
79
|
-
// Cost + time on one row.
|
|
80
|
-
const cost = state.costUsd != null ? paint.brand.data(formatCost(state.costUsd)) : '';
|
|
81
|
-
const time = state.durationS != null ? paint.brand.data(formatDuration(state.durationS)) : '';
|
|
82
|
-
if (cost || time) {
|
|
83
|
-
const segments = [];
|
|
84
|
-
if (cost) segments.push(`${paint.text.dim('💰 Cost')} ${cost}`);
|
|
85
|
-
if (time) segments.push(`${paint.text.dim('⏱ Time')} ${time}`);
|
|
86
|
-
lines.push(' ' + segments.join(' '));
|
|
87
|
-
}
|
|
88
|
-
|
|
89
85
|
// Test health.
|
|
90
86
|
if (state.testsPass && typeof state.testsPass.total === 'number' && state.testsPass.total > 0) {
|
|
91
87
|
const { passed = 0, total = 0 } = state.testsPass;
|
|
@@ -126,12 +122,18 @@ export function renderMissionReport(state) {
|
|
|
126
122
|
*/
|
|
127
123
|
export function toMarkdown(state) {
|
|
128
124
|
const success = state.success !== false;
|
|
125
|
+
const meta = resolveReportMeta(state);
|
|
129
126
|
const out = [];
|
|
130
|
-
out.push(`# ${success ? '
|
|
127
|
+
out.push(`# ${success ? 'Done' : 'Held'}${state.task ? ' — ' + state.task : ''}`);
|
|
131
128
|
out.push('');
|
|
129
|
+
out.push('**Repo**: ' + meta.repo);
|
|
130
|
+
out.push('**Author**: ' + meta.author);
|
|
132
131
|
if (Array.isArray(state.filesChanged) && state.filesChanged.length) {
|
|
133
132
|
out.push('**Files**: ' + state.filesChanged.join(', '));
|
|
134
133
|
}
|
|
134
|
+
if (Array.isArray(state.filesRead) && state.filesRead.length) {
|
|
135
|
+
out.push('**Read**: ' + state.filesRead.join(', '));
|
|
136
|
+
}
|
|
135
137
|
const toolSummary = stripAnsi(formatToolCounts(state.toolCounts) || '');
|
|
136
138
|
if (toolSummary) out.push('**Tools**: ' + toolSummary);
|
|
137
139
|
if (state.subAgents) {
|
|
@@ -178,6 +180,37 @@ function row(icon, label, value, alreadyIcon = false) {
|
|
|
178
180
|
return ` ${i} ${labelText} ${value}`;
|
|
179
181
|
}
|
|
180
182
|
|
|
183
|
+
function resolveReportMeta(state = {}) {
|
|
184
|
+
const cwd = state.cwd || process.cwd();
|
|
185
|
+
return {
|
|
186
|
+
repo: state.repo || gitValue(cwd, ['remote', 'get-url', 'origin']) ||
|
|
187
|
+
gitValue(cwd, ['rev-parse', '--show-toplevel'], value => path.basename(value)) ||
|
|
188
|
+
path.basename(cwd),
|
|
189
|
+
author: state.author || gitAuthor(cwd) || 'unknown',
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function gitAuthor(cwd) {
|
|
194
|
+
const name = gitValue(cwd, ['config', '--get', 'user.name']);
|
|
195
|
+
const email = gitValue(cwd, ['config', '--get', 'user.email']);
|
|
196
|
+
if (name && email) return `${name} <${email}>`;
|
|
197
|
+
return name || email || '';
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function gitValue(cwd, args, transform = value => value) {
|
|
201
|
+
try {
|
|
202
|
+
const value = execFileSync('git', args, {
|
|
203
|
+
cwd,
|
|
204
|
+
encoding: 'utf-8',
|
|
205
|
+
timeout: 1500,
|
|
206
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
207
|
+
}).trim();
|
|
208
|
+
return value ? transform(value) : '';
|
|
209
|
+
} catch {
|
|
210
|
+
return '';
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
181
214
|
function formatFiles(files) {
|
|
182
215
|
const shortened = files.map(f => paint.text.primary(path.basename(f)));
|
|
183
216
|
if (shortened.length <= 4) return shortened.join(paint.text.dim(', '));
|
|
@@ -29,6 +29,29 @@ export const COMMANDS = {
|
|
|
29
29
|
'/whoami': { description: 'Show logged-in user', handler: cmdWhoami },
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
+
const COMMAND_GROUPS = [
|
|
33
|
+
{
|
|
34
|
+
key: 'worktree',
|
|
35
|
+
title: 'Worktree',
|
|
36
|
+
commands: ['/git', '/status', '/commit', '/diff', '/index'],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
key: 'session',
|
|
40
|
+
title: 'Session',
|
|
41
|
+
commands: ['/clear', '/sessions', '/exit', '/quit'],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
key: 'usage',
|
|
45
|
+
title: 'Usage',
|
|
46
|
+
commands: ['/model', '/tokens', '/cost'],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
key: 'settings',
|
|
50
|
+
title: 'Settings',
|
|
51
|
+
commands: ['/config', '/login', '/refresh', '/sync', '/whoami'],
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
|
|
32
55
|
function run(cmd) {
|
|
33
56
|
try {
|
|
34
57
|
return execSync(cmd, { encoding: 'utf-8', timeout: 15_000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
@@ -37,17 +60,37 @@ function run(cmd) {
|
|
|
37
60
|
}
|
|
38
61
|
}
|
|
39
62
|
|
|
40
|
-
function cmdHelp(ctx) {
|
|
63
|
+
function cmdHelp(ctx, args = []) {
|
|
41
64
|
const BOLD = '\x1b[1m', CYAN = '\x1b[36m', DIM = '\x1b[2m', GREEN = '\x1b[32m', BLUE = '\x1b[34m', RESET = '\x1b[0m';
|
|
65
|
+
const topic = String(args[0] || '').toLowerCase();
|
|
66
|
+
const group = COMMAND_GROUPS.find(g => g.key === topic || g.title.toLowerCase() === topic);
|
|
42
67
|
|
|
43
68
|
process.stderr.write(`\n${BLUE}┌──────────────────────────────────────────────┐${RESET}\n`);
|
|
44
69
|
process.stderr.write(`${BLUE}│${RESET} ${BOLD}Kepler Help${RESET} ${BLUE}│${RESET}\n`);
|
|
45
70
|
process.stderr.write(`${BLUE}├──────────────────────────────────────────────┤${RESET}\n`);
|
|
46
71
|
process.stderr.write(`${BLUE}│${RESET} ${BLUE}│${RESET}\n`);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
72
|
+
if (topic === 'all') {
|
|
73
|
+
process.stderr.write(`${BLUE}│${RESET} ${BOLD}All commands:${RESET} ${BLUE}│${RESET}\n`);
|
|
74
|
+
for (const [name, { description }] of Object.entries(COMMANDS)) {
|
|
75
|
+
writeHelpCommandLine(BLUE, CYAN, RESET, name, description);
|
|
76
|
+
}
|
|
77
|
+
} else if (group) {
|
|
78
|
+
process.stderr.write(`${BLUE}│${RESET} ${BOLD}${group.title}:${RESET}${' '.repeat(Math.max(0, 40 - group.title.length))}${BLUE}│${RESET}\n`);
|
|
79
|
+
for (const name of group.commands) {
|
|
80
|
+
writeHelpCommandLine(BLUE, CYAN, RESET, name, COMMANDS[name]?.description || '');
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
process.stderr.write(`${BLUE}│${RESET} ${BOLD}Top commands:${RESET} ${BLUE}│${RESET}\n`);
|
|
84
|
+
for (const name of ['/help', '/status', '/sessions', '/config', '/whoami', '/exit']) {
|
|
85
|
+
writeHelpCommandLine(BLUE, CYAN, RESET, name, COMMANDS[name]?.description || '');
|
|
86
|
+
}
|
|
87
|
+
process.stderr.write(`${BLUE}│${RESET} ${BLUE}│${RESET}\n`);
|
|
88
|
+
process.stderr.write(`${BLUE}│${RESET} ${BOLD}Categories:${RESET} ${BLUE}│${RESET}\n`);
|
|
89
|
+
for (const g of COMMAND_GROUPS) {
|
|
90
|
+
const line = ` ${CYAN}${('/help ' + g.key).padEnd(16)}${RESET} ${g.title}`;
|
|
91
|
+
process.stderr.write(`${BLUE}│${RESET}${line}${' '.repeat(Math.max(0, 44 - visibleLen(`/help ${g.key}`) - g.title.length))}${BLUE}│${RESET}\n`);
|
|
92
|
+
}
|
|
93
|
+
process.stderr.write(`${BLUE}│${RESET} ${DIM}/help all for legacy command list${RESET} ${BLUE}│${RESET}\n`);
|
|
51
94
|
}
|
|
52
95
|
process.stderr.write(`${BLUE}│${RESET} ${BLUE}│${RESET}\n`);
|
|
53
96
|
process.stderr.write(`${BLUE}│${RESET} ${BOLD}Keyboard:${RESET} ${BLUE}│${RESET}\n`);
|
|
@@ -61,6 +104,15 @@ function cmdHelp(ctx) {
|
|
|
61
104
|
process.stderr.write(`${BLUE}└──────────────────────────────────────────────┘${RESET}\n\n`);
|
|
62
105
|
}
|
|
63
106
|
|
|
107
|
+
function writeHelpCommandLine(BLUE, CYAN, RESET, name, description) {
|
|
108
|
+
const line = ` ${CYAN}${name.padEnd(12)}${RESET} ${description}`;
|
|
109
|
+
process.stderr.write(`${BLUE}│${RESET}${line}${' '.repeat(Math.max(0, 44 - name.length - description.length))}${BLUE}│${RESET}\n`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function visibleLen(s) {
|
|
113
|
+
return String(s || '').length;
|
|
114
|
+
}
|
|
115
|
+
|
|
64
116
|
function cmdGit() {
|
|
65
117
|
process.stdout.write(run('git status --short --branch') + '\n');
|
|
66
118
|
}
|
package/src/ui/tool-card.mjs
CHANGED
|
@@ -76,6 +76,14 @@ export function summarizeResult(tool, data) {
|
|
|
76
76
|
if (data._blocked) {
|
|
77
77
|
return { text: firstOutputLine(data) || 'blocked', tone: 'danger' };
|
|
78
78
|
}
|
|
79
|
+
if (data._observation_timeout) {
|
|
80
|
+
const ms = data._observation_timeout_ms;
|
|
81
|
+
const duration = typeof ms === 'number' ? formatDuration(ms) : '';
|
|
82
|
+
return { text: duration ? `observed ${duration} tail` : 'observed output tail', tone: 'warn' };
|
|
83
|
+
}
|
|
84
|
+
if (data._timed_out) {
|
|
85
|
+
return { text: 'timed out', tone: 'danger' };
|
|
86
|
+
}
|
|
79
87
|
if (data.success === false) {
|
|
80
88
|
const msg = String(data.error || firstOutputLine(data) || 'failed').slice(0, 140);
|
|
81
89
|
return { text: msg, tone: 'danger' };
|
|
@@ -224,6 +232,16 @@ export function formatCardHead(tool, args, opts = {}) {
|
|
|
224
232
|
|
|
225
233
|
const leadVisible = visibleWidth(`${indent}${label}`);
|
|
226
234
|
const budget = Math.max(20, cols - leadVisible - 4);
|
|
235
|
+
|
|
236
|
+
if (tool === 'shell' && visibleWidth(argsText) > budget) {
|
|
237
|
+
const wrapWidth = Math.max(32, cols - visibleWidth(indent) - 4);
|
|
238
|
+
const command = toolDisplaySummary(tool, args || {}, { cwd });
|
|
239
|
+
const commandLines = wrapCommand(command, wrapWidth)
|
|
240
|
+
.map(line => `${indent}${paint.text.dim(' ')}${formatShellCommand(line, paintShellAdapter)}`);
|
|
241
|
+
const head = `${indent}${paintLabel(tool, label)}`;
|
|
242
|
+
return `${head}\n${commandLines.join('\n')}`;
|
|
243
|
+
}
|
|
244
|
+
|
|
227
245
|
const argsTruncated = truncateMiddle(argsText, budget);
|
|
228
246
|
|
|
229
247
|
const head = `${indent}${paintLabel(tool, label)}`;
|
|
@@ -255,7 +273,7 @@ export function formatCard({ tool, args, result, durationMs, indent, columns, cw
|
|
|
255
273
|
const tail = showDuration ? paint.text.dim(` · ${duration}`) : '';
|
|
256
274
|
|
|
257
275
|
const candidate = `${head} ${arrow} ${body}${tail}`;
|
|
258
|
-
if (visibleWidth(candidate) <= cols) return candidate;
|
|
276
|
+
if (!head.includes('\n') && visibleWidth(candidate) <= cols) return candidate;
|
|
259
277
|
|
|
260
278
|
// Doesn't fit on one line → push outcome to a separate gutter line.
|
|
261
279
|
const gutterIndent = (indent || ' ') + paint.text.dim('⎿ ');
|
|
@@ -274,6 +292,38 @@ function truncateMiddle(text, max) {
|
|
|
274
292
|
return paint.text.muted(`${head}…${tail}`);
|
|
275
293
|
}
|
|
276
294
|
|
|
295
|
+
function wrapCommand(command, width) {
|
|
296
|
+
const text = String(command || '');
|
|
297
|
+
if (!text) return ['(empty command)'];
|
|
298
|
+
const lines = [];
|
|
299
|
+
let line = '';
|
|
300
|
+
for (const token of text.match(/\S+\s*/g) || [text]) {
|
|
301
|
+
const next = line + token;
|
|
302
|
+
if (line && visibleWidth(next.trimEnd()) > width) {
|
|
303
|
+
lines.push(line.trimEnd());
|
|
304
|
+
line = token;
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
if (!line && visibleWidth(token.trimEnd()) > width) {
|
|
308
|
+
lines.push(...chunkLongToken(token.trimEnd(), width));
|
|
309
|
+
line = '';
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
line = next;
|
|
313
|
+
}
|
|
314
|
+
if (line.trimEnd()) lines.push(line.trimEnd());
|
|
315
|
+
return lines.length ? lines : ['(empty command)'];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function chunkLongToken(token, width) {
|
|
319
|
+
const chunks = [];
|
|
320
|
+
const size = Math.max(8, width);
|
|
321
|
+
for (let i = 0; i < token.length; i += size) {
|
|
322
|
+
chunks.push(token.slice(i, i + size));
|
|
323
|
+
}
|
|
324
|
+
return chunks;
|
|
325
|
+
}
|
|
326
|
+
|
|
277
327
|
function formatDuration(ms) {
|
|
278
328
|
if (ms == null || !Number.isFinite(ms)) return '';
|
|
279
329
|
if (ms < 1000) return `${Math.round(ms)}ms`;
|