@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.
@@ -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
+ }
@@ -162,6 +162,8 @@ async function main() {
162
162
  model: args.model,
163
163
  timeout: args.timeout || (args.maxTurns ? args.maxTurns * 60 : 600),
164
164
  verbose: args.verbose,
165
+ cacheReport: args.cacheReport,
166
+ local: args.local,
165
167
  });
166
168
  return;
167
169
  }