@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
|
@@ -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
|
+
}
|
package/src/terminal/main.mjs
CHANGED
|
@@ -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
|
}
|