@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.
- package/KEPLER-README.md +128 -2
- package/package.json +4 -4
- package/pulse/app/activity/page.tsx +1 -1
- package/pulse/app/api/import/route.ts +1 -1
- package/pulse/app/api/memory/route.ts +2 -2
- package/pulse/app/costs/page.tsx +1 -1
- package/pulse/app/export/page.tsx +3 -3
- package/pulse/app/globals.css +3 -3
- package/pulse/app/help/page.tsx +11 -11
- package/pulse/app/history/page.tsx +2 -2
- package/pulse/app/layout.tsx +2 -2
- package/pulse/app/memory/page.tsx +2 -2
- package/pulse/app/overview-client.tsx +1 -1
- package/pulse/app/page.tsx +2 -2
- package/pulse/app/plans/page.tsx +2 -2
- package/pulse/app/projects/page.tsx +1 -1
- package/pulse/app/sessions/page.tsx +1 -1
- package/pulse/app/settings/page.tsx +4 -4
- package/pulse/app/todos/page.tsx +2 -2
- package/pulse/app/tools/page.tsx +1 -1
- package/pulse/cli.js +15 -25
- package/pulse/components/layout/sidebar.tsx +2 -2
- package/pulse/components/sessions/replay/user-tool-result.tsx +1 -1
- package/pulse/lib/claude-reader.ts +1 -1
- package/pulse/lib/decode.ts +1 -1
- package/pulse/package.json +3 -3
- package/src/auth/tarang-auth.mjs +1 -1
- package/src/config/cli-args.mjs +5 -0
- package/src/context/retriever.mjs +1 -1
- package/src/context/skeleton.mjs +1 -1
- package/src/core/approval.mjs +22 -53
- package/src/core/headless.mjs +68 -24
- package/src/core/paths.mjs +1 -1
- package/src/core/project-artifacts.mjs +37 -0
- package/src/core/stream-client.mjs +6 -1
- package/src/core/tool-executor.mjs +163 -55
- package/src/skills/installer.mjs +188 -0
- package/src/skills/loader.mjs +217 -112
- package/src/terminal/main.mjs +19 -1
- package/src/terminal/repl.mjs +40 -105
- package/src/terminal/skills.mjs +54 -0
- package/src/terminal/tool-display.mjs +82 -0
- package/src/tools/bash.mjs +5 -2
- package/src/tools/project-overview.mjs +418 -0
- package/src/tools/registry.mjs +0 -16
- package/src/ui/banner.mjs +7 -14
- package/src/ui/formatter.mjs +6 -40
- 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;
|
package/src/tools/bash.mjs
CHANGED
|
@@ -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
|
+
}
|
package/src/tools/registry.mjs
CHANGED
|
@@ -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
|
|
21
|
+
* Print the branded orbital banner.
|
|
31
22
|
*/
|
|
32
23
|
export function printBanner() {
|
|
33
24
|
process.stderr.write('\n');
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
process.stderr.write(
|
|
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
|
|
package/src/ui/formatter.mjs
CHANGED
|
@@ -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
|
-
|
|
183
|
-
const
|
|
184
|
-
|
|
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;
|