@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.
Files changed (42) hide show
  1. package/KEPLER-README.md +157 -142
  2. package/README.md +37 -65
  3. package/package.json +2 -2
  4. package/src/config/cli-args.mjs +14 -0
  5. package/src/config/hook-runner.mjs +100 -0
  6. package/src/config/memory-loader.mjs +32 -0
  7. package/src/config/settings-loader.mjs +45 -0
  8. package/src/core/agent-loop.mjs +8 -2
  9. package/src/core/approval-log.mjs +104 -0
  10. package/src/core/approval.mjs +164 -24
  11. package/src/core/cache-control.mjs +92 -0
  12. package/src/core/context-envelope.mjs +54 -0
  13. package/src/core/headless.mjs +99 -10
  14. package/src/core/jsonl-writer.mjs +50 -0
  15. package/src/core/local-agent.mjs +121 -14
  16. package/src/core/local-store.mjs +486 -5
  17. package/src/core/policy-resolver.mjs +156 -0
  18. package/src/core/project-context-loader.mjs +139 -0
  19. package/src/core/rate-limit-display.mjs +97 -0
  20. package/src/core/resume-mode.mjs +154 -0
  21. package/src/core/risk-tier.mjs +88 -2
  22. package/src/core/safety.mjs +3 -0
  23. package/src/core/session-manager.mjs +53 -10
  24. package/src/core/stream-client.mjs +69 -10
  25. package/src/core/system-prompt.mjs +6 -1
  26. package/src/core/tasks.mjs +196 -0
  27. package/src/core/tool-executor.mjs +72 -6
  28. package/src/core/trust.mjs +158 -0
  29. package/src/core/work-scope.mjs +217 -0
  30. package/src/onboarding/preflight.mjs +27 -11
  31. package/src/permissions/command-classifier.mjs +78 -0
  32. package/src/terminal/init.mjs +145 -0
  33. package/src/terminal/main.mjs +9 -0
  34. package/src/terminal/repl.mjs +1822 -140
  35. package/src/tools/bash.mjs +57 -9
  36. package/src/tools/project-overview.mjs +83 -10
  37. package/src/ui/approval.mjs +154 -35
  38. package/src/ui/commands.mjs +5 -4
  39. package/src/ui/formatter.mjs +39 -5
  40. package/src/ui/mission-report.mjs +55 -22
  41. package/src/ui/slash-commands.mjs +57 -5
  42. package/src/ui/tool-card.mjs +51 -1
@@ -144,6 +144,41 @@ function containsCommandSubstitution(command) {
144
144
  return false;
145
145
  }
146
146
 
