@axplusb/kepler 1.0.4 → 1.0.9

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 (48) hide show
  1. package/KEPLER-README.md +128 -2
  2. package/package.json +4 -4
  3. package/pulse/app/activity/page.tsx +1 -1
  4. package/pulse/app/api/import/route.ts +1 -1
  5. package/pulse/app/api/memory/route.ts +2 -2
  6. package/pulse/app/costs/page.tsx +1 -1
  7. package/pulse/app/export/page.tsx +3 -3
  8. package/pulse/app/globals.css +3 -3
  9. package/pulse/app/help/page.tsx +11 -11
  10. package/pulse/app/history/page.tsx +2 -2
  11. package/pulse/app/layout.tsx +2 -2
  12. package/pulse/app/memory/page.tsx +2 -2
  13. package/pulse/app/overview-client.tsx +1 -1
  14. package/pulse/app/page.tsx +2 -2
  15. package/pulse/app/plans/page.tsx +2 -2
  16. package/pulse/app/projects/page.tsx +1 -1
  17. package/pulse/app/sessions/page.tsx +1 -1
  18. package/pulse/app/settings/page.tsx +4 -4
  19. package/pulse/app/todos/page.tsx +2 -2
  20. package/pulse/app/tools/page.tsx +1 -1
  21. package/pulse/cli.js +15 -25
  22. package/pulse/components/layout/sidebar.tsx +2 -2
  23. package/pulse/components/sessions/replay/user-tool-result.tsx +1 -1
  24. package/pulse/lib/claude-reader.ts +1 -1
  25. package/pulse/lib/decode.ts +1 -1
  26. package/pulse/package.json +3 -3
  27. package/src/auth/tarang-auth.mjs +1 -1
  28. package/src/config/cli-args.mjs +5 -0
  29. package/src/context/retriever.mjs +1 -1
  30. package/src/context/skeleton.mjs +1 -1
  31. package/src/core/approval.mjs +22 -53
  32. package/src/core/headless.mjs +68 -24
  33. package/src/core/paths.mjs +1 -1
  34. package/src/core/project-artifacts.mjs +37 -0
  35. package/src/core/stream-client.mjs +6 -1
  36. package/src/core/tool-executor.mjs +163 -55
  37. package/src/skills/installer.mjs +188 -0
  38. package/src/skills/loader.mjs +217 -112
  39. package/src/terminal/main.mjs +19 -1
  40. package/src/terminal/repl.mjs +40 -105
  41. package/src/terminal/skills.mjs +54 -0
  42. package/src/terminal/tool-display.mjs +82 -0
  43. package/src/tools/bash.mjs +5 -2
  44. package/src/tools/project-overview.mjs +418 -0
  45. package/src/tools/registry.mjs +0 -16
  46. package/src/ui/banner.mjs +7 -14
  47. package/src/ui/formatter.mjs +6 -40
  48. package/README.md.orca +0 -82
@@ -21,6 +21,10 @@ const TOOL_LABELS = Object.freeze({
21
21
  analyze_code: 'Analyze code',
22
22
  explore: 'Explore codebase',
23
23
  plan: 'Create implementation plan',
24
+ verify: 'Verify implementation',
25
+ debug: 'Debug issue',
26
+ refactor: 'Refactor code',
27
+ ask_user: 'Ask for input',
24
28
  });
25
29
 
