codelocal 1.5.0-beta.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/README.md +27 -0
- package/dist/approval-memory.js +105 -0
- package/dist/audit.js +34 -0
- package/dist/chat-approval.js +77 -0
- package/dist/cli-saas.js +311 -0
- package/dist/cli.js +344 -0
- package/dist/client-entry-v2.js +22 -0
- package/dist/client-v2.js +910 -0
- package/dist/cloud-client-sync.js +6 -0
- package/dist/context-engine.js +295 -0
- package/dist/editing-engine.js +205 -0
- package/dist/identity.js +30 -0
- package/dist/log.js +235 -0
- package/dist/lsp.js +288 -0
- package/dist/mcp-cloud-sync.js +3 -0
- package/dist/mcp-hub.js +508 -0
- package/dist/native-watcher.js +148 -0
- package/dist/process-manager.js +261 -0
- package/dist/protocol.js +52 -0
- package/dist/runtime-daemon.js +162 -0
- package/dist/security-policy.js +293 -0
- package/dist/semantic-router.js +378 -0
- package/dist/semantic.js +263 -0
- package/dist/state.js +110 -0
- package/dist/terminal-history.js +102 -0
- package/dist/verification.js +66 -0
- package/dist/workspace-index.js +457 -0
- package/dist/workspace-registry.js +86 -0
- package/package.json +31 -0
package/dist/semantic.js
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { readdirSync } from "node:fs";
|
|
4
|
+
const SKIP_DIRS = new Set([".git", "node_modules", ".next", "dist", "build", "target", ".venv", "venv", "coverage", ".cache", ".turbo", ".dart_tool", "Pods", "DerivedData"]);
|
|
5
|
+
function rel(root, file) {
|
|
6
|
+
return path.relative(path.resolve(root), path.resolve(file)).split(path.sep).join("/") || ".";
|
|
7
|
+
}
|
|
8
|
+
function inside(root, file) {
|
|
9
|
+
const relative = path.relative(path.resolve(root), path.resolve(file));
|
|
10
|
+
return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
|
|
11
|
+
}
|
|
12
|
+
function isNodeModules(root, file) {
|
|
13
|
+
const relative = rel(root, file);
|
|
14
|
+
return relative === "node_modules" || relative.startsWith("node_modules/") || relative.includes("/node_modules/");
|
|
15
|
+
}
|
|
16
|
+
function pos(source, node) {
|
|
17
|
+
const p = source.getLineAndCharacterOfPosition(node.getStart(source));
|
|
18
|
+
return { line: p.line + 1, column: p.character + 1 };
|
|
19
|
+
}
|
|
20
|
+
function nodeName(node) {
|
|
21
|
+
const anyNode = node;
|
|
22
|
+
return anyNode.name;
|
|
23
|
+
}
|
|
24
|
+
function kindName(node) {
|
|
25
|
+
return ts.SyntaxKind[node.kind] ?? "Unknown";
|
|
26
|
+
}
|
|
27
|
+
function discoverConfigs(root, maxDepth, maxProjects) {
|
|
28
|
+
const out = [];
|
|
29
|
+
const walk = (dir, depth) => {
|
|
30
|
+
if (depth > maxDepth || out.length >= maxProjects)
|
|
31
|
+
return;
|
|
32
|
+
let entries;
|
|
33
|
+
try {
|
|
34
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const names = new Set(entries.map((entry) => entry.name));
|
|
40
|
+
for (const candidate of ["tsconfig.json", "jsconfig.json"]) {
|
|
41
|
+
if (names.has(candidate)) {
|
|
42
|
+
out.push(path.join(dir, candidate));
|
|
43
|
+
if (out.length >= maxProjects)
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (const entry of entries) {
|
|
48
|
+
if (!entry.isDirectory() || SKIP_DIRS.has(entry.name))
|
|
49
|
+
continue;
|
|
50
|
+
walk(path.join(dir, entry.name), depth + 1);
|
|
51
|
+
if (out.length >= maxProjects)
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
walk(root, 0);
|
|
56
|
+
return out.sort((a, b) => {
|
|
57
|
+
const da = rel(root, a).split("/").length;
|
|
58
|
+
const db = rel(root, b).split("/").length;
|
|
59
|
+
return da - db || a.localeCompare(b);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
export class TypeScriptSemanticIndex {
|
|
63
|
+
root;
|
|
64
|
+
projects = [];
|
|
65
|
+
configPaths = [];
|
|
66
|
+
builtAt = 0;
|
|
67
|
+
attempted = false;
|
|
68
|
+
constructor(root) {
|
|
69
|
+
this.root = root;
|
|
70
|
+
}
|
|
71
|
+
invalidate() {
|
|
72
|
+
this.projects = [];
|
|
73
|
+
this.configPaths = [];
|
|
74
|
+
this.builtAt = 0;
|
|
75
|
+
this.attempted = false;
|
|
76
|
+
}
|
|
77
|
+
build() {
|
|
78
|
+
if (this.attempted)
|
|
79
|
+
return;
|
|
80
|
+
this.attempted = true;
|
|
81
|
+
const maxProjects = Math.max(1, Number(process.env.CODELOCAL_TS_MAX_PROJECTS ?? 20) || 20);
|
|
82
|
+
const maxDepth = Math.max(1, Number(process.env.CODELOCAL_TS_CONFIG_DEPTH ?? 6) || 6);
|
|
83
|
+
const maxFilesPerProject = Math.max(100, Number(process.env.CODELOCAL_TS_MAX_FILES_PER_PROJECT ?? 4_000) || 4_000);
|
|
84
|
+
const maxTotalFiles = Math.max(maxFilesPerProject, Number(process.env.CODELOCAL_TS_MAX_TOTAL_FILES ?? 12_000) || 12_000);
|
|
85
|
+
const configs = discoverConfigs(this.root, maxDepth, maxProjects);
|
|
86
|
+
let remaining = maxTotalFiles;
|
|
87
|
+
for (const config of configs) {
|
|
88
|
+
if (remaining <= 0)
|
|
89
|
+
break;
|
|
90
|
+
const read = ts.readConfigFile(config, ts.sys.readFile);
|
|
91
|
+
if (read.error)
|
|
92
|
+
continue;
|
|
93
|
+
const parsed = ts.parseJsonConfigFileContent(read.config ?? {}, ts.sys, path.dirname(config));
|
|
94
|
+
const rootNames = parsed.fileNames.slice(0, Math.min(maxFilesPerProject, remaining));
|
|
95
|
+
if (!rootNames.length)
|
|
96
|
+
continue;
|
|
97
|
+
remaining -= rootNames.length;
|
|
98
|
+
const options = { ...parsed.options, noEmit: true, skipLibCheck: true };
|
|
99
|
+
try {
|
|
100
|
+
const program = ts.createProgram({ rootNames, options });
|
|
101
|
+
this.projects.push({ configPath: config, program, checker: program.getTypeChecker() });
|
|
102
|
+
this.configPaths.push(config);
|
|
103
|
+
}
|
|
104
|
+
catch { }
|
|
105
|
+
}
|
|
106
|
+
this.builtAt = Date.now();
|
|
107
|
+
}
|
|
108
|
+
info() {
|
|
109
|
+
this.build();
|
|
110
|
+
return {
|
|
111
|
+
available: this.projects.length > 0,
|
|
112
|
+
configured: this.configPaths.length > 0,
|
|
113
|
+
mode: this.projects.length > 1 ? "typescript-multi-project" : this.projects.length === 1 ? "typescript-program" : "fallback",
|
|
114
|
+
configPath: this.configPaths[0] ? rel(this.root, this.configPaths[0]) : null,
|
|
115
|
+
configPaths: this.configPaths.map((config) => rel(this.root, config)),
|
|
116
|
+
projectCount: this.projects.length,
|
|
117
|
+
sourceFiles: this.projectSources().length,
|
|
118
|
+
builtAt: this.builtAt,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
projectSources() {
|
|
122
|
+
this.build();
|
|
123
|
+
const unique = new Map();
|
|
124
|
+
for (const project of this.projects) {
|
|
125
|
+
for (const source of project.program.getSourceFiles()) {
|
|
126
|
+
if (!inside(this.root, source.fileName))
|
|
127
|
+
continue;
|
|
128
|
+
if (isNodeModules(this.root, source.fileName))
|
|
129
|
+
continue;
|
|
130
|
+
unique.set(path.resolve(source.fileName), source);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return [...unique.values()];
|
|
134
|
+
}
|
|
135
|
+
workspaceSymbols(query = "", limit = 200) {
|
|
136
|
+
const q = query.toLowerCase();
|
|
137
|
+
const out = [];
|
|
138
|
+
for (const source of this.projectSources()) {
|
|
139
|
+
const visit = (node) => {
|
|
140
|
+
const name = nodeName(node);
|
|
141
|
+
if (name && (ts.isIdentifier(name) || ts.isStringLiteral(name))) {
|
|
142
|
+
const n = name.text;
|
|
143
|
+
if (!q || n.toLowerCase().includes(q)) {
|
|
144
|
+
out.push({ path: rel(this.root, source.fileName), ...pos(source, name), kind: kindName(node), name: n });
|
|
145
|
+
if (out.length >= limit)
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (out.length < limit)
|
|
150
|
+
ts.forEachChild(node, visit);
|
|
151
|
+
};
|
|
152
|
+
visit(source);
|
|
153
|
+
if (out.length >= limit)
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
definitions(name, limit = 100) {
|
|
159
|
+
return this.workspaceSymbols(name, limit).filter((x) => x.name === name);
|
|
160
|
+
}
|
|
161
|
+
references(name, limit = 500) {
|
|
162
|
+
const out = [];
|
|
163
|
+
for (const source of this.projectSources()) {
|
|
164
|
+
const visit = (node) => {
|
|
165
|
+
if (ts.isIdentifier(node) && node.text === name) {
|
|
166
|
+
out.push({ path: rel(this.root, source.fileName), ...pos(source, node), kind: kindName(node.parent), name });
|
|
167
|
+
}
|
|
168
|
+
if (out.length < limit)
|
|
169
|
+
ts.forEachChild(node, visit);
|
|
170
|
+
};
|
|
171
|
+
visit(source);
|
|
172
|
+
if (out.length >= limit)
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
callers(name, limit = 300) {
|
|
178
|
+
const out = [];
|
|
179
|
+
for (const source of this.projectSources()) {
|
|
180
|
+
const stack = [];
|
|
181
|
+
const visit = (node) => {
|
|
182
|
+
stack.push(node);
|
|
183
|
+
if (ts.isCallExpression(node)) {
|
|
184
|
+
const expression = node.expression;
|
|
185
|
+
const called = ts.isIdentifier(expression) ? expression.text : ts.isPropertyAccessExpression(expression) ? expression.name.text : null;
|
|
186
|
+
if (called === name) {
|
|
187
|
+
const owner = stack.slice(0, -1).reverse().find((n) => ts.isFunctionDeclaration(n) || ts.isMethodDeclaration(n) || ts.isArrowFunction(n) || ts.isFunctionExpression(n));
|
|
188
|
+
const ownerName = owner ? nodeName(owner) : undefined;
|
|
189
|
+
out.push({ path: rel(this.root, source.fileName), ...pos(source, node), kind: "CallExpression", name: ownerName && ts.isIdentifier(ownerName) ? ownerName.text : undefined });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (out.length < limit)
|
|
193
|
+
ts.forEachChild(node, visit);
|
|
194
|
+
stack.pop();
|
|
195
|
+
};
|
|
196
|
+
visit(source);
|
|
197
|
+
if (out.length >= limit)
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
return out;
|
|
201
|
+
}
|
|
202
|
+
callees(name, limit = 300) {
|
|
203
|
+
const out = [];
|
|
204
|
+
for (const source of this.projectSources()) {
|
|
205
|
+
const visit = (node, insideOwner = false) => {
|
|
206
|
+
const n = nodeName(node);
|
|
207
|
+
const matchesOwner = !!n && ts.isIdentifier(n) && n.text === name && (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node) || ts.isFunctionExpression(node));
|
|
208
|
+
const nextInside = insideOwner || matchesOwner;
|
|
209
|
+
if (nextInside && ts.isCallExpression(node)) {
|
|
210
|
+
const expression = node.expression;
|
|
211
|
+
const called = ts.isIdentifier(expression) ? expression.text : ts.isPropertyAccessExpression(expression) ? expression.name.text : null;
|
|
212
|
+
if (called)
|
|
213
|
+
out.push({ path: rel(this.root, source.fileName), ...pos(source, node), kind: "CallExpression", name: called });
|
|
214
|
+
}
|
|
215
|
+
if (out.length < limit)
|
|
216
|
+
ts.forEachChild(node, (child) => visit(child, nextInside));
|
|
217
|
+
};
|
|
218
|
+
visit(source, false);
|
|
219
|
+
if (out.length >= limit)
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
return out;
|
|
223
|
+
}
|
|
224
|
+
importGraph(limit = 2000) {
|
|
225
|
+
const edges = [];
|
|
226
|
+
for (const source of this.projectSources()) {
|
|
227
|
+
for (const statement of source.statements) {
|
|
228
|
+
if (ts.isImportDeclaration(statement) || ts.isExportDeclaration(statement)) {
|
|
229
|
+
const spec = statement.moduleSpecifier;
|
|
230
|
+
if (spec && ts.isStringLiteral(spec))
|
|
231
|
+
edges.push({ from: rel(this.root, source.fileName), to: spec.text, kind: ts.isImportDeclaration(statement) ? "import" : "export" });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (edges.length >= limit)
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
return edges.slice(0, limit);
|
|
238
|
+
}
|
|
239
|
+
diagnostics(limit = 500) {
|
|
240
|
+
this.build();
|
|
241
|
+
const out = [];
|
|
242
|
+
const seen = new Set();
|
|
243
|
+
for (const project of this.projects) {
|
|
244
|
+
for (const d of ts.getPreEmitDiagnostics(project.program)) {
|
|
245
|
+
const message = ts.flattenDiagnosticMessageText(d.messageText, "\n");
|
|
246
|
+
const file = d.file?.fileName;
|
|
247
|
+
const key = `${file ?? ""}:${d.start ?? -1}:${d.code}:${message}`;
|
|
248
|
+
if (seen.has(key))
|
|
249
|
+
continue;
|
|
250
|
+
seen.add(key);
|
|
251
|
+
if (!d.file || d.start == null)
|
|
252
|
+
out.push({ message, code: d.code, category: ts.DiagnosticCategory[d.category] });
|
|
253
|
+
else {
|
|
254
|
+
const p = d.file.getLineAndCharacterOfPosition(d.start);
|
|
255
|
+
out.push({ path: rel(this.root, d.file.fileName), line: p.line + 1, column: p.character + 1, message, code: d.code, category: ts.DiagnosticCategory[d.category] });
|
|
256
|
+
}
|
|
257
|
+
if (out.length >= limit)
|
|
258
|
+
return out;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return out;
|
|
262
|
+
}
|
|
263
|
+
}
|
package/dist/state.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { mkdir, open, readFile, rename, unlink, chmod, appendFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
export const DEFAULT_STATE_DIR = process.env.CODELOCAL_STATE_DIR ?? join(os.homedir(), ".codelocal");
|
|
6
|
+
export async function ensurePrivateDir(dir = DEFAULT_STATE_DIR) {
|
|
7
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
8
|
+
if (process.platform !== "win32")
|
|
9
|
+
await chmod(dir, 0o700).catch(() => undefined);
|
|
10
|
+
return dir;
|
|
11
|
+
}
|
|
12
|
+
export async function readJsonFile(file, fallback) {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(await readFile(file, "utf8"));
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return fallback;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export async function writeJsonAtomic(file, value) {
|
|
21
|
+
const parent = dirname(file);
|
|
22
|
+
await ensurePrivateDir(parent);
|
|
23
|
+
const temp = join(parent, `.${randomUUID()}.tmp`);
|
|
24
|
+
let created = false;
|
|
25
|
+
try {
|
|
26
|
+
const handle = await open(temp, "wx", 0o600);
|
|
27
|
+
created = true;
|
|
28
|
+
try {
|
|
29
|
+
if (process.platform !== "win32")
|
|
30
|
+
await handle.chmod(0o600).catch(() => undefined);
|
|
31
|
+
await handle.writeFile(`${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
32
|
+
await handle.sync();
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
await handle.close();
|
|
36
|
+
}
|
|
37
|
+
await rename(temp, file);
|
|
38
|
+
created = false;
|
|
39
|
+
if (process.platform !== "win32")
|
|
40
|
+
await chmod(file, 0o600).catch(() => undefined);
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
if (created)
|
|
44
|
+
await unlink(temp).catch(() => undefined);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export async function appendPrivateJsonl(file, value) {
|
|
48
|
+
await ensurePrivateDir(dirname(file));
|
|
49
|
+
await appendFile(file, `${JSON.stringify(value)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
50
|
+
if (process.platform !== "win32")
|
|
51
|
+
await chmod(file, 0o600).catch(() => undefined);
|
|
52
|
+
}
|
|
53
|
+
export class IdempotencyJournal {
|
|
54
|
+
file;
|
|
55
|
+
maxEntries;
|
|
56
|
+
map = new Map();
|
|
57
|
+
loaded = false;
|
|
58
|
+
constructor(file = join(DEFAULT_STATE_DIR, "journal.json"), maxEntries = 1000) {
|
|
59
|
+
this.file = file;
|
|
60
|
+
this.maxEntries = maxEntries;
|
|
61
|
+
}
|
|
62
|
+
async load() {
|
|
63
|
+
if (this.loaded)
|
|
64
|
+
return;
|
|
65
|
+
const entries = await readJsonFile(this.file, []);
|
|
66
|
+
for (const entry of entries)
|
|
67
|
+
this.map.set(entry.key, entry);
|
|
68
|
+
this.loaded = true;
|
|
69
|
+
}
|
|
70
|
+
async get(key) {
|
|
71
|
+
await this.load();
|
|
72
|
+
return this.map.get(key) ?? null;
|
|
73
|
+
}
|
|
74
|
+
async start(key, operation) {
|
|
75
|
+
await this.load();
|
|
76
|
+
const existing = this.map.get(key);
|
|
77
|
+
if (existing)
|
|
78
|
+
return existing;
|
|
79
|
+
const entry = { key, operation, startedAt: Date.now(), status: "started" };
|
|
80
|
+
this.map.set(key, entry);
|
|
81
|
+
await this.flush();
|
|
82
|
+
return entry;
|
|
83
|
+
}
|
|
84
|
+
async complete(key, result) {
|
|
85
|
+
await this.load();
|
|
86
|
+
const entry = this.map.get(key);
|
|
87
|
+
if (!entry)
|
|
88
|
+
return;
|
|
89
|
+
entry.status = "completed";
|
|
90
|
+
entry.completedAt = Date.now();
|
|
91
|
+
entry.result = result;
|
|
92
|
+
delete entry.error;
|
|
93
|
+
await this.flush();
|
|
94
|
+
}
|
|
95
|
+
async fail(key, error) {
|
|
96
|
+
await this.load();
|
|
97
|
+
const entry = this.map.get(key);
|
|
98
|
+
if (!entry)
|
|
99
|
+
return;
|
|
100
|
+
entry.status = "failed";
|
|
101
|
+
entry.completedAt = Date.now();
|
|
102
|
+
entry.error = error;
|
|
103
|
+
await this.flush();
|
|
104
|
+
}
|
|
105
|
+
async flush() {
|
|
106
|
+
const values = [...this.map.values()].sort((a, b) => b.startedAt - a.startedAt).slice(0, this.maxEntries);
|
|
107
|
+
this.map = new Map(values.map((x) => [x.key, x]));
|
|
108
|
+
await writeJsonAtomic(this.file, values);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
3
|
+
import { appendPrivateJsonl, DEFAULT_STATE_DIR } from "./state.js";
|
|
4
|
+
import { redactCommand } from "./security-policy.js";
|
|
5
|
+
export class TerminalHistory {
|
|
6
|
+
file;
|
|
7
|
+
starts = new Map();
|
|
8
|
+
constructor(file = process.env.CODELOCAL_TERMINAL_HISTORY_PATH ?? path.join(DEFAULT_STATE_DIR, "terminal-history.jsonl")) {
|
|
9
|
+
this.file = file;
|
|
10
|
+
}
|
|
11
|
+
async started(input) {
|
|
12
|
+
const record = {
|
|
13
|
+
ts: new Date().toISOString(),
|
|
14
|
+
event: "started",
|
|
15
|
+
workspaceKey: input.workspaceKey,
|
|
16
|
+
processId: input.processId,
|
|
17
|
+
requestId: input.requestId,
|
|
18
|
+
sessionId: input.sessionId,
|
|
19
|
+
cwd: input.cwd,
|
|
20
|
+
command: redactCommand(input.command).slice(0, 4000),
|
|
21
|
+
riskLevel: input.riskLevel,
|
|
22
|
+
matchedRules: input.matchedRules,
|
|
23
|
+
approval: input.approval,
|
|
24
|
+
startedAt: input.startedAt,
|
|
25
|
+
executionMode: input.executionMode,
|
|
26
|
+
};
|
|
27
|
+
this.starts.set(record.processId, record);
|
|
28
|
+
await appendPrivateJsonl(this.file, record);
|
|
29
|
+
return record;
|
|
30
|
+
}
|
|
31
|
+
async finished(record) {
|
|
32
|
+
const start = this.starts.get(record.processId);
|
|
33
|
+
const finishedAt = Date.now();
|
|
34
|
+
const output = {
|
|
35
|
+
ts: new Date().toISOString(),
|
|
36
|
+
event: "finished",
|
|
37
|
+
workspaceKey: record.workspaceKey,
|
|
38
|
+
processId: record.processId,
|
|
39
|
+
requestId: start?.requestId,
|
|
40
|
+
sessionId: start?.sessionId,
|
|
41
|
+
cwd: start?.cwd ?? record.cwd,
|
|
42
|
+
command: start?.command ?? redactCommand(record.command).slice(0, 4000),
|
|
43
|
+
riskLevel: start?.riskLevel ?? "UNKNOWN",
|
|
44
|
+
matchedRules: start?.matchedRules ?? [],
|
|
45
|
+
approval: start?.approval ?? "automatic",
|
|
46
|
+
startedAt: record.startedAt,
|
|
47
|
+
finishedAt,
|
|
48
|
+
durationMs: Math.max(0, finishedAt - record.startedAt),
|
|
49
|
+
exitCode: record.exitCode,
|
|
50
|
+
status: record.status,
|
|
51
|
+
executionMode: record.executionMode,
|
|
52
|
+
};
|
|
53
|
+
this.starts.delete(record.processId);
|
|
54
|
+
await appendPrivateJsonl(this.file, output);
|
|
55
|
+
return output;
|
|
56
|
+
}
|
|
57
|
+
async tail(maxBytes = 4 * 1024 * 1024) {
|
|
58
|
+
let handle = null;
|
|
59
|
+
try {
|
|
60
|
+
handle = await fs.open(this.file, "r");
|
|
61
|
+
const stat = await handle.stat();
|
|
62
|
+
const length = Math.min(stat.size, maxBytes);
|
|
63
|
+
const buffer = Buffer.alloc(length);
|
|
64
|
+
await handle.read(buffer, 0, length, Math.max(0, stat.size - length));
|
|
65
|
+
let text = buffer.toString("utf8");
|
|
66
|
+
if (stat.size > length)
|
|
67
|
+
text = text.slice(text.indexOf("\n") + 1);
|
|
68
|
+
return text;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return "";
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
await handle?.close().catch(() => undefined);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async query(options = {}) {
|
|
78
|
+
const raw = await this.tail();
|
|
79
|
+
const query = String(options.query ?? "").trim().toLowerCase();
|
|
80
|
+
const event = options.event ?? "started";
|
|
81
|
+
const limit = Math.max(1, Math.min(Number(options.limit ?? 50), 500));
|
|
82
|
+
const records = [];
|
|
83
|
+
for (const line of raw.split(/\r?\n/).reverse()) {
|
|
84
|
+
if (!line.trim())
|
|
85
|
+
continue;
|
|
86
|
+
try {
|
|
87
|
+
const record = JSON.parse(line);
|
|
88
|
+
if (options.workspaceKey && record.workspaceKey !== options.workspaceKey)
|
|
89
|
+
continue;
|
|
90
|
+
if (event !== "all" && record.event !== event)
|
|
91
|
+
continue;
|
|
92
|
+
if (query && !`${record.command} ${record.cwd} ${record.riskLevel} ${record.status ?? ""}`.toLowerCase().includes(query))
|
|
93
|
+
continue;
|
|
94
|
+
records.push(record);
|
|
95
|
+
if (records.length >= limit)
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
catch { }
|
|
99
|
+
}
|
|
100
|
+
return { records, truncatedByReadWindow: Buffer.byteLength(raw, "utf8") >= 4 * 1024 * 1024 };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
function diagnosticKey(d) {
|
|
4
|
+
return `${d.path ?? ""}:${d.line ?? 0}:${d.column ?? 0}:${d.code ?? ""}:${d.message ?? ""}`;
|
|
5
|
+
}
|
|
6
|
+
async function gitDiff(root) {
|
|
7
|
+
return new Promise((resolve) => {
|
|
8
|
+
const child = spawn("git", ["diff", "--no-ext-diff", "--unified=2"], { cwd: root, stdio: ["ignore", "pipe", "pipe"] });
|
|
9
|
+
let output = "";
|
|
10
|
+
child.stdout.on("data", (d) => { output = (output + d.toString()).slice(-1_500_000); });
|
|
11
|
+
child.stderr.on("data", (d) => { output = (output + d.toString()).slice(-1_500_000); });
|
|
12
|
+
child.on("error", () => resolve(""));
|
|
13
|
+
child.on("close", () => resolve(output));
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
export class VerificationEngine {
|
|
17
|
+
root;
|
|
18
|
+
semantic;
|
|
19
|
+
context;
|
|
20
|
+
snapshots = new Map();
|
|
21
|
+
constructor(root, semantic, context) {
|
|
22
|
+
this.root = root;
|
|
23
|
+
this.semantic = semantic;
|
|
24
|
+
this.context = context;
|
|
25
|
+
}
|
|
26
|
+
async snapshotDiagnostics(paths = []) {
|
|
27
|
+
const diagnostics = [];
|
|
28
|
+
if (paths.length) {
|
|
29
|
+
for (const p of [...new Set(paths)])
|
|
30
|
+
diagnostics.push(...await this.semantic.diagnostics(p, 500));
|
|
31
|
+
}
|
|
32
|
+
else
|
|
33
|
+
diagnostics.push(...await this.semantic.diagnostics(undefined, 1000));
|
|
34
|
+
const snapshotId = randomUUID();
|
|
35
|
+
this.snapshots.set(snapshotId, diagnostics);
|
|
36
|
+
if (this.snapshots.size > 30)
|
|
37
|
+
this.snapshots.delete(this.snapshots.keys().next().value);
|
|
38
|
+
return { snapshotId, diagnostics };
|
|
39
|
+
}
|
|
40
|
+
compare(baselineId, currentDiagnostics) {
|
|
41
|
+
const baseline = this.snapshots.get(baselineId);
|
|
42
|
+
if (!baseline)
|
|
43
|
+
throw new Error("Unknown diagnostic baseline.");
|
|
44
|
+
const before = new Map(baseline.map((d) => [diagnosticKey(d), d]));
|
|
45
|
+
const after = new Map(currentDiagnostics.map((d) => [diagnosticKey(d), d]));
|
|
46
|
+
return {
|
|
47
|
+
new: [...after.entries()].filter(([k]) => !before.has(k)).map(([, d]) => d),
|
|
48
|
+
persisting: [...after.entries()].filter(([k]) => before.has(k)).map(([, d]) => d),
|
|
49
|
+
resolved: [...before.entries()].filter(([k]) => !after.has(k)).map(([, d]) => d),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
async verify(paths = [], baselineId) {
|
|
53
|
+
const current = await this.snapshotDiagnostics(paths);
|
|
54
|
+
const project = await this.context.map();
|
|
55
|
+
const diff = await gitDiff(this.root);
|
|
56
|
+
const checks = [...project.typecheckCommands, ...project.lintCommands, ...project.testCommands].slice(0, 8);
|
|
57
|
+
return {
|
|
58
|
+
diagnostics: current.diagnostics,
|
|
59
|
+
diagnosticSnapshotId: current.snapshotId,
|
|
60
|
+
regression: baselineId ? this.compare(baselineId, current.diagnostics) : null,
|
|
61
|
+
recommendedChecks: checks,
|
|
62
|
+
diff: diff.slice(-1_000_000),
|
|
63
|
+
diffTruncated: diff.length > 1_000_000,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|