klyro 0.1.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/LICENSE +21 -0
- package/README.md +50 -0
- package/dist/agent/anthropic-adapter.d.ts +75 -0
- package/dist/agent/anthropic-adapter.js +297 -0
- package/dist/agent/message.d.ts +40 -0
- package/dist/agent/message.js +21 -0
- package/dist/agent/observation.d.ts +47 -0
- package/dist/agent/observation.js +53 -0
- package/dist/agent/provider-adapter.d.ts +101 -0
- package/dist/agent/provider-adapter.js +254 -0
- package/dist/agent/registry.d.ts +42 -0
- package/dist/agent/registry.js +86 -0
- package/dist/agent/retry.d.ts +34 -0
- package/dist/agent/retry.js +91 -0
- package/dist/agent/runtime.d.ts +130 -0
- package/dist/agent/runtime.js +221 -0
- package/dist/agent/worker-spawner.d.ts +41 -0
- package/dist/agent/worker-spawner.js +76 -0
- package/dist/chat.d.ts +39 -0
- package/dist/chat.js +235 -0
- package/dist/chat.js.map +1 -0
- package/dist/cli/eval.d.ts +75 -0
- package/dist/cli/eval.js +190 -0
- package/dist/cli/repl.d.ts +15 -0
- package/dist/cli/repl.js +194 -0
- package/dist/cli/run.d.ts +67 -0
- package/dist/cli/run.js +198 -0
- package/dist/cli/slash/parser.d.ts +39 -0
- package/dist/cli/slash/parser.js +47 -0
- package/dist/context/level6.d.ts +45 -0
- package/dist/context/level6.js +231 -0
- package/dist/context/level7.d.ts +84 -0
- package/dist/context/level7.js +156 -0
- package/dist/context/project-map.d.ts +48 -0
- package/dist/context/project-map.js +438 -0
- package/dist/context/repo-map.d.ts +30 -0
- package/dist/context/repo-map.js +168 -0
- package/dist/context/selector.d.ts +22 -0
- package/dist/context/selector.js +37 -0
- package/dist/context/snippets.d.ts +17 -0
- package/dist/context/snippets.js +15 -0
- package/dist/context/tokenizer.d.ts +49 -0
- package/dist/context/tokenizer.js +97 -0
- package/dist/eval/harness.d.ts +48 -0
- package/dist/eval/harness.js +149 -0
- package/dist/eval/tasks.d.ts +6 -0
- package/dist/eval/tasks.js +98 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +128 -0
- package/dist/index.js.map +1 -0
- package/dist/persistence/audit.d.ts +75 -0
- package/dist/persistence/audit.js +15 -0
- package/dist/persistence/store.d.ts +67 -0
- package/dist/persistence/store.js +106 -0
- package/dist/policy/approval.d.ts +35 -0
- package/dist/policy/approval.js +54 -0
- package/dist/policy/engine.d.ts +76 -0
- package/dist/policy/engine.js +163 -0
- package/dist/policy/path-guard.d.ts +30 -0
- package/dist/policy/path-guard.js +100 -0
- package/dist/policy/secret-redactor.d.ts +19 -0
- package/dist/policy/secret-redactor.js +52 -0
- package/dist/providers.d.ts +23 -0
- package/dist/providers.js +83 -0
- package/dist/repl.d.ts +9 -0
- package/dist/repl.js +170 -0
- package/dist/repl.js.map +1 -0
- package/dist/tools/fs/edit-file.d.ts +48 -0
- package/dist/tools/fs/edit-file.js +84 -0
- package/dist/tools/fs/list-dir.d.ts +40 -0
- package/dist/tools/fs/list-dir.js +72 -0
- package/dist/tools/fs/read-file.d.ts +66 -0
- package/dist/tools/fs/read-file.js +75 -0
- package/dist/tools/fs/write-file.d.ts +24 -0
- package/dist/tools/fs/write-file.js +48 -0
- package/dist/tools/git/git-diff.d.ts +35 -0
- package/dist/tools/git/git-diff.js +68 -0
- package/dist/tools/git/git-status.d.ts +29 -0
- package/dist/tools/git/git-status.js +57 -0
- package/dist/tools/normalize.d.ts +31 -0
- package/dist/tools/normalize.js +88 -0
- package/dist/tools/registry.d.ts +23 -0
- package/dist/tools/registry.js +87 -0
- package/dist/tools/schema.d.ts +29 -0
- package/dist/tools/schema.js +183 -0
- package/dist/tools/search/dependencies.d.ts +44 -0
- package/dist/tools/search/dependencies.js +188 -0
- package/dist/tools/search/glob.d.ts +26 -0
- package/dist/tools/search/glob.js +88 -0
- package/dist/tools/search/grep.d.ts +52 -0
- package/dist/tools/search/grep.js +125 -0
- package/dist/tools/search/recent-files.d.ts +33 -0
- package/dist/tools/search/recent-files.js +100 -0
- package/dist/tools/search/search-files.d.ts +39 -0
- package/dist/tools/search/search-files.js +110 -0
- package/dist/tools/shell/shell-exec.d.ts +60 -0
- package/dist/tools/shell/shell-exec.js +133 -0
- package/dist/tools/types.d.ts +54 -0
- package/dist/tools/types.js +15 -0
- package/dist/tools/verify/run-verify.d.ts +60 -0
- package/dist/tools/verify/run-verify.js +115 -0
- package/dist/tui/app.d.ts +27 -0
- package/dist/tui/app.js +79 -0
- package/dist/tui/app.test.d.ts +1 -0
- package/dist/tui/app.test.js +77 -0
- package/dist/tui/status.d.ts +21 -0
- package/dist/tui/status.js +13 -0
- package/dist/tui/status.test.d.ts +1 -0
- package/dist/tui/status.test.js +39 -0
- package/dist/tui/transcript.d.ts +40 -0
- package/dist/tui/transcript.js +28 -0
- package/dist/tui/transcript.test.d.ts +1 -0
- package/dist/tui/transcript.test.js +71 -0
- package/dist/verification/detect.d.ts +29 -0
- package/dist/verification/detect.js +85 -0
- package/dist/verification/engine.d.ts +25 -0
- package/dist/verification/engine.js +57 -0
- package/package.json +58 -0
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project map — high-level description of a repository.
|
|
3
|
+
*
|
|
4
|
+
* Goal: when an agent opens a repo for the first time, it should learn
|
|
5
|
+
* "what kind of project is this" in one read. We detect:
|
|
6
|
+
* - language(s) from file extensions
|
|
7
|
+
* - framework from package.json / requirements.txt / go.mod
|
|
8
|
+
* - package manager from lockfiles + scripts
|
|
9
|
+
* - test framework from package.json scripts + devDependencies
|
|
10
|
+
* - build commands from package.json scripts + Makefile
|
|
11
|
+
* - source directories from convention + first-party markers
|
|
12
|
+
* - important config files from a curated allow-list
|
|
13
|
+
*
|
|
14
|
+
* Output: ProjectMap (machine-readable) + formatProjectMap() (model-readable).
|
|
15
|
+
*
|
|
16
|
+
* Design constraints:
|
|
17
|
+
* - Single root-dir read, no subprocess calls (fast + hermetic).
|
|
18
|
+
* - Bounded output: at most MAX_SOURCE_DIRS source dirs, MAX_CONFIG_FILES
|
|
19
|
+
* config files, top-3 languages, top-3 frameworks.
|
|
20
|
+
* - Reuses IGNORED_DIRS from repo-map.ts so the two scanners agree on
|
|
21
|
+
* what to walk past.
|
|
22
|
+
*/
|
|
23
|
+
import * as fs from 'node:fs/promises';
|
|
24
|
+
import * as path from 'node:path';
|
|
25
|
+
export const IGNORED_DIRS = new Set([
|
|
26
|
+
'node_modules', '.git', 'dist', 'build', '.next', '.turbo',
|
|
27
|
+
'coverage', '.klyro', 'out', 'target', 'vendor', '__pycache__',
|
|
28
|
+
'.cache', '.parcel-cache', '.vscode', '.idea',
|
|
29
|
+
]);
|
|
30
|
+
const MAX_SOURCE_DIRS = 8;
|
|
31
|
+
const MAX_CONFIG_FILES = 12;
|
|
32
|
+
const MAX_LANGUAGES = 3;
|
|
33
|
+
const MAX_FRAMEWORKS = 3;
|
|
34
|
+
const MAX_DEPS = 25;
|
|
35
|
+
const MAX_BYTES = 64 * 1024; // 64 KB per config file we read fully
|
|
36
|
+
const CONFIG_FILE_ALLOWLIST = new Set([
|
|
37
|
+
'package.json', 'tsconfig.json', 'tsconfig.test.json', 'jsconfig.json',
|
|
38
|
+
'vite.config.ts', 'vite.config.js', 'vitest.config.ts', 'vitest.config.js',
|
|
39
|
+
'next.config.js', 'next.config.ts', 'next.config.mjs',
|
|
40
|
+
'Cargo.toml', 'go.mod', 'requirements.txt', 'pyproject.toml', 'setup.py',
|
|
41
|
+
'Gemfile', 'pom.xml', 'build.gradle', 'build.gradle.kts',
|
|
42
|
+
'Makefile', 'CMakeLists.txt',
|
|
43
|
+
'.eslintrc.json', '.eslintrc.js', '.prettierrc.json', '.editorconfig',
|
|
44
|
+
'tailwind.config.js', 'tailwind.config.ts', 'postcss.config.js',
|
|
45
|
+
'docker-compose.yml', 'Dockerfile', '.env.example',
|
|
46
|
+
'README.md', 'LICENSE',
|
|
47
|
+
]);
|
|
48
|
+
/** Heuristic source dir candidates, ranked by how commonly they hold first-party code. */
|
|
49
|
+
const SOURCE_DIR_CANDIDATES = [
|
|
50
|
+
'src', 'lib', 'app', 'pkg', 'cmd', 'internal',
|
|
51
|
+
'src/main', 'src/lib', 'src/app', 'src/server', 'src/client',
|
|
52
|
+
'lib/src', 'app/src',
|
|
53
|
+
];
|
|
54
|
+
async function exists(p) {
|
|
55
|
+
try {
|
|
56
|
+
await fs.stat(p);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async function readJsonSafe(p) {
|
|
64
|
+
try {
|
|
65
|
+
const s = await fs.stat(p);
|
|
66
|
+
if (!s.isFile() || s.size > MAX_BYTES)
|
|
67
|
+
return null;
|
|
68
|
+
const raw = await fs.readFile(p, 'utf-8');
|
|
69
|
+
return JSON.parse(raw);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async function readTextSafe(p, max = MAX_BYTES) {
|
|
76
|
+
try {
|
|
77
|
+
const s = await fs.stat(p);
|
|
78
|
+
if (!s.isFile() || s.size > max)
|
|
79
|
+
return null;
|
|
80
|
+
return await fs.readFile(p, 'utf-8');
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Count file extensions in a directory (shallow, capped). */
|
|
87
|
+
async function countExtensions(root, caps = {}) {
|
|
88
|
+
const dirs = caps.dirs ?? 200;
|
|
89
|
+
const filesPerDir = caps.filesPerDir ?? 50;
|
|
90
|
+
const extCounts = new Map();
|
|
91
|
+
let dirsVisited = 0;
|
|
92
|
+
let stop = false;
|
|
93
|
+
async function walk(dir, depth) {
|
|
94
|
+
if (stop)
|
|
95
|
+
return;
|
|
96
|
+
if (dirsVisited >= dirs) {
|
|
97
|
+
stop = true;
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
dirsVisited++;
|
|
101
|
+
let entries;
|
|
102
|
+
try {
|
|
103
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
let files = 0;
|
|
109
|
+
for (const e of entries) {
|
|
110
|
+
if (IGNORED_DIRS.has(e.name))
|
|
111
|
+
continue;
|
|
112
|
+
const full = path.join(dir, e.name);
|
|
113
|
+
if (e.isDirectory()) {
|
|
114
|
+
if (depth < 6)
|
|
115
|
+
await walk(full, depth + 1);
|
|
116
|
+
if (stop)
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
else if (e.isFile()) {
|
|
120
|
+
files++;
|
|
121
|
+
if (files > filesPerDir)
|
|
122
|
+
break;
|
|
123
|
+
const ext = path.extname(e.name).toLowerCase();
|
|
124
|
+
if (!ext)
|
|
125
|
+
continue;
|
|
126
|
+
extCounts.set(ext, (extCounts.get(ext) ?? 0) + 1);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
await walk(root, 0);
|
|
131
|
+
return extCounts;
|
|
132
|
+
}
|
|
133
|
+
const EXT_TO_LANG = {
|
|
134
|
+
'.ts': 'TypeScript', '.tsx': 'TypeScript',
|
|
135
|
+
'.js': 'JavaScript', '.jsx': 'JavaScript', '.mjs': 'JavaScript', '.cjs': 'JavaScript',
|
|
136
|
+
'.py': 'Python',
|
|
137
|
+
'.go': 'Go',
|
|
138
|
+
'.rs': 'Rust',
|
|
139
|
+
'.java': 'Java', '.kt': 'Kotlin', '.scala': 'Scala',
|
|
140
|
+
'.rb': 'Ruby',
|
|
141
|
+
'.c': 'C', '.h': 'C', '.cpp': 'C++', '.hpp': 'C++',
|
|
142
|
+
'.cs': 'C#', '.fs': 'F#',
|
|
143
|
+
'.php': 'PHP',
|
|
144
|
+
'.swift': 'Swift', '.m': 'Objective-C',
|
|
145
|
+
'.sh': 'Shell', '.bash': 'Shell',
|
|
146
|
+
};
|
|
147
|
+
function extensionsToLanguages(extCounts) {
|
|
148
|
+
const langCounts = new Map();
|
|
149
|
+
for (const [ext, count] of extCounts) {
|
|
150
|
+
const lang = EXT_TO_LANG[ext];
|
|
151
|
+
if (!lang)
|
|
152
|
+
continue;
|
|
153
|
+
langCounts.set(lang, (langCounts.get(lang) ?? 0) + count);
|
|
154
|
+
}
|
|
155
|
+
return [...langCounts.entries()]
|
|
156
|
+
.sort((a, b) => b[1] - a[1])
|
|
157
|
+
.slice(0, MAX_LANGUAGES)
|
|
158
|
+
.map(([l]) => l);
|
|
159
|
+
}
|
|
160
|
+
const FRAMEWORK_SIGNATURES = [
|
|
161
|
+
{ name: 'Next.js', npmDeps: ['next'], files: ['next.config.js', 'next.config.ts', 'next.config.mjs'] },
|
|
162
|
+
{ name: 'Nuxt', npmDeps: ['nuxt'] },
|
|
163
|
+
{ name: 'Remix', npmDeps: ['@remix-run/react', '@remix-run/node'] },
|
|
164
|
+
{ name: 'SvelteKit', npmDeps: ['@sveltejs/kit'] },
|
|
165
|
+
{ name: 'Astro', npmDeps: ['astro'] },
|
|
166
|
+
{ name: 'Express', npmDeps: ['express'] },
|
|
167
|
+
{ name: 'Fastify', npmDeps: ['fastify'] },
|
|
168
|
+
{ name: 'NestJS', npmDeps: ['@nestjs/core'] },
|
|
169
|
+
{ name: 'Koa', npmDeps: ['koa'] },
|
|
170
|
+
{ name: 'Hapi', npmDeps: ['@hapi/hapi'] },
|
|
171
|
+
{ name: 'React', npmDeps: ['react'] },
|
|
172
|
+
{ name: 'Vue', npmDeps: ['vue'] },
|
|
173
|
+
{ name: 'Angular', npmDeps: ['@angular/core'] },
|
|
174
|
+
{ name: 'Svelte', npmDeps: ['svelte'] },
|
|
175
|
+
{ name: 'Solid', npmDeps: ['solid-js'] },
|
|
176
|
+
{ name: 'Vite', npmDeps: ['vite'], files: ['vite.config.ts', 'vite.config.js'] },
|
|
177
|
+
{ name: 'Vitest', npmDeps: ['vitest'], files: ['vitest.config.ts', 'vitest.config.js'] },
|
|
178
|
+
{ name: 'Jest', npmDeps: ['jest'] },
|
|
179
|
+
{ name: 'Mocha', npmDeps: ['mocha'] },
|
|
180
|
+
{ name: 'Pytest', pythonReq: ['pytest', 'pytest-asyncio', 'pytest-cov'] },
|
|
181
|
+
{ name: 'Django', pythonReq: ['django'] },
|
|
182
|
+
{ name: 'Flask', pythonReq: ['flask'] },
|
|
183
|
+
{ name: 'FastAPI', pythonReq: ['fastapi'] },
|
|
184
|
+
{ name: 'Tornado', pythonReq: ['tornado'] },
|
|
185
|
+
{ name: 'Rails', files: ['Gemfile'] },
|
|
186
|
+
{ name: 'Spring', files: ['pom.xml', 'build.gradle', 'build.gradle.kts'] },
|
|
187
|
+
{ name: 'Cargo', files: ['Cargo.toml'] },
|
|
188
|
+
{ name: 'CMake', files: ['CMakeLists.txt'] },
|
|
189
|
+
];
|
|
190
|
+
function detectFrameworks(pkg, filesInRoot, reqLines) {
|
|
191
|
+
const npmDeps = new Set();
|
|
192
|
+
if (pkg) {
|
|
193
|
+
for (const k of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
|
|
194
|
+
const d = pkg[k];
|
|
195
|
+
if (d && typeof d === 'object') {
|
|
196
|
+
for (const name of Object.keys(d))
|
|
197
|
+
npmDeps.add(name);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const detected = [];
|
|
202
|
+
for (const sig of FRAMEWORK_SIGNATURES) {
|
|
203
|
+
let rank = 0;
|
|
204
|
+
if (sig.npmDeps)
|
|
205
|
+
for (const d of sig.npmDeps)
|
|
206
|
+
if (npmDeps.has(d)) {
|
|
207
|
+
rank += 2;
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
if (sig.files)
|
|
211
|
+
for (const f of sig.files)
|
|
212
|
+
if (filesInRoot.has(f)) {
|
|
213
|
+
rank += 3;
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
if (sig.pythonReq)
|
|
217
|
+
for (const r of sig.pythonReq)
|
|
218
|
+
if (reqLines.some((l) => l.toLowerCase().startsWith(r.toLowerCase()))) {
|
|
219
|
+
rank += 2;
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
if (rank > 0)
|
|
223
|
+
detected.push({ name: sig.name, rank });
|
|
224
|
+
}
|
|
225
|
+
return detected
|
|
226
|
+
.sort((a, b) => b.rank - a.rank)
|
|
227
|
+
.slice(0, MAX_FRAMEWORKS)
|
|
228
|
+
.map((d) => d.name);
|
|
229
|
+
}
|
|
230
|
+
function detectPackageManager(root, pkg) {
|
|
231
|
+
// Lockfile order roughly maps to the most common PM per ecosystem.
|
|
232
|
+
const checks = [
|
|
233
|
+
['package-lock.json', 'npm'],
|
|
234
|
+
['yarn.lock', 'yarn'],
|
|
235
|
+
['pnpm-lock.yaml', 'pnpm'],
|
|
236
|
+
['bun.lockb', 'bun'],
|
|
237
|
+
['Cargo.lock', 'cargo'],
|
|
238
|
+
['poetry.lock', 'poetry'],
|
|
239
|
+
['Pipfile.lock', 'pipenv'],
|
|
240
|
+
['uv.lock', 'uv'],
|
|
241
|
+
['go.sum', 'go modules'],
|
|
242
|
+
];
|
|
243
|
+
// Heuristic: lockfile presence is the strongest signal.
|
|
244
|
+
// We do a sync existsFile check for each — they live in the root, so cheap.
|
|
245
|
+
for (const [lock, pm] of checks) {
|
|
246
|
+
// Avoid the async cost by re-using the root listing we already do.
|
|
247
|
+
// (Caller threads the listing into detectFrameworks; here we re-stat directly.)
|
|
248
|
+
try {
|
|
249
|
+
require('node:fs').statSync(path.join(root, lock));
|
|
250
|
+
return pm;
|
|
251
|
+
}
|
|
252
|
+
catch { /* not present */ }
|
|
253
|
+
}
|
|
254
|
+
// Fall back: if package.json has a "packageManager" field, use it.
|
|
255
|
+
if (pkg && typeof pkg.packageManager === 'string') {
|
|
256
|
+
const m = /^([a-z]+)/i.exec(pkg.packageManager);
|
|
257
|
+
if (m && m[1])
|
|
258
|
+
return m[1].toLowerCase();
|
|
259
|
+
}
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
function detectTestFramework(pkg) {
|
|
263
|
+
if (!pkg)
|
|
264
|
+
return undefined;
|
|
265
|
+
const dev = (pkg.devDependencies ?? {});
|
|
266
|
+
if (dev.vitest || dev['@vitest/runner'])
|
|
267
|
+
return 'Vitest';
|
|
268
|
+
if (dev.jest)
|
|
269
|
+
return 'Jest';
|
|
270
|
+
if (dev.mocha)
|
|
271
|
+
return 'Mocha';
|
|
272
|
+
if (dev['@playwright/test'])
|
|
273
|
+
return 'Playwright';
|
|
274
|
+
if (dev.cypress)
|
|
275
|
+
return 'Cypress';
|
|
276
|
+
if (dev.tape)
|
|
277
|
+
return 'Tape';
|
|
278
|
+
const deps = (pkg.dependencies ?? {});
|
|
279
|
+
if (deps.vitest)
|
|
280
|
+
return 'Vitest';
|
|
281
|
+
if (deps.jest)
|
|
282
|
+
return 'Jest';
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
function extractBuildCommands(pkg, root) {
|
|
286
|
+
const out = [];
|
|
287
|
+
if (pkg && typeof pkg.scripts === 'object' && pkg.scripts) {
|
|
288
|
+
const scripts = pkg.scripts;
|
|
289
|
+
for (const key of ['build', 'compile', 'prepare', 'start', 'dev', 'test', 'lint', 'typecheck']) {
|
|
290
|
+
if (typeof scripts[key] === 'string') {
|
|
291
|
+
out.push(`npm run ${key} # ${scripts[key]}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
// Makefile: pick the first `target:` block as a hint.
|
|
296
|
+
// (Don't try to parse make fully — just surface the targets as a single hint.)
|
|
297
|
+
try {
|
|
298
|
+
const stat = require('node:fs').statSync(path.join(root, 'Makefile'));
|
|
299
|
+
if (stat.isFile() && stat.size < MAX_BYTES) {
|
|
300
|
+
const raw = require('node:fs').readFileSync(path.join(root, 'Makefile'), 'utf-8');
|
|
301
|
+
const targets = [...raw.matchAll(/^([a-zA-Z_][\w-]*)\s*:/gm)].map((m) => m[1]).slice(0, 6);
|
|
302
|
+
if (targets.length)
|
|
303
|
+
out.push(`make <target> # targets: ${targets.join(', ')}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
catch { /* no Makefile */ }
|
|
307
|
+
return out.slice(0, 8);
|
|
308
|
+
}
|
|
309
|
+
async function detectSourceDirs(root) {
|
|
310
|
+
const found = [];
|
|
311
|
+
for (const candidate of SOURCE_DIR_CANDIDATES) {
|
|
312
|
+
const full = path.join(root, candidate);
|
|
313
|
+
if (await exists(full)) {
|
|
314
|
+
try {
|
|
315
|
+
const s = await fs.stat(full);
|
|
316
|
+
if (s.isDirectory())
|
|
317
|
+
found.push(candidate);
|
|
318
|
+
}
|
|
319
|
+
catch { /* race */ }
|
|
320
|
+
}
|
|
321
|
+
if (found.length >= MAX_SOURCE_DIRS)
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
return found;
|
|
325
|
+
}
|
|
326
|
+
async function listRootEntries(root) {
|
|
327
|
+
try {
|
|
328
|
+
const entries = await fs.readdir(root, { withFileTypes: true });
|
|
329
|
+
const files = new Set();
|
|
330
|
+
const dirs = [];
|
|
331
|
+
for (const e of entries) {
|
|
332
|
+
if (e.isFile())
|
|
333
|
+
files.add(e.name);
|
|
334
|
+
else if (e.isDirectory())
|
|
335
|
+
dirs.push(e.name);
|
|
336
|
+
}
|
|
337
|
+
return { files, dirs };
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
return { files: new Set(), dirs: [] };
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
function extractDependencies(pkg) {
|
|
344
|
+
if (!pkg)
|
|
345
|
+
return [];
|
|
346
|
+
const out = [];
|
|
347
|
+
const deps = (pkg.dependencies ?? {});
|
|
348
|
+
for (const [name, version] of Object.entries(deps)) {
|
|
349
|
+
if (typeof version === 'string')
|
|
350
|
+
out.push({ name, version });
|
|
351
|
+
else
|
|
352
|
+
out.push({ name });
|
|
353
|
+
if (out.length >= MAX_DEPS)
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
return out;
|
|
357
|
+
}
|
|
358
|
+
/** Build a project map for the repo rooted at `root`. */
|
|
359
|
+
export async function buildProjectMap(root) {
|
|
360
|
+
const { files: rootFiles, dirs: rootDirs } = await listRootEntries(root);
|
|
361
|
+
const pkg = await readJsonSafe(path.join(root, 'package.json'));
|
|
362
|
+
const hasPackageJson = pkg !== null;
|
|
363
|
+
const reqText = await readTextSafe(path.join(root, 'requirements.txt'));
|
|
364
|
+
const reqLines = reqText ? reqText.split(/\r?\n/).map((l) => l.trim()).filter((l) => l && !l.startsWith('#')) : [];
|
|
365
|
+
// Walk the tree shallowly for extensions.
|
|
366
|
+
const extCounts = await countExtensions(root);
|
|
367
|
+
// Source dirs by convention.
|
|
368
|
+
const sourceDirs = await detectSourceDirs(root);
|
|
369
|
+
// Important config files (allowlist only, root + src).
|
|
370
|
+
const configFiles = [];
|
|
371
|
+
for (const name of CONFIG_FILE_ALLOWLIST) {
|
|
372
|
+
if (rootFiles.has(name))
|
|
373
|
+
configFiles.push(name);
|
|
374
|
+
if (configFiles.length >= MAX_CONFIG_FILES)
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
if (rootDirs.includes('src')) {
|
|
378
|
+
for (const name of ['tsconfig.json', 'package.json', 'README.md']) {
|
|
379
|
+
if (await exists(path.join(root, 'src', name)) && !configFiles.includes(`src/${name}`)) {
|
|
380
|
+
configFiles.push(`src/${name}`);
|
|
381
|
+
if (configFiles.length >= MAX_CONFIG_FILES)
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
const framework = detectFrameworks(pkg, rootFiles, reqLines);
|
|
387
|
+
const packageManager = detectPackageManager(root, pkg);
|
|
388
|
+
const testFramework = detectTestFramework(pkg);
|
|
389
|
+
const buildCommands = extractBuildCommands(pkg, root);
|
|
390
|
+
const dependencies = extractDependencies(pkg);
|
|
391
|
+
const language = extensionsToLanguages(extCounts);
|
|
392
|
+
const hasGit = await exists(path.join(root, '.git'));
|
|
393
|
+
return {
|
|
394
|
+
root,
|
|
395
|
+
language,
|
|
396
|
+
framework,
|
|
397
|
+
packageManager,
|
|
398
|
+
testFramework,
|
|
399
|
+
buildCommands,
|
|
400
|
+
sourceDirs,
|
|
401
|
+
configFiles,
|
|
402
|
+
dependencies,
|
|
403
|
+
hasPackageJson,
|
|
404
|
+
hasGit,
|
|
405
|
+
generatedAt: new Date().toISOString(),
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
/** Render a ProjectMap as a compact, model-friendly block. */
|
|
409
|
+
export function formatProjectMap(m) {
|
|
410
|
+
const lines = [];
|
|
411
|
+
lines.push('# Project map');
|
|
412
|
+
if (m.language.length)
|
|
413
|
+
lines.push(`- Language: ${m.language.join(', ')}`);
|
|
414
|
+
if (m.framework.length)
|
|
415
|
+
lines.push(`- Framework: ${m.framework.join(', ')}`);
|
|
416
|
+
if (m.packageManager)
|
|
417
|
+
lines.push(`- Package manager: ${m.packageManager}`);
|
|
418
|
+
if (m.testFramework)
|
|
419
|
+
lines.push(`- Test framework: ${m.testFramework}`);
|
|
420
|
+
if (m.sourceDirs.length)
|
|
421
|
+
lines.push(`- Source dirs: ${m.sourceDirs.join(', ')}`);
|
|
422
|
+
if (m.configFiles.length)
|
|
423
|
+
lines.push(`- Important config: ${m.configFiles.join(', ')}`);
|
|
424
|
+
if (m.buildCommands.length) {
|
|
425
|
+
lines.push('- Build / scripts:');
|
|
426
|
+
for (const c of m.buildCommands)
|
|
427
|
+
lines.push(` - ${c}`);
|
|
428
|
+
}
|
|
429
|
+
if (m.dependencies.length) {
|
|
430
|
+
lines.push(`- Direct dependencies (top ${m.dependencies.length}):`);
|
|
431
|
+
for (const d of m.dependencies) {
|
|
432
|
+
lines.push(` - ${d.name}${d.version ? `@${d.version}` : ''}`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if (!m.hasGit)
|
|
436
|
+
lines.push('- Note: not a git working tree');
|
|
437
|
+
return lines.join('\n');
|
|
438
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight repo map — extract top-level symbols from a small set of
|
|
3
|
+
* source-file extensions. Regex-based so it works on any platform without
|
|
4
|
+
* a native tree-sitter binary. Quality is intentionally low — the goal is
|
|
5
|
+
* "give the model a useful index", not a full AST.
|
|
6
|
+
*
|
|
7
|
+
* Supported extensions: .ts, .tsx, .js, .jsx, .mjs, .cjs, .py, .go, .rs
|
|
8
|
+
*
|
|
9
|
+
* Output: per-file symbol list with kind and approximate line number.
|
|
10
|
+
*/
|
|
11
|
+
export type SymbolKind = 'class' | 'function' | 'method' | 'type' | 'const' | 'var' | 'module';
|
|
12
|
+
export interface Symbol {
|
|
13
|
+
kind: SymbolKind;
|
|
14
|
+
name: string;
|
|
15
|
+
line: number;
|
|
16
|
+
}
|
|
17
|
+
export interface RepoFile {
|
|
18
|
+
path: string;
|
|
19
|
+
symbols: Symbol[];
|
|
20
|
+
}
|
|
21
|
+
/** Heuristic: walk cwd, return at most `maxFiles` files with symbols. */
|
|
22
|
+
export declare function buildRepoMap(opts: {
|
|
23
|
+
cwd: string;
|
|
24
|
+
maxFiles?: number;
|
|
25
|
+
maxFileBytes?: number;
|
|
26
|
+
}): Promise<RepoFile[]>;
|
|
27
|
+
/** Extract symbols with cheap regex per language. */
|
|
28
|
+
export declare function extractSymbols(content: string, ext: string): Symbol[];
|
|
29
|
+
/** Format a RepoFile[] as a compact outline for the model. */
|
|
30
|
+
export declare function formatRepoMap(files: RepoFile[]): string;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight repo map — extract top-level symbols from a small set of
|
|
3
|
+
* source-file extensions. Regex-based so it works on any platform without
|
|
4
|
+
* a native tree-sitter binary. Quality is intentionally low — the goal is
|
|
5
|
+
* "give the model a useful index", not a full AST.
|
|
6
|
+
*
|
|
7
|
+
* Supported extensions: .ts, .tsx, .js, .jsx, .mjs, .cjs, .py, .go, .rs
|
|
8
|
+
*
|
|
9
|
+
* Output: per-file symbol list with kind and approximate line number.
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from 'node:fs/promises';
|
|
12
|
+
import * as path from 'node:path';
|
|
13
|
+
const SUPPORTED_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py', '.go', '.rs']);
|
|
14
|
+
const IGNORED_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.klyro', 'coverage', 'out']);
|
|
15
|
+
/** Heuristic: walk cwd, return at most `maxFiles` files with symbols. */
|
|
16
|
+
export async function buildRepoMap(opts) {
|
|
17
|
+
const maxFiles = opts.maxFiles ?? 500;
|
|
18
|
+
const maxFileBytes = opts.maxFileBytes ?? 256 * 1024;
|
|
19
|
+
const out = [];
|
|
20
|
+
await walk(opts.cwd, async (file) => {
|
|
21
|
+
if (out.length >= maxFiles)
|
|
22
|
+
return false;
|
|
23
|
+
const ext = path.extname(file).toLowerCase();
|
|
24
|
+
if (!SUPPORTED_EXTS.has(ext))
|
|
25
|
+
return true;
|
|
26
|
+
let stat;
|
|
27
|
+
try {
|
|
28
|
+
stat = await fs.stat(file);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
if (stat.size > maxFileBytes)
|
|
34
|
+
return true;
|
|
35
|
+
let content;
|
|
36
|
+
try {
|
|
37
|
+
content = await fs.readFile(file, 'utf-8');
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
out.push({ path: path.relative(opts.cwd, file), symbols: extractSymbols(content, ext) });
|
|
43
|
+
return true;
|
|
44
|
+
});
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
async function walk(dir, visit) {
|
|
48
|
+
let entries;
|
|
49
|
+
try {
|
|
50
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
for (const e of entries) {
|
|
56
|
+
if (IGNORED_DIRS.has(e.name))
|
|
57
|
+
continue;
|
|
58
|
+
const full = path.join(dir, e.name);
|
|
59
|
+
if (e.isDirectory()) {
|
|
60
|
+
await walk(full, visit);
|
|
61
|
+
}
|
|
62
|
+
else if (e.isFile()) {
|
|
63
|
+
const keepGoing = await visit(full);
|
|
64
|
+
if (!keepGoing)
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** Extract symbols with cheap regex per language. */
|
|
70
|
+
export function extractSymbols(content, ext) {
|
|
71
|
+
const lines = content.split(/\r?\n/);
|
|
72
|
+
const syms = [];
|
|
73
|
+
switch (ext) {
|
|
74
|
+
case '.ts':
|
|
75
|
+
case '.tsx':
|
|
76
|
+
case '.js':
|
|
77
|
+
case '.jsx':
|
|
78
|
+
case '.mjs':
|
|
79
|
+
case '.cjs':
|
|
80
|
+
extractJsLike(lines, syms);
|
|
81
|
+
break;
|
|
82
|
+
case '.py':
|
|
83
|
+
extractPython(lines, syms);
|
|
84
|
+
break;
|
|
85
|
+
case '.go':
|
|
86
|
+
extractGo(lines, syms);
|
|
87
|
+
break;
|
|
88
|
+
case '.rs':
|
|
89
|
+
extractRust(lines, syms);
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
return syms;
|
|
93
|
+
}
|
|
94
|
+
function lineOf(lines, idx) { return idx + 1; }
|
|
95
|
+
function extractJsLike(lines, out) {
|
|
96
|
+
for (let i = 0; i < lines.length; i++) {
|
|
97
|
+
const l = lines[i] ?? '';
|
|
98
|
+
let m;
|
|
99
|
+
if ((m = /^\s*(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)/.exec(l)) && m[1]) {
|
|
100
|
+
out.push({ kind: 'function', name: m[1], line: lineOf(lines, i) });
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if ((m = /^\s*(?:export\s+)?class\s+([A-Za-z_$][\w$]*)/.exec(l)) && m[1]) {
|
|
104
|
+
out.push({ kind: 'class', name: m[1], line: lineOf(lines, i) });
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if ((m = /^\s*(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*[:=]/.exec(l)) && m[1]) {
|
|
108
|
+
out.push({ kind: 'const', name: m[1], line: lineOf(lines, i) });
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if ((m = /^\s*(?:export\s+)?type\s+([A-Za-z_$][\w$]*)/.exec(l)) && m[1]) {
|
|
112
|
+
out.push({ kind: 'type', name: m[1], line: lineOf(lines, i) });
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function extractPython(lines, out) {
|
|
118
|
+
for (let i = 0; i < lines.length; i++) {
|
|
119
|
+
const l = lines[i] ?? '';
|
|
120
|
+
let m;
|
|
121
|
+
if ((m = /^\s*(?:async\s+)?def\s+([A-Za-z_][\w]*)/.exec(l)) && m[1]) {
|
|
122
|
+
out.push({ kind: 'function', name: m[1], line: lineOf(lines, i) });
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if ((m = /^\s*class\s+([A-Za-z_][\w]*)/.exec(l)) && m[1]) {
|
|
126
|
+
out.push({ kind: 'class', name: m[1], line: lineOf(lines, i) });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function extractGo(lines, out) {
|
|
131
|
+
for (let i = 0; i < lines.length; i++) {
|
|
132
|
+
const l = lines[i] ?? '';
|
|
133
|
+
let m;
|
|
134
|
+
if ((m = /^\s*func\s+(?:\([^)]+\)\s+)?([A-Za-z_][\w]*)/.exec(l)) && m[1]) {
|
|
135
|
+
out.push({ kind: 'function', name: m[1], line: lineOf(lines, i) });
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if ((m = /^\s*type\s+([A-Za-z_][\w]*)\s+struct/.exec(l)) && m[1]) {
|
|
139
|
+
out.push({ kind: 'type', name: m[1], line: lineOf(lines, i) });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function extractRust(lines, out) {
|
|
144
|
+
for (let i = 0; i < lines.length; i++) {
|
|
145
|
+
const l = lines[i] ?? '';
|
|
146
|
+
let m;
|
|
147
|
+
if ((m = /^\s*(?:pub\s+)?fn\s+([A-Za-z_][\w]*)/.exec(l)) && m[1]) {
|
|
148
|
+
out.push({ kind: 'function', name: m[1], line: lineOf(lines, i) });
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if ((m = /^\s*(?:pub\s+)?struct\s+([A-Za-z_][\w]*)/.exec(l)) && m[1]) {
|
|
152
|
+
out.push({ kind: 'type', name: m[1], line: lineOf(lines, i) });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/** Format a RepoFile[] as a compact outline for the model. */
|
|
157
|
+
export function formatRepoMap(files) {
|
|
158
|
+
const lines = [];
|
|
159
|
+
for (const f of files) {
|
|
160
|
+
if (f.symbols.length === 0)
|
|
161
|
+
continue;
|
|
162
|
+
lines.push(`# ${f.path}`);
|
|
163
|
+
for (const s of f.symbols) {
|
|
164
|
+
lines.push(` ${s.kind} ${s.name} L${s.line}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return lines.join('\n');
|
|
168
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart file selector — given the current transcript, return a list of
|
|
3
|
+
* file paths the model is most likely to need next. Used to populate the
|
|
4
|
+
* repo-map slot of the system prompt.
|
|
5
|
+
*
|
|
6
|
+
* Scoring (very simple):
|
|
7
|
+
* - exact path match in the transcript → +100
|
|
8
|
+
* - basename match (e.g. "user.ts") → +20
|
|
9
|
+
* - symbol name match (functions/types) → +10
|
|
10
|
+
* - file in repo map → +1
|
|
11
|
+
*
|
|
12
|
+
* Top N by score.
|
|
13
|
+
*/
|
|
14
|
+
import type { RepoFile } from './repo-map.js';
|
|
15
|
+
export interface SelectOptions {
|
|
16
|
+
/** Cap on returned files. */
|
|
17
|
+
maxFiles?: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function selectFiles(transcript: {
|
|
20
|
+
role: string;
|
|
21
|
+
content: string;
|
|
22
|
+
}[], repo: RepoFile[], opts?: SelectOptions): RepoFile[];
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart file selector — given the current transcript, return a list of
|
|
3
|
+
* file paths the model is most likely to need next. Used to populate the
|
|
4
|
+
* repo-map slot of the system prompt.
|
|
5
|
+
*
|
|
6
|
+
* Scoring (very simple):
|
|
7
|
+
* - exact path match in the transcript → +100
|
|
8
|
+
* - basename match (e.g. "user.ts") → +20
|
|
9
|
+
* - symbol name match (functions/types) → +10
|
|
10
|
+
* - file in repo map → +1
|
|
11
|
+
*
|
|
12
|
+
* Top N by score.
|
|
13
|
+
*/
|
|
14
|
+
export function selectFiles(transcript, repo, opts = {}) {
|
|
15
|
+
const maxFiles = opts.maxFiles ?? 20;
|
|
16
|
+
const corpus = transcript.map((m) => m.content).join('\n').toLowerCase();
|
|
17
|
+
const scored = repo
|
|
18
|
+
.map((f) => {
|
|
19
|
+
let score = 1;
|
|
20
|
+
const pathLower = f.path.toLowerCase();
|
|
21
|
+
const base = pathLower.split('/').pop() ?? pathLower;
|
|
22
|
+
if (corpus.includes(pathLower))
|
|
23
|
+
score += 100;
|
|
24
|
+
if (corpus.includes(base))
|
|
25
|
+
score += 20;
|
|
26
|
+
for (const s of f.symbols) {
|
|
27
|
+
if (s.name.length > 3 && corpus.includes(s.name.toLowerCase()))
|
|
28
|
+
score += 10;
|
|
29
|
+
}
|
|
30
|
+
return { file: f, score };
|
|
31
|
+
})
|
|
32
|
+
.filter((x) => x.score > 1)
|
|
33
|
+
.sort((a, b) => b.score - a.score)
|
|
34
|
+
.slice(0, maxFiles)
|
|
35
|
+
.map((x) => x.file);
|
|
36
|
+
return scored;
|
|
37
|
+
}
|