147
+ function isQuotedMessage(text) {
148
+ const value = String(text || '').trim();
149
+ return /^echo\s+(?:"[^"]*"|'[^']*'|[A-Za-z0-9_ .:-]+)$/.test(value);
150
+ }
151
+
152
+ function isNumericKillCommand(command) {
153
+ const trimmed = String(command || '').trim();
154
+ return /^kill\s+(?:-(?:\d+|[A-Z]+)\s+)?\d+(?:\s+\d+)*\s*(?:2>\s*\/dev\/null)?$/.test(trimmed);
155
+ }
156
+
157
+ function isPortLsofKillSubstitution(command) {
158
+ const trimmed = String(command || '').trim();
159
+ return /^kill\s+(?:-(?:\d+|[A-Z]+)\s+)?\$\(\s*lsof\s+-ti:?\d+\s*\)\s*(?:2>\s*\/dev\/null)?$/.test(trimmed);
160
+ }
161
+
162
+ function isPortLsofXargsKill(command) {
163
+ const trimmed = String(command || '').trim();
164
+ return /^lsof\s+-ti:?\d+\s*\|\s*xargs\s+kill(?:\s+-(?:\d+|[A-Z]+))?\s*(?:2>\s*\/dev\/null)?(?:\s*;\s*echo\s+(?:"[^"]*"|'[^']*'|[A-Za-z0-9_ .:-]+))?$/.test(trimmed);
165
+ }
166
+
167
+ function isApprovedProcessCleanupCommand(command) {
168
+ const trimmed = String(command || '').trim();
169
+ if (!trimmed) return false;
170
+ if (isPortLsofXargsKill(trimmed)) return true;
171
+ const segments = splitCommand(trimmed);
172
+ if (segments.length === 0) return false;
173
+ return segments.every(segment => {
174
+ const sub = segment.trim();
175
+ return isNumericKillCommand(sub) ||
176
+ isPortLsofKillSubstitution(sub) ||
177
+ isPortLsofXargsKill(sub) ||
178
+ isQuotedMessage(sub);
179
+ });
180
+ }
181
+
147
182
  // ═══════════════════════════════════════════════════════════════════
148
183
  // Section 3: Read-Only Command Allowlist
149
184
  // ═══════════════════════════════════════════════════════════════════
@@ -207,6 +242,10 @@ const COMMAND_ALLOWLIST = {
207
242
  find: { flags: { '-name': 'string', '-iname': 'string', '-path': 'string', '-ipath': 'string', '-type': 'string', '-maxdepth': 'number', '-mindepth': 'number', '-newer': 'string', '-size': 'string', '-mtime': 'string', '-atime': 'string', '-ctime': 'string', '-perm': 'string', '-user': 'string', '-group': 'string', '-not': 'none', '!': 'none', '-or': 'none', '-o': 'none', '-and': 'none', '-a': 'none', '-print': 'none', '-print0': 'none', '-empty': 'none', '-readable': 'none', '-writable': 'none', '-executable': 'none', '-follow': 'none', '-L': 'none', '-P': 'none', '-H': 'none', '-xdev': 'none', '-mount': 'none', '-daystart': 'none', '-regextype': 'string', '-regex': 'string', '-iregex': 'string' },
208
243
  blocked: ['-exec', '-execdir', '-ok', '-okdir', '-delete', '-fprint', '-fprint0', '-fls', '-fprintf'] },
209
244
 
245
+ // ── sed (read-only subset — NO -i/--in-place) ──
246
+ sed: { flags: { '-n': 'none', '--quiet': 'none', '--silent': 'none', '-E': 'none', '-r': 'none', '--regexp-extended': 'none', '-e': 'string', '--expression': 'string', '-f': 'string', '--file': 'string', '-l': 'number', '--line-length': 'number', '-u': 'none', '--unbuffered': 'none' },
247
+ blocked: ['-i', '--in-place', '-z', '--null-data', '-s', '--separate'] },
248
+
210
249
  // ── ls ──
211
250
  ls: { flags: { '-l': 'none', '-a': 'none', '-A': 'none', '-h': 'none', '--human-readable': 'none', '-R': 'none', '--recursive': 'none', '-S': 'none', '-t': 'none', '-r': 'none', '--reverse': 'none', '-1': 'none', '-d': 'none', '--directory': 'none', '-F': 'none', '--classify': 'none', '-i': 'none', '--inode': 'none', '-s': 'none', '--size': 'none', '--color': 'string', '--sort': 'string', '--time': 'string', '--group-directories-first': 'none', '-p': 'none', '-G': 'none', '-n': 'none', '--numeric-uid-gid': 'none' } },
212
251
 
@@ -295,6 +334,7 @@ const BLOCKED_PATTERNS = [
295
334
  /curl.*\|\s*(ba)?sh/, // pipe curl to shell
296
335
  /wget.*\|\s*(ba)?sh/, // pipe wget to shell
297
336
  /eval\s*\$\(/, // eval command substitution
337
+ /\bfind\s+\/(?:\s|$)/, // find / scans the whole VM
298
338
  /find\s.*-exec/, // find with -exec (code execution)
299
339
  /find\s.*-delete/, // find with -delete
300
340
  ];
@@ -349,6 +389,7 @@ function validateFlags(tokens, startIdx, config) {
349
389
  if (blocked) {
350
390
  for (const b of blocked) {
351
391
  if (token === b || token.startsWith(b + '=')) return false;
392
+ if (/^-[A-Za-z]$/.test(b) && token.startsWith(b) && token.length > b.length) return false;
352
393
  }
353
394
  }
354
395
 
@@ -449,6 +490,14 @@ export function classifyCommand(command) {
449
490
 
450
491
  const trimmed = command.trim();
451
492
 
493
+ if (isApprovedProcessCleanupCommand(trimmed)) {
494
+ return {
495
+ classification: 'contained',
496
+ reason: 'Process cleanup by numeric PID or lsof port lookup; requires approval',
497
+ highRisk: true,
498
+ };
499
+ }
500
+
452
501
  // ── Step 1: Check for always-blocked patterns ──
453
502
  for (const pattern of BLOCKED_PATTERNS) {
454
503
  if (pattern.test(trimmed)) {
@@ -461,6 +510,10 @@ export function classifyCommand(command) {
461
510
  return { classification: 'blocked', reason: 'Contains command substitution (backticks or $())' };
462
511
  }
463
512
 
513
+ if (containsUnsafeOutputRedirection(trimmed)) {
514
+ return { classification: 'contained', reason: 'Writes shell output via redirection' };
515
+ }
516
+
464
517
  // ── Step 3: Split compound commands and classify each ──
465
518
  const subcommands = splitCommand(trimmed);
466
519
 
@@ -500,6 +553,31 @@ export function classifyCommand(command) {
500
553
  };
501
554
  }
502
555
 
556
+ function containsUnsafeOutputRedirection(command) {
557
+ let inSingle = false;
558
+ let inDouble = false;
559
+ let escaped = false;
560
+ for (let i = 0; i < command.length; i++) {
561
+ const ch = command[i];
562
+ if (escaped) { escaped = false; continue; }
563
+ if (ch === '\\' && !inSingle) { escaped = true; continue; }
564
+ if (ch === "'" && !inDouble) { inSingle = !inSingle; continue; }
565
+ if (ch === '"' && !inSingle) { inDouble = !inDouble; continue; }
566
+ if (inSingle || inDouble || ch !== '>') continue;
567
+
568
+ const prev = command[i - 1] || '';
569
+ const next = command[i + 1] || '';
570
+ if (next === '&') continue;
571
+ let cursor = next === '>' ? i + 2 : i + 1;
572
+ while (/\s/.test(command[cursor] || '')) cursor++;
573
+ const target = command.slice(cursor).match(/^[^\s;&|]+/)?.[0] || '';
574
+ if (target === '/dev/null') continue;
575
+ if (prev === '2' && target === '/dev/null') continue;
576
+ return true;
577
+ }
578
+ return false;
579
+ }
580
+
503
581
  /**
504
582
  * Classify a single (non-compound) command.
505
583
  */
@@ -0,0 +1,145 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+
4
+ const FILES = {
5
+ 'README.md': `# .kepler/ - project agent context
6
+
7
+ Kepler reads and writes here to keep state between sessions.
8
+
9
+ ## Files Kepler writes
10
+ - \`plan.md\` - current agent plan
11
+ - \`goal.md\` - durable session goal
12
+ - \`tasks/\` - task list
13
+ - \`reports/*.md\` - end-of-turn mission reports
14
+ - \`sessions/*.jsonl\` - turn transcripts
15
+ - \`commands.log\` - command executions
16
+ - \`approvals.log\` - HITL decisions
17
+ - \`trust.json\` - approved patterns
18
+
19
+ ## Files you can write
20
+ - \`config.json\` - project policy
21
+ - \`project.md\` - durable project context
22
+ - \`style.md\` - codebase conventions
23
+ - \`hitl.md\` - approval guidance
24
+ - \`skills/<name>/SKILL.md\` - reusable domain skills
25
+
26
+ Format v1. Markdown files are intentionally hand-editable.
27
+ `,
28
+ 'config.json': JSON.stringify({
29
+ version: 1,
30
+ context: {
31
+ loadEveryTurn: ['KEPLER.md', 'project.md', 'style.md', 'goal.md', 'plan.md', 'tasks/*.md'],
32
+ showReloadNotice: true,
33
+ },
34
+ planning: { owner: 'auto', onUserEditedPlan: 'prefer_user_plan' },
35
+ tasks: { storage: 'project_markdown', syncTodoWrite: true, resumePrompt: true },
36
+ hitl: {
37
+ defaultScope: 'once',
38
+ allowSessionTrust: true,
39
+ allowProjectTrust: false,
40
+ reaskAfterMinutes: 30,
41
+ },
42
+ commands: {
43
+ enabled: ['map', 'probe', 'footprint', 'heal', 'align', 'distill', 'brief', 'rewind'],
44
+ dryRunDefault: false,
45
+ },
46
+ }, null, 2) + '\n',
47
+ 'settings.json': JSON.stringify({
48
+ env: {},
49
+ permissions: {
50
+ shellAllowlist: ['git', 'npm', 'pnpm'],
51
+ editDenylist: ['**/*.env', 'secrets/**'],
52
+ },
53
+ hooks: {
54
+ UserPromptSubmit: [],
55
+ PreToolUse: [],
56
+ PostToolUse: [],
57
+ Stop: [],
58
+ },
59
+ }, null, 2) + '\n',
60
+ 'KEPLER.md': `# Project
61
+
62
+ ## Quick Facts
63
+ - Stack:
64
+ - Test command:
65
+ - Lint command:
66
+ - Build command:
67
+
68
+ ## Key Directories
69
+
70
+ ## Code Style
71
+
72
+ ## Critical Rules
73
+ `,
74
+ 'project.md': '# Project Context\n\nAdd durable project context here.\n',
75
+ 'style.md': '# Style\n\nAdd code and communication conventions here.\n',
76
+ 'hitl.md': '# HITL Guidance\n\nAdd project-specific approval guidance here.\n',
77
+ 'trust.json': JSON.stringify({ version: 1, rules: [] }, null, 2) + '\n',
78
+ 'tasks/README.md': `# Kepler Tasks
79
+
80
+ Checklist files are read every turn.
81
+
82
+ - \`backlog.md\` - pending tasks
83
+ - \`active.md\` - current task
84
+ - \`done.md\` - completed tasks
85
+ - \`blocked.md\` - waiting on input
86
+ `,
87
+ 'tasks/backlog.md': '# Backlog\n\n',
88
+ 'tasks/active.md': '# Active\n\n',
89
+ 'tasks/done.md': '# Done\n\n',
90
+ 'tasks/blocked.md': '# Blocked\n\n',
91
+ 'skills/starter/SKILL.md': `---
92
+ name: starter
93
+ description: Project-specific conventions and setup notes. Use when onboarding to this repo.
94
+ ---
95
+
96
+ # Starter Skill
97
+
98
+ Add reusable project knowledge here.
99
+ `,
100
+ 'commands/onboard.md': `---
101
+ name: onboard
102
+ description: Explore the project and record onboarding notes.
103
+ ---
104
+
105
+ # Onboard
106
+
107
+ Context:
108
+ $ARGUMENTS
109
+
110
+ Explore the codebase, ask clarifying questions, and record useful notes in .kepler/tasks/active.md.
111
+ `,
112
+ };
113
+
114
+ export function scaffoldKeplerProject({ cwd = process.cwd(), force = false } = {}) {
115
+ const root = path.join(cwd, '.kepler');
116
+ const written = [];
117
+ const skipped = [];
118
+ for (const [rel, content] of Object.entries(FILES)) {
119
+ const filePath = path.join(root, rel);
120
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
121
+ if (fs.existsSync(filePath) && !force) {
122
+ skipped.push(filePath);
123
+ continue;
124
+ }
125
+ fs.writeFileSync(filePath, content);
126
+ written.push(filePath);
127
+ }
128
+
129
+ const gitignore = path.join(root, '.gitignore');
130
+ if (!fs.existsSync(gitignore) || force) {
131
+ fs.writeFileSync(gitignore, 'settings.local.json\nsessions/\nreports/\n*.log\n');
132
+ written.push(gitignore);
133
+ }
134
+
135
+ return { root, written, skipped };
136
+ }
137
+
138
+ export async function runInitCommand(args = [], { cwd = process.cwd() } = {}) {
139
+ const force = args.includes('--force');
140
+ const result = scaffoldKeplerProject({ cwd, force });
141
+ process.stderr.write(`\x1b[32m✓\x1b[0m Initialized .kepler at ${result.root}\n`);
142
+ process.stderr.write(` wrote ${result.written.length} files`);
143
+ if (result.skipped.length) process.stderr.write(`, skipped ${result.skipped.length} existing files`);
144
+ process.stderr.write('\n');
145
+ }
@@ -47,6 +47,12 @@ async function main() {
47
47
  return;
48
48
  }
49
49
 
50
+ if (subcommand === 'init') {
51
+ const { runInitCommand } = await import('./init.mjs');
52
+ await runInitCommand(subcommandArgs);
53
+ return;
54
+ }
55
+
50
56
  if (subcommand === 'skills' || subcommand === 'skill') {
51
57
  const { runSkillsCommand } = await import('./skills.mjs');
52
58
  try {
@@ -103,6 +109,7 @@ async function main() {
103
109
  kepler dashboard Open Kepler Pulse analytics dashboard
104
110
  kepler login Sign in via browser
105
111
  kepler logout Sign out and clear credentials
112
+ kepler init Scaffold .kepler config, memory, hooks, tasks
106
113
  kepler version Show version
107
114
 
108
115
  \x1b[1mAnalytics:\x1b[0m
@@ -155,6 +162,8 @@ async function main() {
155
162
  model: args.model,
156
163
  timeout: args.timeout || (args.maxTurns ? args.maxTurns * 60 : 600),
157
164
  verbose: args.verbose,
165
+ cacheReport: args.cacheReport,
166
+ local: args.local,
158
167
  });
159
168
  return;
160
169
  }