@axplusb/kepler 1.0.5 → 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 +34 -0
- package/package.json +4 -4
- package/src/core/headless.mjs +68 -24
- package/src/core/project-artifacts.mjs +37 -0
- 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 +18 -0
- package/src/terminal/repl.mjs +26 -47
- package/src/terminal/skills.mjs +54 -0
- package/src/tools/bash.mjs +5 -2
- package/src/tools/project-overview.mjs +418 -0
- package/src/tools/registry.mjs +0 -16
|
@@ -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
|
|