@axplusb/kepler 2.2.0 → 2.3.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/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/core/agent-loop.mjs +8 -2
- package/src/core/cache-control.mjs +92 -0
- package/src/core/compact-history.mjs +127 -0
- package/src/core/headless.mjs +85 -9
- package/src/core/jsonl-writer.mjs +9 -0
- package/src/core/local-agent.mjs +121 -14
- package/src/core/local-store.mjs +20 -4
- package/src/core/resume-mode.mjs +11 -1
- package/src/core/tasks.mjs +67 -1
- package/src/core/work-scope.mjs +217 -0
- package/src/terminal/main.mjs +2 -0
- package/src/terminal/repl.mjs +249 -17
- package/src/ui/commands.mjs +5 -4
package/src/core/tasks.mjs
CHANGED
|
@@ -67,6 +67,57 @@ export function appendTask({ cwd = process.cwd(), list = 'backlog', text }) {
|
|
|
67
67
|
return { list: normalized, path: filePath, text: taskText };
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
export function updateTask({ cwd = process.cwd(), list = 'active', index, text, checked } = {}) {
|
|
71
|
+
const normalized = normalizeList(list);
|
|
72
|
+
const taskIndex = normalizeTaskIndex(index);
|
|
73
|
+
ensureTaskFiles({ cwd });
|
|
74
|
+
const filePath = taskFilePath(cwd, normalized);
|
|
75
|
+
const content = readText(filePath);
|
|
76
|
+
const tasks = parseTaskMarkdown(content, normalized);
|
|
77
|
+
const task = tasks[taskIndex - 1];
|
|
78
|
+
if (!task) throw new Error(`No task ${taskIndex} in ${normalized}`);
|
|
79
|
+
|
|
80
|
+
const lines = content.split(/\r?\n/);
|
|
81
|
+
const nextText = String(text ?? task.text).trim();
|
|
82
|
+
if (!nextText) throw new Error('Task text is required');
|
|
83
|
+
const nextChecked = checked === undefined ? task.checked : Boolean(checked);
|
|
84
|
+
lines[task.line - 1] = taskLine(nextText, normalized, nextChecked);
|
|
85
|
+
fs.writeFileSync(filePath, lines.join('\n'));
|
|
86
|
+
return { list: normalized, path: filePath, index: taskIndex, previous: task, text: nextText, checked: nextChecked };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function removeTask({ cwd = process.cwd(), list = 'active', index } = {}) {
|
|
90
|
+
const normalized = normalizeList(list);
|
|
91
|
+
const taskIndex = normalizeTaskIndex(index);
|
|
92
|
+
ensureTaskFiles({ cwd });
|
|
93
|
+
const filePath = taskFilePath(cwd, normalized);
|
|
94
|
+
const content = readText(filePath);
|
|
95
|
+
const tasks = parseTaskMarkdown(content, normalized);
|
|
96
|
+
const task = tasks[taskIndex - 1];
|
|
97
|
+
if (!task) throw new Error(`No task ${taskIndex} in ${normalized}`);
|
|
98
|
+
|
|
99
|
+
const lines = content.split(/\r?\n/);
|
|
100
|
+
lines.splice(task.line - 1, 1);
|
|
101
|
+
fs.writeFileSync(filePath, lines.join('\n'));
|
|
102
|
+
return { list: normalized, path: filePath, index: taskIndex, task };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function moveTask({ cwd = process.cwd(), from = 'active', index, to = 'done', text } = {}) {
|
|
106
|
+
const source = normalizeList(from);
|
|
107
|
+
const target = normalizeList(to);
|
|
108
|
+
const removed = removeTask({ cwd, list: source, index });
|
|
109
|
+
const taskText = String(text ?? removed.task.text).trim();
|
|
110
|
+
const appended = appendTask({ cwd, list: target, text: taskText });
|
|
111
|
+
return {
|
|
112
|
+
from: source,
|
|
113
|
+
to: target,
|
|
114
|
+
index: removed.index,
|
|
115
|
+
text: taskText,
|
|
116
|
+
sourcePath: removed.path,
|
|
117
|
+
targetPath: appended.path,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
70
121
|
export function taskCounts(board) {
|
|
71
122
|
const lists = board?.lists || {};
|
|
72
123
|
return Object.fromEntries(
|
|
@@ -74,7 +125,7 @@ export function taskCounts(board) {
|
|
|
74
125
|
);
|
|
75
126
|
}
|
|
76
127
|
|
|
77
|
-
function normalizeList(value) {
|
|
128
|
+
export function normalizeList(value) {
|
|
78
129
|
const key = String(value || '').toLowerCase();
|
|
79
130
|
if (key === 'todo' || key === 'pending') return 'backlog';
|
|
80
131
|
if (key === 'current' || key === 'doing') return 'active';
|
|
@@ -83,6 +134,21 @@ function normalizeList(value) {
|
|
|
83
134
|
throw new Error(`Unknown task list: ${value}`);
|
|
84
135
|
}
|
|
85
136
|
|
|
137
|
+
function normalizeTaskIndex(value) {
|
|
138
|
+
const n = Number(value);
|
|
139
|
+
if (!Number.isInteger(n) || n < 1) throw new Error('Task index must be a positive number');
|
|
140
|
+
return n;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function taskFilePath(cwd, list) {
|
|
144
|
+
return path.join(cwd, '.kepler', 'tasks', TASK_FILES[list]);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function taskLine(text, list, checked = false) {
|
|
148
|
+
const mark = checked || list === 'done' ? 'x' : ' ';
|
|
149
|
+
return `- [${mark}] ${text}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
86
152
|
function readText(filePath) {
|
|
87
153
|
try {
|
|
88
154
|
return fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf-8') : '';
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import * as crypto from 'node:crypto';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
|
|
6
|
+
const SCHEMA = 'kepler.work_scope/1';
|
|
7
|
+
const ROOT_MARKERS = [
|
|
8
|
+
'.kepler',
|
|
9
|
+
'.git',
|
|
10
|
+
'package.json',
|
|
11
|
+
'pyproject.toml',
|
|
12
|
+
'setup.py',
|
|
13
|
+
'go.mod',
|
|
14
|
+
'Cargo.toml',
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
function stable(value) {
|
|
18
|
+
if (Array.isArray(value)) return value.map(stable);
|
|
19
|
+
if (value && typeof value === 'object') {
|
|
20
|
+
return Object.fromEntries(
|
|
21
|
+
Object.entries(value)
|
|
22
|
+
.filter(([, v]) => v !== undefined)
|
|
23
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
24
|
+
.map(([k, v]) => [k, stable(v)]),
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function sha(payload) {
|
|
31
|
+
return crypto
|
|
32
|
+
.createHash('sha256')
|
|
33
|
+
.update(JSON.stringify(stable(payload)))
|
|
34
|
+
.digest('hex')
|
|
35
|
+
.slice(0, 16);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizePathInput(value) {
|
|
39
|
+
let s = String(value || '').trim();
|
|
40
|
+
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
|
|
41
|
+
s = s.slice(1, -1);
|
|
42
|
+
}
|
|
43
|
+
if (s === '~' || s.startsWith('~/')) {
|
|
44
|
+
s = path.join(os.homedir(), s.slice(1));
|
|
45
|
+
}
|
|
46
|
+
return s.replace(/\\([ \t()&$;'"])/g, '$1');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function nearestProjectRoot(candidate) {
|
|
50
|
+
let resolved = normalizePathInput(candidate);
|
|
51
|
+
if (!resolved || !path.isAbsolute(resolved)) return null;
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
resolved = fs.realpathSync(resolved);
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let dir = resolved;
|
|
60
|
+
try {
|
|
61
|
+
if (fs.statSync(resolved).isFile()) dir = path.dirname(resolved);
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const root = path.parse(dir).root;
|
|
67
|
+
let current = dir;
|
|
68
|
+
while (current && current !== root) {
|
|
69
|
+
if (ROOT_MARKERS.some(marker => fs.existsSync(path.join(current, marker)))) {
|
|
70
|
+
return fs.realpathSync(current);
|
|
71
|
+
}
|
|
72
|
+
current = path.dirname(current);
|
|
73
|
+
}
|
|
74
|
+
return fs.realpathSync(dir);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function extractQuotedPaths(text) {
|
|
78
|
+
const paths = [];
|
|
79
|
+
const re = /(['"])(\/[^'"\n]+)\1/g;
|
|
80
|
+
let match;
|
|
81
|
+
while ((match = re.exec(String(text || ''))) !== null) {
|
|
82
|
+
paths.push(match[2]);
|
|
83
|
+
}
|
|
84
|
+
return paths;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function extractPastedPaths(text) {
|
|
88
|
+
const source = String(text || '');
|
|
89
|
+
const paths = [];
|
|
90
|
+
for (let i = 0; i < source.length; i++) {
|
|
91
|
+
if (source[i] !== '/') continue;
|
|
92
|
+
const line = source.slice(i).split(/\r?\n/, 1)[0].replace(/[),.;:]+$/g, '');
|
|
93
|
+
const parts = line.split(/\s+/).filter(Boolean);
|
|
94
|
+
let candidate = '';
|
|
95
|
+
let lastRoot = null;
|
|
96
|
+
for (const part of parts.slice(0, 12)) {
|
|
97
|
+
candidate = candidate ? `${candidate} ${part}` : part;
|
|
98
|
+
const root = nearestProjectRoot(candidate.replace(/[),.;:]+$/g, ''));
|
|
99
|
+
if (root) lastRoot = root;
|
|
100
|
+
}
|
|
101
|
+
if (lastRoot) paths.push(lastRoot);
|
|
102
|
+
}
|
|
103
|
+
return paths;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function uniqueRoots(entries) {
|
|
107
|
+
const seen = new Set();
|
|
108
|
+
const result = [];
|
|
109
|
+
for (const entry of entries) {
|
|
110
|
+
if (!entry?.path || seen.has(entry.path)) continue;
|
|
111
|
+
seen.add(entry.path);
|
|
112
|
+
result.push(entry);
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function roleForRoot(root, cwd) {
|
|
118
|
+
const base = path.basename(root).toLowerCase();
|
|
119
|
+
if (base.includes('backend')) return 'backend';
|
|
120
|
+
if (base.includes('frontend') || base.includes('web')) return 'frontend';
|
|
121
|
+
if (base.includes('deploy')) return 'deploy';
|
|
122
|
+
if (base.includes('docs') || base.includes('prd')) return 'docs';
|
|
123
|
+
if (base.includes('npm') || base.includes('cli')) return 'cli';
|
|
124
|
+
if (root === cwd) return 'primary';
|
|
125
|
+
return 'workspace';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function truncate(value, max = 280) {
|
|
129
|
+
const s = String(value || '').replace(/\s+/g, ' ').trim();
|
|
130
|
+
return s.length > max ? `${s.slice(0, max - 1)}…` : s;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function sortedResources(resources) {
|
|
134
|
+
return (Array.isArray(resources) ? resources : [])
|
|
135
|
+
.filter(resource => resource && resource.root)
|
|
136
|
+
.map(resource => ({
|
|
137
|
+
project_id: String(resource.project_id || ''),
|
|
138
|
+
root: String(resource.root || ''),
|
|
139
|
+
name: String(resource.name || path.basename(String(resource.root || ''))),
|
|
140
|
+
index_version: String(resource.index_version || ''),
|
|
141
|
+
}))
|
|
142
|
+
.sort((a, b) => a.root.localeCompare(b.root));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function buildWorkScope({
|
|
146
|
+
instruction = '',
|
|
147
|
+
cwd = process.cwd(),
|
|
148
|
+
projectResources = [],
|
|
149
|
+
} = {}) {
|
|
150
|
+
const cwdRoot = nearestProjectRoot(cwd) || path.resolve(cwd);
|
|
151
|
+
const roots = [{
|
|
152
|
+
path: cwdRoot,
|
|
153
|
+
role: roleForRoot(cwdRoot, cwdRoot),
|
|
154
|
+
source: 'cwd',
|
|
155
|
+
status: 'active',
|
|
156
|
+
}];
|
|
157
|
+
|
|
158
|
+
for (const raw of [...extractQuotedPaths(instruction), ...extractPastedPaths(instruction)]) {
|
|
159
|
+
const root = nearestProjectRoot(raw);
|
|
160
|
+
if (!root) continue;
|
|
161
|
+
roots.push({
|
|
162
|
+
path: root,
|
|
163
|
+
role: roleForRoot(root, cwdRoot),
|
|
164
|
+
source: 'prompt',
|
|
165
|
+
status: 'active',
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
for (const resource of sortedResources(projectResources)) {
|
|
170
|
+
roots.push({
|
|
171
|
+
path: resource.root,
|
|
172
|
+
role: roleForRoot(resource.root, cwdRoot),
|
|
173
|
+
source: 'registered',
|
|
174
|
+
status: 'active',
|
|
175
|
+
project_id: resource.project_id || undefined,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const activeRoots = uniqueRoots(roots);
|
|
180
|
+
const resources = sortedResources(projectResources);
|
|
181
|
+
const scope = {
|
|
182
|
+
schema: SCHEMA,
|
|
183
|
+
primary_root: cwdRoot,
|
|
184
|
+
intent: truncate(instruction),
|
|
185
|
+
active_roots: activeRoots,
|
|
186
|
+
candidate_roots: [],
|
|
187
|
+
workspace_resources: resources,
|
|
188
|
+
cache_policy: {
|
|
189
|
+
stable_system: false,
|
|
190
|
+
placement: 'pinned_context',
|
|
191
|
+
reason: 'scope changes with user intent and discovered roots',
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
scope.version = sha({
|
|
195
|
+
schema: scope.schema,
|
|
196
|
+
primary_root: scope.primary_root,
|
|
197
|
+
intent: scope.intent,
|
|
198
|
+
active_roots: scope.active_roots,
|
|
199
|
+
workspace_resources: scope.workspace_resources,
|
|
200
|
+
});
|
|
201
|
+
return scope;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function summarizeWorkScope(scope) {
|
|
205
|
+
if (!scope || typeof scope !== 'object') return '';
|
|
206
|
+
const roots = Array.isArray(scope.active_roots) ? scope.active_roots : [];
|
|
207
|
+
const lines = [
|
|
208
|
+
`Work scope ${scope.version || 'unknown'}`,
|
|
209
|
+
`Primary: ${scope.primary_root || '(unknown)'}`,
|
|
210
|
+
];
|
|
211
|
+
if (scope.intent) lines.push(`Intent: ${scope.intent}`);
|
|
212
|
+
for (const root of roots) {
|
|
213
|
+
if (!root?.path) continue;
|
|
214
|
+
lines.push(`- ${root.path} [${root.role || 'workspace'}; ${root.source || 'unknown'}]`);
|
|
215
|
+
}
|
|
216
|
+
return lines.join('\n');
|
|
217
|
+
}
|
package/src/terminal/main.mjs
CHANGED