@axplusb/kepler 2.0.7 → 2.2.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.
@@ -5,18 +5,17 @@
5
5
  * structured summary:
6
6
  *
7
7
  * ───────────────────────────────────────────────────
8
- * MISSION ACCOMPLISHED · Fix JWT expiration bug
8
+ * done
9
9
  * ───────────────────────────────────────────────────
10
10
  * 📂 Files auth.py, tests/test_auth.py
11
- * 🛠️ Tools read(4) edit(2) shell(1) test(1)
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 (PRD §11.1) uses MISSION HELD and lists blockers.
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 titleIcon = success ? paint.state.success('') : paint.state.danger('');
56
- const titleText = success ? 'MISSION ACCOMPLISHED' : 'MISSION HELD';
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(`${titleIcon} ${paint.bold(titleAccent(titleText))}${headerTask}`);
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
- if (toolSummary) {
71
- lines.push(row(icons.write, 'Tools', toolSummary));
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 ? '✅ Mission Accomplished' : '❌ Mission Held'}${state.task ? ' — ' + state.task : ''}`);
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
- process.stderr.write(`${BLUE}│${RESET} ${BOLD}Commands:${RESET} ${BLUE}│${RESET}\n`);
48
- for (const [name, { description }] of Object.entries(COMMANDS)) {
49
- const line = ` ${CYAN}${name.padEnd(12)}${RESET} ${description}`;
50
- process.stderr.write(`${BLUE}│${RESET}${line}${' '.repeat(Math.max(0, 44 - name.length - description.length))}${BLUE}│${RESET}\n`);
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
  }
@@ -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`;