26
30
  export function toolDisplayLabel(tool) {
@@ -36,6 +40,84 @@ export function toolDisplayLabel(tool) {
36
40
  .join(' ');
37
41
  }
38
42
 
43
+ function currentWorkingDirectory() {
44
+ try {
45
+ return process.cwd();
46
+ } catch {
47
+ return '';
48
+ }
49
+ }
50
+
51
+ function shortPath(filePath, cwd = currentWorkingDirectory()) {
52
+ const value = String(filePath || '');
53
+ if (cwd && value.startsWith(`${cwd}/`)) return value.slice(cwd.length + 1);
54
+ return value;
55
+ }
56
+
57
+ export function toolDisplaySummary(tool, args = {}, { cwd } = {}) {
58
+ switch (tool) {
59
+ case 'shell':
60
+ return args.command || '(empty command)';
61
+ case 'read_file': {
62
+ const filePath = shortPath(args.file_path || args.path, cwd);
63
+ if (args.start_line && args.end_line) {
64
+ return `${filePath} · lines ${args.start_line}-${args.end_line}`;
65
+ }
66
+ if (args.start_line) return `${filePath} · from line ${args.start_line}`;
67
+ return filePath;
68
+ }
69
+ case 'read_files':
70
+ return (args.file_paths || args.paths || [])
71
+ .map(filePath => shortPath(filePath, cwd))
72
+ .join(', ');
73
+ case 'write_file': {
74
+ const filePath = shortPath(args.file_path || args.path, cwd);
75
+ const lineCount = typeof args.content === 'string'
76
+ ? args.content.split('\n').length
77
+ : null;
78
+ return lineCount ? `${filePath} · ${lineCount} lines` : filePath;
79
+ }
80
+ case 'write_project':
81
+ return (args.files || [])
82
+ .map(file => shortPath(file.path || file.file_path, cwd))
83
+ .filter(Boolean)
84
+ .join(', ') || 'Project files';
85
+ case 'edit_file': {
86
+ const filePath = shortPath(args.file_path || args.path, cwd);
87
+ const search = String(args.search || '').trim();
88
+ return search ? `${filePath} · match "${search.slice(0, 40)}${search.length > 40 ? '...' : ''}"` : filePath;
89
+ }
90
+ case 'delete_file':
91
+ return shortPath(args.file_path || args.path, cwd);
92
+ case 'search_code':
93
+ case 'search_files':
94
+ case 'grep':
95
+ return `"${args.query || args.pattern || ''}"${args.path ? ` in ${shortPath(args.path, cwd)}` : ''}`;
96
+ case 'list_files':
97
+ return `${args.pattern || '*'}${args.path ? ` in ${shortPath(args.path, cwd)}` : ''}`;
98
+ case 'run_tests':
99
+ case 'validate_build':
100
+ case 'lint_check':
101
+ return args.command || args.path || args.file_path || '';
102
+ case 'git_diff':
103
+ case 'git_status':
104
+ return args.path ? shortPath(args.path, cwd) : '';
105
+ case 'explore':
106
+ case 'plan':
107
+ case 'verify':
108
+ case 'debug':
109
+ case 'refactor':
110
+ return args.query || args.task || args.prompt || '';
111
+ case 'ask_user':
112
+ return args.question || args.prompt || '';
113
+ default:
114
+ return Object.values(args)
115
+ .filter(value => typeof value === 'string' && value)
116
+ .join(', ')
117
+ .slice(0, 120);
118
+ }
119
+ }
120
+
39
121
  export function formatShellCommand(command, colors) {
40
122
  const tokens = String(command || '').match(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|&&|\|\||[|;<>]|[^\s]+|\s+/g) || [];
41
123
  let expectsCommand = true;
@@ -27,6 +27,7 @@ export const BashTool = {
27
27
  command: { type: 'string', description: 'The command to execute' },
28
28
  timeout: { type: 'number', description: 'Timeout in ms (max 600000)', default: 120000 },
29
29
  description: { type: 'string', description: 'Description of what this command does' },
30
+ cwd: { type: 'string', description: 'Working directory for the command' },
30
31
  run_in_background: { type: 'boolean', description: 'Run in background', default: false },
31
32
  },
32
33
  required: ['command'],
@@ -40,7 +41,7 @@ export const BashTool = {
40
41
  const timeout = Math.min(input.timeout || 120000, 600000);
41
42
 
42
43
  if (input.run_in_background) {
43
- return runBackground(input.command);
44
+ return runBackground(input.command, input.cwd);
44
45
  }
45
46
 
46
47
  return new Promise((resolve) => {
@@ -50,6 +51,7 @@ export const BashTool = {
50
51
  let exitCode = null;
51
52
 
52
53
  const proc = spawn('bash', ['-c', input.command], {
54
+ cwd: input.cwd,
53
55
  env: { ...process.env },
54
56
  stdio: ['pipe', 'pipe', 'pipe'],
55
57
  timeout: 0, // we handle timeout ourselves
@@ -120,9 +122,10 @@ export const BashTool = {
120
122
  const backgroundJobs = new Map();
121
123
  let bgJobId = 0;
122
124
 
123
- function runBackground(command) {
125
+ function runBackground(command, cwd) {
124
126
  const id = ++bgJobId;
125
127
  const proc = spawn('bash', ['-c', command], {
128
+ cwd,
126
129
  detached: true,
127
130
  stdio: ['ignore', 'pipe', 'pipe'],
128
131
  });
@@ -0,0 +1,418 @@
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
+ import { spawnSync } from 'node:child_process';
6
+ import { ContextRetriever } from '../context/retriever.mjs';
7
+ import { buildProjectSkeleton } from '../context/skeleton.mjs';
8
+ import { indexDir as getIndexDir } from '../core/paths.mjs';
9
+
10
+ const RESOURCE_FILE = 'project-resource.json';
11
+ const LANGUAGE_EXTENSIONS = new Map([
12
+ ['.py', 'Python'],
13
+ ['.js', 'JavaScript'],
14
+ ['.mjs', 'JavaScript'],
15
+ ['.ts', 'TypeScript'],
16
+ ['.tsx', 'TypeScript'],
17
+ ['.go', 'Go'],
18
+ ['.rs', 'Rust'],
19
+ ['.java', 'Java'],
20
+ ['.rb', 'Ruby'],
21
+ ['.c', 'C'],
22
+ ['.cpp', 'C++'],
23
+ ]);
24
+ const IGNORED_DIRS = new Set([
25
+ '.git', '.kepler', '.next', '.venv', '__pycache__',
26
+ 'build', 'dist', 'node_modules', 'venv',
27
+ ]);
28
+
29
+ function projectId(canonicalPath) {
30
+ return crypto.createHash('sha256').update(canonicalPath).digest('hex').slice(0, 12);
31
+ }
32
+
33
+ function isWithin(root, candidate) {
34
+ const relative = path.relative(root, candidate);
35
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
36
+ }
37
+
38
+ function canonicalizeCandidate(candidate) {
39
+ if (fs.existsSync(candidate)) return fs.realpathSync(candidate);
40
+
41
+ const missing = [];
42
+ let parent = candidate;
43
+ while (!fs.existsSync(parent)) {
44
+ const next = path.dirname(parent);
45
+ if (next === parent) break;
46
+ missing.unshift(path.basename(parent));
47
+ parent = next;
48
+ }
49
+ return path.join(fs.realpathSync(parent), ...missing);
50
+ }
51
+
52
+ function projectFingerprint(projectDir) {
53
+ const hash = crypto.createHash('sha256');
54
+ const queue = [projectDir];
55
+
56
+ while (queue.length > 0) {
57
+ const dir = queue.shift();
58
+ let entries;
59
+ try {
60
+ entries = fs.readdirSync(dir, { withFileTypes: true })
61
+ .sort((a, b) => a.name.localeCompare(b.name));
62
+ } catch {
63
+ continue;
64
+ }
65
+ for (const entry of entries) {
66
+ if (entry.name.startsWith('.') || IGNORED_DIRS.has(entry.name)) continue;
67
+ const fullPath = path.join(dir, entry.name);
68
+ if (entry.isDirectory()) {
69
+ queue.push(fullPath);
70
+ continue;
71
+ }
72
+ if (!entry.isFile()) continue;
73
+ try {
74
+ const stat = fs.statSync(fullPath);
75
+ hash.update(
76
+ `${path.relative(projectDir, fullPath)}:${stat.size}:${Math.trunc(stat.mtimeMs)}\n`
77
+ );
78
+ } catch { /* file changed during scan */ }
79
+ }
80
+ }
81
+ return hash.digest('hex').slice(0, 16);
82
+ }
83
+
84
+ function detectLanguages(projectDir) {
85
+ const counts = new Map();
86
+ const queue = [projectDir];
87
+ let scanned = 0;
88
+
89
+ while (queue.length > 0 && scanned < 500) {
90
+ const dir = queue.shift();
91
+ let entries;
92
+ try {
93
+ entries = fs.readdirSync(dir, { withFileTypes: true });
94
+ } catch {
95
+ continue;
96
+ }
97
+ for (const entry of entries) {
98
+ if (scanned >= 500) break;
99
+ if (entry.name.startsWith('.') || IGNORED_DIRS.has(entry.name)) continue;
100
+ const fullPath = path.join(dir, entry.name);
101
+ if (entry.isDirectory()) {
102
+ queue.push(fullPath);
103
+ } else if (entry.isFile()) {
104
+ scanned++;
105
+ const language = LANGUAGE_EXTENSIONS.get(path.extname(entry.name));
106
+ if (language) counts.set(language, (counts.get(language) || 0) + 1);
107
+ }
108
+ }
109
+ }
110
+
111
+ return [...counts.entries()]
112
+ .sort((a, b) => b[1] - a[1])
113
+ .slice(0, 4)
114
+ .map(([language]) => language);
115
+ }
116
+
117
+ function detectCommands(projectDir) {
118
+ const commands = {};
119
+ try {
120
+ const pkg = JSON.parse(fs.readFileSync(path.join(projectDir, 'package.json'), 'utf-8'));
121
+ if (pkg.scripts?.test) commands.test = 'npm test';
122
+ if (pkg.scripts?.build) commands.build = 'npm run build';
123
+ if (pkg.scripts?.lint) commands.lint = 'npm run lint';
124
+ } catch { /* no package.json */ }
125
+
126
+ if (
127
+ fs.existsSync(path.join(projectDir, 'pyproject.toml')) ||
128
+ fs.existsSync(path.join(projectDir, 'setup.py'))
129
+ ) {
130
+ if (!commands.test) commands.test = 'python -m pytest';
131
+ }
132
+ if (fs.existsSync(path.join(projectDir, 'Makefile')) && !commands.build) {
133
+ commands.build = 'make';
134
+ }
135
+ return commands;
136
+ }
137
+
138
+ function commandVersion(command, args = ['--version']) {
139
+ try {
140
+ const result = spawnSync(command, args, {
141
+ encoding: 'utf-8',
142
+ timeout: 2000,
143
+ windowsHide: true,
144
+ });
145
+ if (result.error || result.status !== 0) return '';
146
+ return `${result.stdout || result.stderr || ''}`.trim().split('\n')[0].slice(0, 120);
147
+ } catch {
148
+ return '';
149
+ }
150
+ }
151
+
152
+ function detectEnvironment() {
153
+ const candidates = [
154
+ ['python', 'python3'],
155
+ ['node', 'node'],
156
+ ['git', 'git'],
157
+ ['npm', 'npm'],
158
+ ['uv', 'uv'],
159
+ ['pytest', 'pytest'],
160
+ ['docker', 'docker'],
161
+ ];
162
+ const tools = {};
163
+ for (const [name, command] of candidates) {
164
+ const version = commandVersion(command);
165
+ if (version) tools[name] = version;
166
+ }
167
+ return {
168
+ platform: os.platform(),
169
+ release: os.release(),
170
+ architecture: os.arch(),
171
+ shell: process.env.SHELL || process.env.ComSpec || '',
172
+ node: process.version,
173
+ tools,
174
+ };
175
+ }
176
+
177
+ function formatResource(resource) {
178
+ const lines = [
179
+ `Project registered: ${resource.name} (project_id=${resource.project_id})`,
180
+ `Root: ${resource.root}`,
181
+ `Languages: ${resource.languages.join(', ') || 'unknown'}`,
182
+ `Index: ${resource.index_status} (${resource.index_version})`,
183
+ ];
184
+ if (resource.environment) {
185
+ const env = resource.environment;
186
+ lines.push(
187
+ `Environment: ${env.platform || 'unknown'} ${env.release || ''} ` +
188
+ `(${env.architecture || 'unknown'}), shell=${env.shell || 'unknown'}, node=${env.node || 'unknown'}`
189
+ );
190
+ const toolVersions = Object.entries(env.tools || {});
191
+ if (toolVersions.length > 0) {
192
+ lines.push(`Available tools: ${toolVersions.map(([name, version]) =>
193
+ `${name}=${version}`).join(', ')}`);
194
+ }
195
+ }
196
+ if (Object.keys(resource.commands).length > 0) {
197
+ lines.push(`Commands: ${Object.entries(resource.commands)
198
+ .map(([name, command]) => `${name}="${command}"`).join(', ')}`);
199
+ }
200
+ if (resource.skills_index && resource.skills_index.length > 0) {
201
+ lines.push(`Skills: ${resource.skills_index.map(s => s.name).join(', ')}`);
202
+ }
203
+ lines.push('', resource.overview);
204
+ if (resource.project_context) {
205
+ lines.push('', '--- Project Context ---', resource.project_context);
206
+ }
207
+ if (resource.goal) {
208
+ lines.push('', '--- Current Goal ---', resource.goal);
209
+ }
210
+ if (resource.plan) {
211
+ lines.push('', '--- Current Plan ---', resource.plan);
212
+ }
213
+ return lines.join('\n');
214
+ }
215
+
216
+ function _readIfExists(dir, filename, maxChars = 8000) {
217
+ try {
218
+ const filePath = path.join(dir, filename);
219
+ if (!fs.existsSync(filePath)) return '';
220
+ const content = fs.readFileSync(filePath, 'utf-8');
221
+ if (content.length > maxChars) {
222
+ // 70/20 head/tail truncation
223
+ const head = Math.floor(maxChars * 0.7);
224
+ const tail = Math.floor(maxChars * 0.2);
225
+ return content.slice(0, head) + '\n\n[...truncated...]\n\n' + content.slice(-tail);
226
+ }
227
+ return content;
228
+ } catch { return ''; }
229
+ }
230
+
231
+ function _scanSkills(keplerDir) {
232
+ const skillsDir = path.join(keplerDir, 'skills');
233
+ if (!fs.existsSync(skillsDir)) return [];
234
+ try {
235
+ return fs.readdirSync(skillsDir)
236
+ .filter(f => f.endsWith('.md'))
237
+ .map(f => {
238
+ const content = fs.readFileSync(path.join(skillsDir, f), 'utf-8');
239
+ const descMatch = content.match(/^#\s+.*\n+(.+)/);
240
+ return {
241
+ name: f.replace('.md', ''),
242
+ description: descMatch ? descMatch[1].slice(0, 100) : f.replace('.md', ''),
243
+ };
244
+ });
245
+ } catch { return []; }
246
+ }
247
+
248
+ export class ProjectRegistry {
249
+ constructor() {
250
+ this.projects = new Map();
251
+ this._globalIdentity = null;
252
+ this._globalPreferences = null;
253
+ this._globalSkills = null;
254
+ }
255
+
256
+ /**
257
+ * Load global context from ~/.kepler/ (once per session).
258
+ */
259
+ loadGlobalContext() {
260
+ if (this._globalIdentity !== null) return;
261
+ const globalDir = path.join(os.homedir(), '.kepler');
262
+ this._globalIdentity = _readIfExists(globalDir, 'identity.md', 4000);
263
+ this._globalPreferences = _readIfExists(globalDir, 'preferences.md', 2000);
264
+ this._globalSkills = _scanSkills(globalDir);
265
+ }
266
+
267
+ /**
268
+ * Get the global agent context (identity, preferences, skills).
269
+ */
270
+ getGlobalContext() {
271
+ this.loadGlobalContext();
272
+ return {
273
+ identity: this._globalIdentity || '',
274
+ preferences: this._globalPreferences || '',
275
+ skills: this._globalSkills || [],
276
+ };
277
+ }
278
+
279
+ async register(rawPath) {
280
+ if (!rawPath) {
281
+ throw new Error('get_project_overview requires a project path');
282
+ }
283
+ if (!path.isAbsolute(rawPath)) {
284
+ rawPath = path.resolve(process.cwd(), rawPath);
285
+ }
286
+
287
+ let root;
288
+ try {
289
+ root = fs.realpathSync(rawPath);
290
+ } catch {
291
+ throw new Error(`Project path not found: ${rawPath}`);
292
+ }
293
+ if (!fs.statSync(root).isDirectory()) {
294
+ throw new Error(`Project path is not a directory: ${root}`);
295
+ }
296
+ if (root === path.parse(root).root || root === os.homedir()) {
297
+ throw new Error(
298
+ `Refusing to index ${root} — too broad. Pass the project directory itself.`
299
+ );
300
+ }
301
+
302
+ const id = projectId(root);
303
+ const existing = this.projects.get(id);
304
+ if (existing) {
305
+ return {
306
+ already_registered: true,
307
+ resource: existing.resource,
308
+ output:
309
+ `Project already registered as project_id=${id}. ` +
310
+ `Use project_id=${id} with search_code and use absolute paths for file tools.`,
311
+ };
312
+ }
313
+
314
+ const fingerprint = projectFingerprint(root);
315
+ const retriever = new ContextRetriever(root);
316
+ const resourcePath = path.join(getIndexDir(root), RESOURCE_FILE);
317
+ let resource = null;
318
+
319
+ try {
320
+ const persisted = JSON.parse(fs.readFileSync(resourcePath, 'utf-8'));
321
+ if (persisted.index_version === fingerprint && retriever.loadIndex()) {
322
+ resource = persisted;
323
+ }
324
+ } catch { /* missing or stale index */ }
325
+
326
+ if (!resource) {
327
+ await retriever.buildIndex();
328
+ resource = {
329
+ project_id: id,
330
+ root,
331
+ name: path.basename(root),
332
+ languages: detectLanguages(root),
333
+ commands: detectCommands(root),
334
+ overview: buildProjectSkeleton(root, { maxFiles: 150, maxChars: 2500 }) ||
335
+ `Project at ${root}`,
336
+ index_status: 'ready',
337
+ index_version: fingerprint,
338
+ };
339
+ fs.writeFileSync(resourcePath, JSON.stringify(resource));
340
+ }
341
+
342
+ // Read project-level context files (.kepler/project.md, goal.md, skills/)
343
+ const keplerDir = path.join(root, '.kepler');
344
+ resource.environment = detectEnvironment();
345
+ resource.project_context = _readIfExists(keplerDir, 'project.md', 8000);
346
+ resource.goal = _readIfExists(keplerDir, 'goal.md', 2000);
347
+ resource.plan = _readIfExists(keplerDir, 'plan.md', 6000);
348
+ resource.skills_index = _scanSkills(keplerDir);
349
+
350
+ // Also check for top-level context files (AGENTS.md, CLAUDE.md, .kepler.md)
351
+ if (!resource.project_context) {
352
+ for (const name of ['.kepler.md', 'AGENTS.md', 'CLAUDE.md']) {
353
+ const content = _readIfExists(root, name, 8000);
354
+ if (content) { resource.project_context = content; break; }
355
+ }
356
+ }
357
+
358
+ this.projects.set(id, { resource, retriever });
359
+ return { already_registered: false, resource, output: formatResource(resource) };
360
+ }
361
+
362
+ resources() {
363
+ return [...this.projects.values()].map(({ resource }) => resource);
364
+ }
365
+
366
+ get(projectIdValue) {
367
+ return this.projects.get(projectIdValue) || null;
368
+ }
369
+
370
+ resolvePath(rawPath, projectIdValue, { allowMissing = false } = {}) {
371
+ let root = null;
372
+ if (projectIdValue) {
373
+ root = this.get(projectIdValue)?.resource.root || null;
374
+ if (!root) throw new Error(`Unknown project_id: ${projectIdValue}`);
375
+ }
376
+
377
+ if (!rawPath) {
378
+ if (root) return root;
379
+ if (this.projects.size === 1) return this.resources()[0].root;
380
+ throw new Error('Path requires project_id when multiple or no projects are registered');
381
+ }
382
+
383
+ let candidate;
384
+ if (path.isAbsolute(rawPath)) {
385
+ candidate = canonicalizeCandidate(path.resolve(rawPath));
386
+ } else {
387
+ if (!root) {
388
+ if (this.projects.size !== 1) {
389
+ throw new Error('Relative path requires project_id when multiple or no projects are registered');
390
+ }
391
+ root = this.resources()[0].root;
392
+ }
393
+ candidate = canonicalizeCandidate(path.resolve(root, rawPath));
394
+ }
395
+
396
+ const containingProject = [...this.projects.values()].find(({ resource }) =>
397
+ isWithin(resource.root, candidate)
398
+ );
399
+ if (!containingProject) {
400
+ throw new Error(`Path is outside registered project roots: ${rawPath}`);
401
+ }
402
+ if (!allowMissing && !fs.existsSync(candidate)) {
403
+ throw new Error(`Path not found: ${rawPath}`);
404
+ }
405
+ return candidate;
406
+ }
407
+
408
+ projectForPath(filePath) {
409
+ const candidate = canonicalizeCandidate(path.resolve(filePath));
410
+ return [...this.projects.values()].find(({ resource }) =>
411
+ isWithin(resource.root, candidate)
412
+ ) || null;
413
+ }
414
+
415
+ reset() {
416
+ this.projects.clear();
417
+ }
418
+ }
@@ -29,10 +29,6 @@ import { CronDeleteTool } from './cron-delete.mjs';
29
29
  import { CronListTool } from './cron-list.mjs';
30
30
  import { LspTool } from './lsp.mjs';
31
31
  import { ReadMcpResourceTool } from './read-mcp-resource.mjs';
32
- import { ContextRetriever } from '../context/retriever.mjs';
33
-
34
- // Tools that modify files — trigger index update after success
35
- const WRITE_TOOLS = new Set(['Edit', 'Write', 'MultiEdit']);
36
32
 
37
33
  const BUILTIN_TOOLS = [
38
34
  BashTool,
@@ -84,18 +80,6 @@ export function createToolRegistry() {
84
80
  if (errors.length > 0) return `Validation error: ${errors.join(', ')}`;
85
81
  const result = await tool.call(input);
86
82
 
87
- // After successful file writes, update BM25 index incrementally
88
- // so search stays fresh within the session (~5-50ms, non-blocking)
89
- if (WRITE_TOOLS.has(name) && typeof result === 'string' && !result.startsWith('Error')) {
90
- const filePath = input.file_path;
91
- if (filePath) {
92
- try {
93
- const retriever = new ContextRetriever();
94
- retriever.updateFile(filePath);
95
- } catch { /* index update is best-effort */ }
96
- }
97
- }
98
-
99
83
  return result;
100
84
  },
101
85
 
package/src/ui/banner.mjs CHANGED
@@ -17,24 +17,17 @@ const BLUE = '\x1b[34m';
17
17
  const BOLD_GREEN = '\x1b[1;32m';
18
18
  const BOLD_CYAN = '\x1b[1;36m';
19
19
 
20
- const BANNER_KEPLER = [
21
- '██╗ ██╗ ███████╗ ██████╗ ██╗ ███████╗ ██████╗ ',
22
- '██║ ██╔╝ ██╔════╝ ██╔══██╗ ██║ ██╔════╝ ██╔══██╗',
23
- '█████╔╝ █████╗ ██████╔╝ ██║ █████╗ ██████╔╝',
24
- '██╔═██╗ ██╔══╝ ██╔═══╝ ██║ ██╔══╝ ██╔══██╗',
25
- '██║ ██╗ ███████╗ ██║ ███████╗ ███████╗ ██║ ██║',
26
- '╚═╝ ╚═╝ ╚══════╝ ╚═╝ ╚══════╝ ╚══════╝ ╚═╝ ╚═╝',
27
- ];
28
-
29
20
  /**
30
- * Print the branded ASCII art banner.
21
+ * Print the branded orbital banner.
31
22
  */
32
23
  export function printBanner() {
33
24
  process.stderr.write('\n');
34
- for (const line of BANNER_KEPLER) {
35
- process.stderr.write(` ${BOLD_CYAN}${line}${RESET}\n`);
36
- }
37
- process.stderr.write(` ${DIM}AI Coding Agent — codekepler.ai${RESET}\n`);
25
+ process.stderr.write(`${DIM} ✦${RESET}\n`);
26
+ process.stderr.write(`${DIM} ╭──────────────────────────╮${RESET}\n`);
27
+ process.stderr.write(`${DIM} │${RESET} ${BOLD}${CYAN}K · E · P · L · E · R${RESET} ${DIM}│${RESET}\n`);
28
+ process.stderr.write(`${DIM} ╰──────── ${YELLOW}◯${RESET}${DIM} ───────────────╯${RESET}\n`);
29
+ process.stderr.write(`${DIM} ╱ ╲${RESET}\n`);
30
+ process.stderr.write(`${DIM} the agentic os${RESET}\n`);
38
31
  process.stderr.write('\n');
39
32
  }
40
33
 
@@ -8,6 +8,8 @@
8
8
  * White for content and summaries
9
9
  */
10
10
 
11
+ import { toolDisplayLabel, toolDisplaySummary } from '../terminal/tool-display.mjs';
12
+
11
13
  const RESET = '\x1b[0m';
12
14
  const BOLD = '\x1b[1m';
13
15
  const DIM = '\x1b[2m';
@@ -179,50 +181,14 @@ export class EventFormatter {
179
181
 
180
182
  this.toolCount++;
181
183
 
182
- // Build human-readable description
183
- const desc = this._toolDescription(tool, args);
184
- process.stderr.write(` ${this._spinner()} [${this.toolCount}] ${CYAN}${tool}: ${desc}${RESET}\n`);
184
+ const label = toolDisplayLabel(tool);
185
+ const summary = toolDisplaySummary(tool, args);
186
+ const detail = summary ? `${DIM}${summary}${RESET}` : '';
187
+ process.stderr.write(` ${this._spinner()} [${this.toolCount}] ${CYAN}${label}${RESET}${detail ? ` ${detail}` : ''}\n`);
185
188
 
186
189
  this.toolCalls.push({ name: tool, callId, startTime: Date.now() });
187
190
  }
188
191
 
189
- _toolDescription(tool, args) {
190
- switch (tool) {
191
- case 'read_file':
192
- return `Reading ${args.file_path || 'file'}...`;
193
- case 'read_files': {
194
- const paths = args.file_paths || [];
195
- return `Reading ${paths.length} files...`;
196
- }
197
- case 'write_file':
198
- return `Writing ${args.file_path || 'file'}...`;
199
- case 'write_project': {
200
- const files = args.files || [];
201
- return `Writing ${files.length} files...`;
202
- }
203
- case 'edit_file':
204
- return `Editing ${args.file_path || 'file'}...`;
205
- case 'list_files':
206
- return `Listing files${args.pattern ? ' (' + args.pattern + ')' : ''}...`;
207
- case 'search_files':
208
- return `Searching for "${args.pattern || ''}"...`;
209
- case 'search_code':
210
- return `Searching code: ${args.query || ''}...`;
211
- case 'shell': {
212
- const cmd = (args.command || '').slice(0, 60);
213
- return `Running: ${cmd}${(args.command || '').length > 60 ? '...' : ''}`;
214
- }
215
- case 'validate_build':
216
- return `Running build validation...`;
217
- case 'validate_file':
218
- return `Validating ${args.file_path || 'file'}...`;
219
- case 'lint_check':
220
- return `Linting ${args.file_path || 'file'}...`;
221
- default:
222
- return `${tool}...`;
223
- }
224
- }
225
-
226
192
  _toolDone(data) {
227
193
  const tool = data?.tool || '';
228
194
  const success = data?.success !== false;