@mrace07/kairo 0.1.0
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/README.md +141 -0
- package/dist/application/coding-agent.d.ts +85 -0
- package/dist/application/coding-agent.js +765 -0
- package/dist/application/context-manager.d.ts +22 -0
- package/dist/application/context-manager.js +174 -0
- package/dist/application/context-selector.d.ts +11 -0
- package/dist/application/context-selector.js +74 -0
- package/dist/application/evaluated-agent.d.ts +7 -0
- package/dist/application/evaluated-agent.js +16 -0
- package/dist/application/evaluation-comparison.d.ts +34 -0
- package/dist/application/evaluation-comparison.js +91 -0
- package/dist/application/evaluation-harness.d.ts +19 -0
- package/dist/application/evaluation-harness.js +217 -0
- package/dist/application/failure-analyzer.d.ts +5 -0
- package/dist/application/failure-analyzer.js +37 -0
- package/dist/application/interaction-routing.d.ts +9 -0
- package/dist/application/interaction-routing.js +20 -0
- package/dist/application/live-evaluation.d.ts +8 -0
- package/dist/application/live-evaluation.js +185 -0
- package/dist/application/model-routing.d.ts +12 -0
- package/dist/application/model-routing.js +40 -0
- package/dist/application/model-system-instruction.d.ts +4 -0
- package/dist/application/model-system-instruction.js +4 -0
- package/dist/application/self-evaluation.d.ts +26 -0
- package/dist/application/self-evaluation.js +394 -0
- package/dist/application/task-metrics.d.ts +31 -0
- package/dist/application/task-metrics.js +42 -0
- package/dist/application/verification-planner.d.ts +12 -0
- package/dist/application/verification-planner.js +97 -0
- package/dist/domain/models.d.ts +247 -0
- package/dist/domain/models.js +1 -0
- package/dist/domain/ports.d.ts +87 -0
- package/dist/domain/ports.js +1 -0
- package/dist/domain/provider-error.d.ts +18 -0
- package/dist/domain/provider-error.js +17 -0
- package/dist/infrastructure/configuration/config.d.ts +24 -0
- package/dist/infrastructure/configuration/config.js +79 -0
- package/dist/infrastructure/filesystem/platform-paths.d.ts +8 -0
- package/dist/infrastructure/filesystem/platform-paths.js +18 -0
- package/dist/infrastructure/persistence/sqlite-session-store.d.ts +82 -0
- package/dist/infrastructure/persistence/sqlite-session-store.js +447 -0
- package/dist/infrastructure/providers/gemini-provider.d.ts +14 -0
- package/dist/infrastructure/providers/gemini-provider.js +90 -0
- package/dist/infrastructure/providers/groq-provider.d.ts +16 -0
- package/dist/infrastructure/providers/groq-provider.js +101 -0
- package/dist/infrastructure/providers/jev-safety-advisor.d.ts +18 -0
- package/dist/infrastructure/providers/jev-safety-advisor.js +95 -0
- package/dist/infrastructure/providers/mistral-provider.d.ts +15 -0
- package/dist/infrastructure/providers/mistral-provider.js +137 -0
- package/dist/infrastructure/providers/openrouter-provider.d.ts +15 -0
- package/dist/infrastructure/providers/openrouter-provider.js +104 -0
- package/dist/infrastructure/providers/provider-recovery.d.ts +10 -0
- package/dist/infrastructure/providers/provider-recovery.js +108 -0
- package/dist/infrastructure/providers/provider-registry.d.ts +22 -0
- package/dist/infrastructure/providers/provider-registry.js +67 -0
- package/dist/infrastructure/repository/repository-awareness.d.ts +12 -0
- package/dist/infrastructure/repository/repository-awareness.js +25 -0
- package/dist/infrastructure/repository/repository-profiler.d.ts +35 -0
- package/dist/infrastructure/repository/repository-profiler.js +498 -0
- package/dist/infrastructure/security/macos-keychain-store.d.ts +17 -0
- package/dist/infrastructure/security/macos-keychain-store.js +73 -0
- package/dist/infrastructure/tools/workspace-tools.d.ts +30 -0
- package/dist/infrastructure/tools/workspace-tools.js +321 -0
- package/dist/interface/cli/evaluation-comparison-report.d.ts +6 -0
- package/dist/interface/cli/evaluation-comparison-report.js +46 -0
- package/dist/interface/cli/evaluation-report.d.ts +14 -0
- package/dist/interface/cli/evaluation-report.js +122 -0
- package/dist/interface/cli/index.d.ts +2 -0
- package/dist/interface/cli/index.js +238 -0
- package/dist/interface/cli/provider-setup.d.ts +16 -0
- package/dist/interface/cli/provider-setup.js +86 -0
- package/dist/interface/cli/repl.d.ts +7 -0
- package/dist/interface/cli/repl.js +19 -0
- package/dist/interface/cli/task-trace.d.ts +7 -0
- package/dist/interface/cli/task-trace.js +48 -0
- package/dist/interface/cli/tui.d.ts +147 -0
- package/dist/interface/cli/tui.js +910 -0
- package/package.json +61 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { RepositoryFingerprint, RepositorySnapshot } from "../../domain/models.js";
|
|
2
|
+
export declare class RepositoryProfiler {
|
|
3
|
+
private readonly limits;
|
|
4
|
+
constructor(limits?: {
|
|
5
|
+
maxInventoryEntries?: number;
|
|
6
|
+
maxControlFileBytes?: number;
|
|
7
|
+
});
|
|
8
|
+
/** Builds a bounded, language-neutral snapshot with optional JS/TS enrichment. */
|
|
9
|
+
profile(root: string): Promise<RepositorySnapshot>;
|
|
10
|
+
/** Computes the same deterministic identity used by a full snapshot. */
|
|
11
|
+
fingerprint(root: string): Promise<RepositoryFingerprint>;
|
|
12
|
+
private inventory;
|
|
13
|
+
private gitMetadata;
|
|
14
|
+
private gitPaths;
|
|
15
|
+
private walkPaths;
|
|
16
|
+
private changedPaths;
|
|
17
|
+
private inventoryFingerprint;
|
|
18
|
+
private classify;
|
|
19
|
+
private ecosystems;
|
|
20
|
+
private pathsOf;
|
|
21
|
+
private isControlFile;
|
|
22
|
+
private hashFile;
|
|
23
|
+
private readPackage;
|
|
24
|
+
private readGitignore;
|
|
25
|
+
private scripts;
|
|
26
|
+
private indexFiles;
|
|
27
|
+
private readText;
|
|
28
|
+
private terms;
|
|
29
|
+
private symbols;
|
|
30
|
+
private connectFiles;
|
|
31
|
+
private imports;
|
|
32
|
+
private resolveImport;
|
|
33
|
+
private testTarget;
|
|
34
|
+
private isUseful;
|
|
35
|
+
}
|
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { lstat, readdir, readFile } from "node:fs/promises";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { basename, extname, join, posix, relative, resolve, sep } from "node:path";
|
|
6
|
+
import { VerificationPlanner } from "../../application/verification-planner.js";
|
|
7
|
+
const execute = promisify(execFile);
|
|
8
|
+
const gitExecutable = process.platform === "darwin" ? "/usr/bin/git" : "git";
|
|
9
|
+
const DEFAULT_IGNORES = new Set([
|
|
10
|
+
".git",
|
|
11
|
+
".kairo",
|
|
12
|
+
".cache",
|
|
13
|
+
".next",
|
|
14
|
+
".venv",
|
|
15
|
+
"node_modules",
|
|
16
|
+
"vendor",
|
|
17
|
+
"dist",
|
|
18
|
+
"build",
|
|
19
|
+
"coverage",
|
|
20
|
+
"target",
|
|
21
|
+
"out",
|
|
22
|
+
"venv",
|
|
23
|
+
]);
|
|
24
|
+
const SOURCE_DIRS = new Set(["src", "app", "lib", "pkg", "cmd", "internal", "crates", "packages"]);
|
|
25
|
+
const TEST_DIRS = new Set(["test", "tests", "__tests__", "spec", "specs"]);
|
|
26
|
+
const MAX_INVENTORY_ENTRIES = 20_000;
|
|
27
|
+
const MAX_INDEXED_FILES = 800;
|
|
28
|
+
const MAX_CONTROL_FILE_BYTES = 256 * 1024;
|
|
29
|
+
const MAX_INDEXED_FILE_BYTES = 200_000;
|
|
30
|
+
const MAX_TERMS_PER_FILE = 400;
|
|
31
|
+
const MAX_SYMBOLS_PER_FILE = 80;
|
|
32
|
+
const SOURCE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
|
|
33
|
+
const SOURCE_FILE_EXTENSIONS = new Set([
|
|
34
|
+
".c",
|
|
35
|
+
".cc",
|
|
36
|
+
".cpp",
|
|
37
|
+
".cs",
|
|
38
|
+
".ex",
|
|
39
|
+
".exs",
|
|
40
|
+
".go",
|
|
41
|
+
".h",
|
|
42
|
+
".hpp",
|
|
43
|
+
".java",
|
|
44
|
+
".js",
|
|
45
|
+
".jsx",
|
|
46
|
+
".kt",
|
|
47
|
+
".kts",
|
|
48
|
+
".php",
|
|
49
|
+
".py",
|
|
50
|
+
".rb",
|
|
51
|
+
".rs",
|
|
52
|
+
".swift",
|
|
53
|
+
".ts",
|
|
54
|
+
".tsx",
|
|
55
|
+
]);
|
|
56
|
+
const MANIFEST_NAMES = new Set([
|
|
57
|
+
"package.json",
|
|
58
|
+
"pyproject.toml",
|
|
59
|
+
"requirements.txt",
|
|
60
|
+
"setup.py",
|
|
61
|
+
"go.mod",
|
|
62
|
+
"cargo.toml",
|
|
63
|
+
"pom.xml",
|
|
64
|
+
"build.gradle",
|
|
65
|
+
"build.gradle.kts",
|
|
66
|
+
"gemfile",
|
|
67
|
+
"composer.json",
|
|
68
|
+
"mix.exs",
|
|
69
|
+
"workspace.json",
|
|
70
|
+
"nx.json",
|
|
71
|
+
"turbo.json",
|
|
72
|
+
"lerna.json",
|
|
73
|
+
]);
|
|
74
|
+
const LOCKFILE_NAMES = new Set([
|
|
75
|
+
"package-lock.json",
|
|
76
|
+
"pnpm-lock.yaml",
|
|
77
|
+
"yarn.lock",
|
|
78
|
+
"bun.lock",
|
|
79
|
+
"bun.lockb",
|
|
80
|
+
"poetry.lock",
|
|
81
|
+
"uv.lock",
|
|
82
|
+
"cargo.lock",
|
|
83
|
+
"go.sum",
|
|
84
|
+
"gemfile.lock",
|
|
85
|
+
"composer.lock",
|
|
86
|
+
"mix.lock",
|
|
87
|
+
]);
|
|
88
|
+
const BUILD_NAMES = new Set([
|
|
89
|
+
"makefile",
|
|
90
|
+
"justfile",
|
|
91
|
+
"taskfile.yml",
|
|
92
|
+
"taskfile.yaml",
|
|
93
|
+
"dockerfile",
|
|
94
|
+
"compose.yml",
|
|
95
|
+
"compose.yaml",
|
|
96
|
+
"docker-compose.yml",
|
|
97
|
+
"docker-compose.yaml",
|
|
98
|
+
"cmakelists.txt",
|
|
99
|
+
"build",
|
|
100
|
+
"build.bazel",
|
|
101
|
+
"workspace",
|
|
102
|
+
"jenkinsfile",
|
|
103
|
+
]);
|
|
104
|
+
const CONFIG_NAMES = new Set([
|
|
105
|
+
"tsconfig.json",
|
|
106
|
+
"jsconfig.json",
|
|
107
|
+
"vite.config.ts",
|
|
108
|
+
"vite.config.js",
|
|
109
|
+
"vitest.config.ts",
|
|
110
|
+
"jest.config.js",
|
|
111
|
+
"eslint.config.js",
|
|
112
|
+
".eslintrc.json",
|
|
113
|
+
"tox.ini",
|
|
114
|
+
"pytest.ini",
|
|
115
|
+
"ruff.toml",
|
|
116
|
+
]);
|
|
117
|
+
export class RepositoryProfiler {
|
|
118
|
+
limits;
|
|
119
|
+
constructor(limits = {}) {
|
|
120
|
+
this.limits = limits;
|
|
121
|
+
}
|
|
122
|
+
/** Builds a bounded, language-neutral snapshot with optional JS/TS enrichment. */
|
|
123
|
+
async profile(root) {
|
|
124
|
+
const absoluteRoot = resolve(root);
|
|
125
|
+
const inventory = await this.inventory(absoluteRoot);
|
|
126
|
+
const [packageJson, gitignore] = await Promise.all([
|
|
127
|
+
this.readPackage(absoluteRoot),
|
|
128
|
+
this.readGitignore(absoluteRoot),
|
|
129
|
+
]);
|
|
130
|
+
const paths = new Set(inventory.entries.map((entry) => entry.path));
|
|
131
|
+
const sourceRoots = [...SOURCE_DIRS].filter((name) => inventory.entries.some((entry) => entry.path.startsWith(`${name}/`)));
|
|
132
|
+
const testRoots = [...TEST_DIRS].filter((name) => inventory.entries.some((entry) => entry.path.startsWith(`${name}/`)));
|
|
133
|
+
const packageManager = paths.has("pnpm-lock.yaml")
|
|
134
|
+
? "pnpm"
|
|
135
|
+
: paths.has("yarn.lock")
|
|
136
|
+
? "yarn"
|
|
137
|
+
: paths.has("bun.lockb") || paths.has("bun.lock")
|
|
138
|
+
? "bun"
|
|
139
|
+
: paths.has("package-lock.json")
|
|
140
|
+
? "npm"
|
|
141
|
+
: "unknown";
|
|
142
|
+
const files = await this.indexFiles(absoluteRoot, inventory.entries);
|
|
143
|
+
const snapshot = {
|
|
144
|
+
schemaVersion: 1,
|
|
145
|
+
root: absoluteRoot,
|
|
146
|
+
fingerprint: this.inventoryFingerprint(inventory),
|
|
147
|
+
entries: inventory.entries,
|
|
148
|
+
ecosystems: this.ecosystems(paths),
|
|
149
|
+
changedPaths: inventory.git?.changedPaths ?? [],
|
|
150
|
+
instructionFiles: this.pathsOf(inventory.entries, "instruction"),
|
|
151
|
+
documentationFiles: this.pathsOf(inventory.entries, "documentation"),
|
|
152
|
+
manifestFiles: this.pathsOf(inventory.entries, "manifest"),
|
|
153
|
+
ciFiles: this.pathsOf(inventory.entries, "ci"),
|
|
154
|
+
buildFiles: this.pathsOf(inventory.entries, "build"),
|
|
155
|
+
truncated: inventory.truncated,
|
|
156
|
+
packageName: typeof packageJson?.name === "string" ? packageJson.name : undefined,
|
|
157
|
+
packageManager,
|
|
158
|
+
scripts: this.scripts(packageJson),
|
|
159
|
+
configFiles: this.pathsOf(inventory.entries, "config"),
|
|
160
|
+
sourceRoots,
|
|
161
|
+
testRoots,
|
|
162
|
+
ignoredPaths: [...new Set([...DEFAULT_IGNORES, ...gitignore])].sort(),
|
|
163
|
+
indexedFiles: files.map((file) => file.path),
|
|
164
|
+
files: this.connectFiles(files, sourceRoots, testRoots),
|
|
165
|
+
verificationCandidates: [],
|
|
166
|
+
createdAt: Date.now(),
|
|
167
|
+
};
|
|
168
|
+
snapshot.verificationCandidates = new VerificationPlanner().candidates(snapshot);
|
|
169
|
+
return snapshot;
|
|
170
|
+
}
|
|
171
|
+
/** Computes the same deterministic identity used by a full snapshot. */
|
|
172
|
+
async fingerprint(root) {
|
|
173
|
+
return this.inventoryFingerprint(await this.inventory(resolve(root)));
|
|
174
|
+
}
|
|
175
|
+
async inventory(root) {
|
|
176
|
+
const git = await this.gitMetadata(root);
|
|
177
|
+
const paths = git
|
|
178
|
+
? await this.gitPaths(root)
|
|
179
|
+
: await this.walkPaths(root, new Set(await this.readGitignore(root)));
|
|
180
|
+
const maxEntries = this.limits.maxInventoryEntries ?? MAX_INVENTORY_ENTRIES;
|
|
181
|
+
const truncated = paths.length > maxEntries;
|
|
182
|
+
const entries = [];
|
|
183
|
+
for (const path of paths.slice(0, maxEntries).sort()) {
|
|
184
|
+
const full = resolve(root, path);
|
|
185
|
+
if (full !== root && !full.startsWith(`${root}${sep}`))
|
|
186
|
+
continue;
|
|
187
|
+
try {
|
|
188
|
+
const file = await lstat(full);
|
|
189
|
+
if (!file.isFile() || file.isSymbolicLink())
|
|
190
|
+
continue;
|
|
191
|
+
const kind = this.classify(path);
|
|
192
|
+
entries.push({
|
|
193
|
+
path: path.split(sep).join("/"),
|
|
194
|
+
kind,
|
|
195
|
+
size: file.size,
|
|
196
|
+
mtimeMs: Math.trunc(file.mtimeMs),
|
|
197
|
+
contentHash: this.isControlFile(kind) &&
|
|
198
|
+
file.size <= (this.limits.maxControlFileBytes ?? MAX_CONTROL_FILE_BYTES)
|
|
199
|
+
? await this.hashFile(full)
|
|
200
|
+
: undefined,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
// A concurrent edit can remove a file; the next fingerprint will observe it.
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return { entries, truncated, git };
|
|
208
|
+
}
|
|
209
|
+
async gitMetadata(root) {
|
|
210
|
+
try {
|
|
211
|
+
const [{ stdout: gitRoot }, head, branch, { stdout: status }] = await Promise.all([
|
|
212
|
+
execute(gitExecutable, ["-C", root, "rev-parse", "--show-toplevel"], {
|
|
213
|
+
encoding: "utf8",
|
|
214
|
+
}),
|
|
215
|
+
execute(gitExecutable, ["-C", root, "rev-parse", "HEAD"], { encoding: "utf8" }).catch(() => ({
|
|
216
|
+
stdout: "",
|
|
217
|
+
})),
|
|
218
|
+
execute(gitExecutable, ["-C", root, "branch", "--show-current"], {
|
|
219
|
+
encoding: "utf8",
|
|
220
|
+
}).catch(() => ({ stdout: "" })),
|
|
221
|
+
execute(gitExecutable, ["-C", root, "status", "--porcelain=v1", "-z", "--untracked-files=all"], {
|
|
222
|
+
encoding: "utf8",
|
|
223
|
+
}),
|
|
224
|
+
]);
|
|
225
|
+
return {
|
|
226
|
+
root: gitRoot.trim(),
|
|
227
|
+
head: head.stdout.trim() || undefined,
|
|
228
|
+
branch: branch.stdout.trim() || undefined,
|
|
229
|
+
changedPaths: this.changedPaths(status),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
async gitPaths(root) {
|
|
237
|
+
const { stdout } = await execute(gitExecutable, ["-C", root, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 });
|
|
238
|
+
return stdout.split("\0").filter(Boolean);
|
|
239
|
+
}
|
|
240
|
+
async walkPaths(root, ignored) {
|
|
241
|
+
const paths = [];
|
|
242
|
+
const visit = async (directory) => {
|
|
243
|
+
const maxEntries = this.limits.maxInventoryEntries ?? MAX_INVENTORY_ENTRIES;
|
|
244
|
+
if (paths.length > maxEntries)
|
|
245
|
+
return;
|
|
246
|
+
let entries;
|
|
247
|
+
try {
|
|
248
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
254
|
+
const path = relative(root, join(directory, entry.name)).split(sep).join("/");
|
|
255
|
+
if (DEFAULT_IGNORES.has(entry.name) ||
|
|
256
|
+
ignored.has(entry.name) ||
|
|
257
|
+
ignored.has(path) ||
|
|
258
|
+
entry.isSymbolicLink())
|
|
259
|
+
continue;
|
|
260
|
+
const full = join(directory, entry.name);
|
|
261
|
+
if (entry.isDirectory())
|
|
262
|
+
await visit(full);
|
|
263
|
+
else if (entry.isFile())
|
|
264
|
+
paths.push(relative(root, full));
|
|
265
|
+
if (paths.length > maxEntries)
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
await visit(root);
|
|
270
|
+
return paths;
|
|
271
|
+
}
|
|
272
|
+
changedPaths(status) {
|
|
273
|
+
const fields = status.split("\0").filter(Boolean);
|
|
274
|
+
const paths = [];
|
|
275
|
+
for (let index = 0; index < fields.length; index += 1) {
|
|
276
|
+
const field = fields[index];
|
|
277
|
+
if (field.length < 4)
|
|
278
|
+
continue;
|
|
279
|
+
paths.push(field.slice(3));
|
|
280
|
+
if (field[0] === "R" || field[0] === "C" || field[1] === "R" || field[1] === "C")
|
|
281
|
+
index += 1;
|
|
282
|
+
}
|
|
283
|
+
return [...new Set(paths)].sort();
|
|
284
|
+
}
|
|
285
|
+
inventoryFingerprint(inventory) {
|
|
286
|
+
const payload = {
|
|
287
|
+
gitRoot: inventory.git?.root,
|
|
288
|
+
branch: inventory.git?.branch,
|
|
289
|
+
head: inventory.git?.head,
|
|
290
|
+
changedPaths: inventory.git?.changedPaths,
|
|
291
|
+
truncated: inventory.truncated,
|
|
292
|
+
entries: inventory.entries.map(({ path, size, mtimeMs, contentHash }) => ({
|
|
293
|
+
path,
|
|
294
|
+
size,
|
|
295
|
+
mtimeMs,
|
|
296
|
+
contentHash,
|
|
297
|
+
})),
|
|
298
|
+
};
|
|
299
|
+
return {
|
|
300
|
+
value: createHash("sha256").update(JSON.stringify(payload)).digest("hex"),
|
|
301
|
+
kind: inventory.git ? "git" : "filesystem",
|
|
302
|
+
gitRoot: inventory.git?.root,
|
|
303
|
+
branch: inventory.git?.branch,
|
|
304
|
+
head: inventory.git?.head,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
classify(path) {
|
|
308
|
+
const lower = path.replaceAll("\\", "/").toLowerCase();
|
|
309
|
+
const name = basename(lower);
|
|
310
|
+
if (name === "agents.md" ||
|
|
311
|
+
name === "claude.md" ||
|
|
312
|
+
lower === ".github/copilot-instructions.md" ||
|
|
313
|
+
lower.startsWith(".cursor/rules/"))
|
|
314
|
+
return "instruction";
|
|
315
|
+
if (MANIFEST_NAMES.has(name) || /\.(?:sln|csproj)$/.test(name))
|
|
316
|
+
return "manifest";
|
|
317
|
+
if (lower.startsWith(".github/workflows/") ||
|
|
318
|
+
lower === ".gitlab-ci.yml" ||
|
|
319
|
+
lower === ".gitlab-ci.yaml" ||
|
|
320
|
+
lower.startsWith(".circleci/"))
|
|
321
|
+
return "ci";
|
|
322
|
+
if (BUILD_NAMES.has(name) || /^dockerfile(?:\..+)?$/.test(name))
|
|
323
|
+
return "build";
|
|
324
|
+
if (name.startsWith("readme") ||
|
|
325
|
+
name.startsWith("contributing") ||
|
|
326
|
+
name.startsWith("architecture") ||
|
|
327
|
+
lower.startsWith("docs/"))
|
|
328
|
+
return "documentation";
|
|
329
|
+
if (CONFIG_NAMES.has(name) || /(?:^|\/)[^/]+\.config\.[^/]+$/.test(lower))
|
|
330
|
+
return "config";
|
|
331
|
+
const segments = lower.split("/");
|
|
332
|
+
if (segments.some((segment) => TEST_DIRS.has(segment)) ||
|
|
333
|
+
/(?:^|[._-])(test|spec)\.[^.]+$/.test(name))
|
|
334
|
+
return "test";
|
|
335
|
+
if (segments.some((segment) => SOURCE_DIRS.has(segment)) ||
|
|
336
|
+
SOURCE_FILE_EXTENSIONS.has(extname(name)))
|
|
337
|
+
return "source";
|
|
338
|
+
return "other";
|
|
339
|
+
}
|
|
340
|
+
ecosystems(paths) {
|
|
341
|
+
const detected = new Set();
|
|
342
|
+
const has = (...names) => names.some((name) => paths.has(name));
|
|
343
|
+
if (has("package.json"))
|
|
344
|
+
detected.add("node");
|
|
345
|
+
if (has("pyproject.toml", "requirements.txt", "setup.py"))
|
|
346
|
+
detected.add("python");
|
|
347
|
+
if (has("go.mod"))
|
|
348
|
+
detected.add("go");
|
|
349
|
+
if (has("Cargo.toml"))
|
|
350
|
+
detected.add("rust");
|
|
351
|
+
if (has("pom.xml", "build.gradle", "build.gradle.kts"))
|
|
352
|
+
detected.add("jvm");
|
|
353
|
+
if (has("Gemfile"))
|
|
354
|
+
detected.add("ruby");
|
|
355
|
+
if (has("composer.json"))
|
|
356
|
+
detected.add("php");
|
|
357
|
+
if (has("mix.exs"))
|
|
358
|
+
detected.add("elixir");
|
|
359
|
+
if ([...paths].some((path) => /\.(?:sln|csproj)$/.test(path)))
|
|
360
|
+
detected.add("dotnet");
|
|
361
|
+
return [...detected].sort();
|
|
362
|
+
}
|
|
363
|
+
pathsOf(entries, kind) {
|
|
364
|
+
return entries.filter((entry) => entry.kind === kind).map((entry) => entry.path);
|
|
365
|
+
}
|
|
366
|
+
isControlFile(kind) {
|
|
367
|
+
return kind === "instruction" || kind === "manifest" || kind === "ci" || kind === "build";
|
|
368
|
+
}
|
|
369
|
+
async hashFile(path) {
|
|
370
|
+
return createHash("sha256")
|
|
371
|
+
.update(await readFile(path))
|
|
372
|
+
.digest("hex");
|
|
373
|
+
}
|
|
374
|
+
async readPackage(root) {
|
|
375
|
+
try {
|
|
376
|
+
const parsed = JSON.parse(await readFile(join(root, "package.json"), "utf8"));
|
|
377
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
378
|
+
? parsed
|
|
379
|
+
: undefined;
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
return undefined;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
async readGitignore(root) {
|
|
386
|
+
try {
|
|
387
|
+
return (await readFile(join(root, ".gitignore"), "utf8"))
|
|
388
|
+
.split("\n")
|
|
389
|
+
.map((line) => line.trim().replace(/\/$/, ""))
|
|
390
|
+
.filter((line) => line && !line.startsWith("#") && !line.includes("*") && !line.startsWith("!"));
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
return [];
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
scripts(packageJson) {
|
|
397
|
+
const scripts = packageJson?.scripts;
|
|
398
|
+
if (!scripts || typeof scripts !== "object" || Array.isArray(scripts))
|
|
399
|
+
return {};
|
|
400
|
+
return Object.fromEntries(Object.entries(scripts).filter((entry) => typeof entry[1] === "string"));
|
|
401
|
+
}
|
|
402
|
+
async indexFiles(root, entries) {
|
|
403
|
+
const files = [];
|
|
404
|
+
for (const entry of entries) {
|
|
405
|
+
if (files.length >= MAX_INDEXED_FILES || !this.isUseful(entry))
|
|
406
|
+
continue;
|
|
407
|
+
const text = await this.readText(join(root, entry.path));
|
|
408
|
+
if (!text)
|
|
409
|
+
continue;
|
|
410
|
+
const isJavaScript = SOURCE_EXTENSIONS.includes(extname(entry.path));
|
|
411
|
+
files.push({
|
|
412
|
+
path: entry.path,
|
|
413
|
+
terms: this.terms(text),
|
|
414
|
+
symbols: isJavaScript ? this.symbols(text) : [],
|
|
415
|
+
imports: isJavaScript ? this.imports(text) : [],
|
|
416
|
+
relatedFiles: [],
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
return files;
|
|
420
|
+
}
|
|
421
|
+
async readText(path) {
|
|
422
|
+
try {
|
|
423
|
+
const content = await readFile(path);
|
|
424
|
+
if (content.subarray(0, 8_000).includes(0))
|
|
425
|
+
return "";
|
|
426
|
+
return content.toString("utf8");
|
|
427
|
+
}
|
|
428
|
+
catch {
|
|
429
|
+
return "";
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
terms(text) {
|
|
433
|
+
return [text, text.replace(/([a-z])([A-Z])/g, "$1 $2")]
|
|
434
|
+
.flatMap((part) => part.toLowerCase().split(/[^a-z0-9_$]+/))
|
|
435
|
+
.filter((term) => term.length > 2)
|
|
436
|
+
.filter((term, index, terms) => terms.indexOf(term) === index)
|
|
437
|
+
.slice(0, MAX_TERMS_PER_FILE);
|
|
438
|
+
}
|
|
439
|
+
symbols(text) {
|
|
440
|
+
return [
|
|
441
|
+
...text.matchAll(/(?:function|class|interface|type|enum|const|let|var)\s+([A-Za-z_$][\w$]*)/g),
|
|
442
|
+
]
|
|
443
|
+
.map((match) => match[1].toLowerCase())
|
|
444
|
+
.slice(0, MAX_SYMBOLS_PER_FILE);
|
|
445
|
+
}
|
|
446
|
+
connectFiles(files, sourceRoots, testRoots) {
|
|
447
|
+
const paths = new Set(files.map((file) => file.path));
|
|
448
|
+
const byPath = new Map(files.map((file) => [file.path, file]));
|
|
449
|
+
for (const file of files) {
|
|
450
|
+
const targets = new Set();
|
|
451
|
+
for (const specifier of file.imports) {
|
|
452
|
+
const target = this.resolveImport(file.path, specifier, paths);
|
|
453
|
+
if (target)
|
|
454
|
+
targets.add(target);
|
|
455
|
+
}
|
|
456
|
+
const testTarget = this.testTarget(file.path, paths, sourceRoots, testRoots);
|
|
457
|
+
if (testTarget)
|
|
458
|
+
targets.add(testTarget);
|
|
459
|
+
file.relatedFiles = [...targets].sort();
|
|
460
|
+
}
|
|
461
|
+
for (const file of files) {
|
|
462
|
+
for (const target of file.relatedFiles) {
|
|
463
|
+
const targetFile = byPath.get(target);
|
|
464
|
+
if (targetFile && !targetFile.relatedFiles.includes(file.path))
|
|
465
|
+
targetFile.relatedFiles.push(file.path);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return files.map((file) => ({ ...file, relatedFiles: file.relatedFiles.sort() }));
|
|
469
|
+
}
|
|
470
|
+
imports(text) {
|
|
471
|
+
return [...text.matchAll(/(?:import|export)\s+(?:[\s\S]*?\s+from\s+)?["']([^"']+)["']/g)]
|
|
472
|
+
.map((match) => match[1])
|
|
473
|
+
.filter((specifier) => specifier.startsWith("."));
|
|
474
|
+
}
|
|
475
|
+
resolveImport(from, specifier, paths) {
|
|
476
|
+
const base = posix.normalize(posix.join(posix.dirname(from), specifier));
|
|
477
|
+
const extensionless = base.replace(/\.[^.\/]+$/, "");
|
|
478
|
+
return [
|
|
479
|
+
base,
|
|
480
|
+
extensionless,
|
|
481
|
+
...SOURCE_EXTENSIONS.map((extension) => `${extensionless}${extension}`),
|
|
482
|
+
...SOURCE_EXTENSIONS.map((extension) => `${extensionless}/index${extension}`),
|
|
483
|
+
].find((candidate) => paths.has(candidate));
|
|
484
|
+
}
|
|
485
|
+
testTarget(file, paths, sourceRoots, testRoots) {
|
|
486
|
+
if (!testRoots.some((root) => file.startsWith(`${root}/`)))
|
|
487
|
+
return undefined;
|
|
488
|
+
const name = basename(file).replace(/\.(test|spec)\.[^.]+$/, "");
|
|
489
|
+
return sourceRoots
|
|
490
|
+
.flatMap((root) => SOURCE_EXTENSIONS.map((extension) => `${root}/${name}${extension}`))
|
|
491
|
+
.find((candidate) => paths.has(candidate));
|
|
492
|
+
}
|
|
493
|
+
isUseful(entry) {
|
|
494
|
+
return (entry.size <= MAX_INDEXED_FILE_BYTES &&
|
|
495
|
+
!LOCKFILE_NAMES.has(basename(entry.path).toLowerCase()) &&
|
|
496
|
+
!/(\.min\.js|\.map|\.lock)$/i.test(entry.path));
|
|
497
|
+
}
|
|
498
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { CredentialId } from "../../domain/models.js";
|
|
2
|
+
import type { CredentialStore } from "../../domain/ports.js";
|
|
3
|
+
type KeychainCommand = (file: string, args: string[]) => Promise<{
|
|
4
|
+
stdout: string;
|
|
5
|
+
}>;
|
|
6
|
+
export declare class MacOSKeychainStore implements CredentialStore {
|
|
7
|
+
private readonly command;
|
|
8
|
+
private readonly env;
|
|
9
|
+
constructor(command?: KeychainCommand, env?: NodeJS.ProcessEnv);
|
|
10
|
+
/** Reads an environment override first, then the macOS Keychain credential. */
|
|
11
|
+
get(provider: CredentialId): Promise<string | undefined>;
|
|
12
|
+
/** Saves a non-empty provider key in the macOS Keychain rather than local config. */
|
|
13
|
+
save(provider: CredentialId, value: string): Promise<void>;
|
|
14
|
+
/** Removes Kairo's saved Keychain entry during logout. */
|
|
15
|
+
clear(provider: CredentialId): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
const run = promisify(execFile);
|
|
4
|
+
const account = "default";
|
|
5
|
+
const services = {
|
|
6
|
+
gemini: "dev.kairo.gemini",
|
|
7
|
+
groq: "dev.kairo.groq",
|
|
8
|
+
mistral: "dev.kairo.mistral",
|
|
9
|
+
jev: "dev.kairo.jev",
|
|
10
|
+
};
|
|
11
|
+
const environment = {
|
|
12
|
+
gemini: "GEMINI_API_KEY",
|
|
13
|
+
groq: "GROQ_API_KEY",
|
|
14
|
+
mistral: "MISTRAL_API_KEY",
|
|
15
|
+
jev: "TYPESAFE_API_KEY",
|
|
16
|
+
};
|
|
17
|
+
export class MacOSKeychainStore {
|
|
18
|
+
command;
|
|
19
|
+
env;
|
|
20
|
+
constructor(command = run, env = process.env) {
|
|
21
|
+
this.command = command;
|
|
22
|
+
this.env = env;
|
|
23
|
+
}
|
|
24
|
+
/** Reads an environment override first, then the macOS Keychain credential. */
|
|
25
|
+
async get(provider) {
|
|
26
|
+
const override = this.env[environment[provider]];
|
|
27
|
+
if (override)
|
|
28
|
+
return override;
|
|
29
|
+
try {
|
|
30
|
+
return ((await this.command("security", [
|
|
31
|
+
"find-generic-password",
|
|
32
|
+
"-s",
|
|
33
|
+
services[provider],
|
|
34
|
+
"-a",
|
|
35
|
+
account,
|
|
36
|
+
"-w",
|
|
37
|
+
])).stdout.trim() || undefined);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** Saves a non-empty provider key in the macOS Keychain rather than local config. */
|
|
44
|
+
async save(provider, value) {
|
|
45
|
+
if (!value.trim())
|
|
46
|
+
throw new Error("API key cannot be empty.");
|
|
47
|
+
await this.command("security", [
|
|
48
|
+
"add-generic-password",
|
|
49
|
+
"-U",
|
|
50
|
+
"-s",
|
|
51
|
+
services[provider],
|
|
52
|
+
"-a",
|
|
53
|
+
account,
|
|
54
|
+
"-w",
|
|
55
|
+
value.trim(),
|
|
56
|
+
]);
|
|
57
|
+
}
|
|
58
|
+
/** Removes Kairo's saved Keychain entry during logout. */
|
|
59
|
+
async clear(provider) {
|
|
60
|
+
try {
|
|
61
|
+
await this.command("security", [
|
|
62
|
+
"delete-generic-password",
|
|
63
|
+
"-s",
|
|
64
|
+
services[provider],
|
|
65
|
+
"-a",
|
|
66
|
+
account,
|
|
67
|
+
]);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
/* missing credential is already logged out */
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ToolCall, ToolResult } from "../../domain/models.js";
|
|
2
|
+
import type { ToolDefinition, ToolExecutor } from "../../domain/ports.js";
|
|
3
|
+
export declare const definitions: ToolDefinition[];
|
|
4
|
+
export declare class WorkspaceTools implements ToolExecutor {
|
|
5
|
+
readonly root: string;
|
|
6
|
+
/** Stores the already-resolved workspace root used for every safety check. */
|
|
7
|
+
private constructor();
|
|
8
|
+
/** Resolves the requested workspace once before exposing any file tools. */
|
|
9
|
+
static create(workspace: string): Promise<WorkspaceTools>;
|
|
10
|
+
/** Returns whether an absolute path remains inside the resolved workspace root. */
|
|
11
|
+
private inside;
|
|
12
|
+
/** Validates a user path and rejects traversal or symlink escapes before access. */
|
|
13
|
+
private filePath;
|
|
14
|
+
/** Resolves an optional directory, treating a blank value as the workspace root. */
|
|
15
|
+
private directoryPath;
|
|
16
|
+
/** Produces the exact action text shown in the terminal approval prompt. */
|
|
17
|
+
description(call: ToolCall): string;
|
|
18
|
+
/** Dispatches a validated tool request and converts executor errors into tool results. */
|
|
19
|
+
execute(call: ToolCall): Promise<ToolResult>;
|
|
20
|
+
/** Recursively lists a bounded set of non-ignored workspace files. */
|
|
21
|
+
private list;
|
|
22
|
+
/** Searches readable workspace files for a literal query with bounded matches. */
|
|
23
|
+
private search;
|
|
24
|
+
/** Returns an inclusive, line-numbered slice while enforcing a small read limit. */
|
|
25
|
+
private readRange;
|
|
26
|
+
/** Replaces one unambiguous text occurrence in a workspace file. */
|
|
27
|
+
private edit;
|
|
28
|
+
/** Runs a shell command in the workspace and captures bounded output and timing. */
|
|
29
|
+
private command;
|
|
30
|
+
}
|